Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,56 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed — names left unmasked
- A full name before a rank that is followed by more words was not masked
at all: `Петренко Іван Іванович доповів капітану про виконання.`,
`Петренко Іван Іванович, 1990 р.н., солдат за контрактом`. The parser
looked for a name only after the first rank of the line.
- A line break inside a name left part of it open: the surname in
`Петренка\nІвана Івановича`, the patronymic in `Петренка Івана\nІвановича`,
`Шановний Віталію\nРомановичу!`, the rank in `старшого сержанта\nПетренка …`.
Such lines are now parsed together, and the line break stays where it was.
- Initials and a surname on adjacent lines (`… Л.І.\nКоваль`,
`Петренко\nО.П.`) are masked; one initial at the end of a line
(`Додаток А.`) is still not taken for a name.
- In a run of initials and surnames (`Ю.А. Зінченко Н.І. Савчук`) only the
middle pair was masked, and `Ю.А.` and `Савчук` stayed open; the masker now
takes the set of matches that covers the most names.
- A double surname in capitals with initials (`ПЕТРЕНКО-КОВАЛЬ І.М.`) kept
one part open; a surname in capitals with an apostrophe (`ПИЛИП'ЮК О.І.`)
was not masked.
- The surname stayed open after a heading in capitals (`ВИТЯГ З НАКАЗУ
Коваль Сергій Васильович`) and for an adjective surname in an oblique case
(`Рапорт Білого Ярослава Миколайовича`).
- `Шановна Любове Андріївно!`: the vocative `Любове` was not recognised, so
the name got a surname mask and the patronymic a male name.

### Fixed — unmasking did not restore the original
- A rank `… медичної служби` masked as `генерал-майор медичної служби` was
not restored (the unmasker found `майор медичної служби` inside it).
- A rank written with a non-breaking space, a tab or several spaces between
its words (`старший майстер-сержант`) came back with a plain space when
the mask had a different number of words. The mask now keeps the number of
words, so the separators carry over and back.
- A rank broken across lines (`старшого\nсержанта`) came back joined with a
space; the line break is now restored. `normalize_broken_ranks()` itself
is unchanged.
- An open rank `… медичної служби` (one without a name) could become the
mask of another rank, and unmasking then replaced it.
- A patronymic before initials (`Мороз Ольги Богданівни Л.А. Мороз`) was
masked as a surname and then masked again.
- Two different forms with the same mask in one line (`Іване` and `Івану`
both masked as `Ігорю`) were restored the wrong way round: masks in a line
are now recorded in text order.
- A brigade number broken across lines lost the rest of its name in the
masked text (`60 окремої\nмеханізованої бригади` → `30 окремої`).

### Changed — masks
- Naval ranks get naval masks: `капітан 1-го рангу` and `капітан-лейтенант`
were masked as army ranks (`майор`, `лейтенант`).
- The feminine genitive of an adjective surname (`Білої`) gets the same
synthetic stem as its other forms (`Біла`, `Білій`).

### Changed — release process
- The version is bumped at release time, not on every commit. Changes
collect under `## [Unreleased]`, and the release commit sets the version
Expand Down
4 changes: 4 additions & 0 deletions datamasking/masking/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ def set_faker_locale(locale: str) -> None:
# жіноча форма маскується чоловічою і unmask повертає «солдата» замість
# «солдатки» (v3.1.14). Задається рушієм на час обробки документа.
FEMININE_RANK_BASES: frozenset = frozenset()
# Базові звання, які в поточному документі записані з нерозривним пробілом,
# табом чи кількома пробілами між словами: маска має ту саму кількість слів,
# щоб роздільники перенеслись у неї й назад (після 3.1.20). Задається рушієм.
SPACED_RANK_BASES: frozenset = frozenset()

# ============================================================================
# СПИСКИ ТА КОНСТАНТИ
Expand Down
31 changes: 25 additions & 6 deletions datamasking/masking/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,18 @@ def parse_hybrid_line(line: str) -> Tuple[Optional[str], Optional[str], Optional
# якір у рядку. Раніше брався лише ПЕРШИЙ якір; якщо за ним не було
# ≥2 слів імені (наприклад, «Сірченко Е.Г.» — уже замаскована фаза
# ініціалів), увесь рядок кидався і справжній ПІБ далі лишався відкритим.
# Якорі — по всьому рядку, крім слів звань: до 3.1.20 включно бралися лише
# якорі ПІСЛЯ першого звання, і «Петренко Іван Іванович доповів
# капітану про виконання» лишалось відкритим цілком (звання знайдено,
# після нього ПІБ немає, а ПІБ перед ним не розглядався)
rank_tokens: Set[int] = set()
for _r_index, _r_form, r_first, r_count in rank_matches:
rank_tokens.update(range(r_first, r_first + r_count))
starts = []
if pib_start_index != -1:
starts.append((pib_start_index, True))
for i, part in enumerate(parts):
if i > pib_start_index and is_pib_anchor(part):
if i != pib_start_index and i not in rank_tokens and is_pib_anchor(part):
starts.append((i, False))
if not starts: return None, None, identifier

Expand All @@ -399,8 +406,11 @@ def parse_hybrid_line(line: str) -> Tuple[Optional[str], Optional[str], Optional
pib_words = _extract_pib_words(parts, start, after_rank)
if not pib_words:
continue
# Посада чи назва підрозділу капсом без по батькові — не ПІБ (v3.1.17)
if not after_rank and _caps_block_without_patronymic(parts, start):
# Посада чи назва підрозділу капсом без по батькові — не ПІБ (v3.1.17).
# Кандидат із по батькові у вікні («НАКАЗУ БІЛИЙ Тарас Васильович») —
# не назва, навіть коли капсом стоїть і заголовок перед ним (після 3.1.20)
if (not after_rank and _caps_block_without_patronymic(parts, start)
and not _strong_pib_words(pib_words)):
continue
# «Заява Петренка Олега Петровича», «Характеристика Коваль Тетяни
# Сергіївни»: слово з великої перед ПІБ потрапляло у вікно з трьох
Expand Down Expand Up @@ -453,7 +463,11 @@ def _caps_block_without_patronymic(parts: List[str], start: int) -> bool:
собою. До 3.1.17 назва посади маскувалась як «МАСАҐАНИКОМ ІГОР ОРЕСТОВИЧ»."""
# Блок рахується в обидва боки від початку кандидата: «СТАРШОГО ОПЕРАТОРА
# РОЗРАХУНКУ» відкидалось із першого слова, а з другого («ОПЕРАТОРА
# РОЗРАХУНКУ») знову бралось за ПІБ
# РОЗРАХУНКУ») знову бралось за ПІБ. Кандидат не капсом — не частина
# блоку: «ВИТЯГ З НАКАЗУ Коваль Сергій Васильович» (до 3.1.20 включно прізвище
# лишалось відкритим, бо капсом стояв заголовок перед ним)
if not _caps_word(parts[start]):
return False
begin = start
while begin > 0 and _caps_word(parts[begin - 1]) and parts[begin - 1].rstrip(_cfg.QUOTE_CHARS)[-1:] not in ',.;:!?':
begin -= 1
Expand Down Expand Up @@ -509,8 +523,13 @@ def roles_plausible(pib_words: List[str], after_rank: bool = False) -> bool:
if len(surname_low) >= 7 and surname_low.endswith(('ом', 'ем')) \
and not name_low.endswith(_INSTRUMENTAL_NAME_ENDINGS):
return False
if surname_low.endswith(('ого', 'ього', 'ому', 'ьому')) and analyze_name(name_low).case == 'nominative':
return False
if surname_low.endswith(('ого', 'ього', 'ому', 'ьому')):
# Відмінок імені — за по батькові, якщо воно є: «Білого Ярослава
# Миколайовича» — родовий від Ярослав, а не жіноче «Ярослава» в
# називному (до 3.1.20 включно прізвище лишалось відкритим)
pat_case = analyze_patronymic(patronymic).case if patronymic else None
if (pat_case or analyze_name(name_low).case) == 'nominative':
return False
return True


Expand Down
4 changes: 4 additions & 0 deletions datamasking/masking/declension.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ def _female_form(f: str, names: Set[str], strict: bool) -> Optional[Tuple[str, s
return _pick([f[:-1] + 'а'], VOCATIVE, names, strict)
if n >= 3 and f.endswith('є'):
return _pick([f[:-1] + 'я'], VOCATIVE, names, strict)
if n >= 4 and f.endswith('е') and f[:-1] in names:
# Любове (III відміна) — кличний; лише відоме ім'я: іншомовні на -е
# (Рене) лишаються в називному
return f[:-1], VOCATIVE, True
if f.endswith(('а', 'я')):
return None if strict else (f, NOMINATIVE, False)
# Любов, Нінель — не відмінюються тут
Expand Down
Loading
Loading