Andrey Epifancev 2973cc7966 Документирование warnings адаптера, тест листа-продолжения, обезличенная фамилия (M-2, M-3, M-4)
M-2: README и docstring normalize_candidate — при отсутствии документа
candidate.warnings хост обрабатывает сам. M-3: тесты сопоставления
по дате (переставленные даты) и по порядку (дубль даты).
M-4: «Косарев» заменён на «Иванов» в тестах и docstring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
2026-10-18 14:29:10 +04:00

61 lines
3.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Очистка OCR-текста: буквы-двойники, нормализация подписей и меток (§11.1 п. 1, 6)."""
import re
from collections.abc import Mapping
# первая строка из цифр, пробелов, `|`, точек, запятых и двоеточий: `|` в ней — разделитель клеток формы
_CELL_SEPARATOR_LINE = re.compile(r"[\s\d|•·.:,]*")
_INVISIBLE = re.compile("[\u00ad\u200b\ufeff]")
_SEPARATORS = re.compile(r"[\s\-\u2010-\u2015\u2212]+")
_NON_CAPTION = re.compile(r"[^\w%]|_")
def replace_lookalikes(text: str, lookalikes: Mapping[str, str], *, anywhere: bool = False) -> str:
"""Двойники → цифры вплотную к цифре (несколько проходов); `anywhere` — без условия соседства."""
if not lookalikes:
return text
table = str.maketrans(dict(lookalikes))
if anywhere:
return text.translate(table)
chars = re.escape("".join(lookalikes))
near_digit = re.compile(rf"(?<=\d)[{chars}]|[{chars}](?=\d)")
previous = None
while previous != text: # «2ОО5» требует двух проходов
previous = text
text = near_digit.sub(lambda match: match.group(0).translate(table), text)
return text
def fix_lookalikes(text: str, lookalikes: Mapping[str, str], *, anywhere: bool = False) -> str:
"""NBSP → пробел; `|` — разделитель клеток, если первая строка из цифр и пунктуации; двойники → цифры."""
text = text.replace("\xa0", " ")
if _CELL_SEPARATOR_LINE.fullmatch(text.split("\n", 1)[0]):
text = text.replace("|", " ")
return replace_lookalikes(text, lookalikes, anywhere=anywhere)
def has_digit(text: str | None) -> bool:
return text is not None and any(char.isdigit() for char in text)
def is_disguised_digit(text: str, lookalikes: Mapping[str, str]) -> bool:
"""После замены двойников везде есть цифра и нет других букв («I», «| О» — да; «Иванов» — нет)."""
fixed = fix_lookalikes(text, lookalikes, anywhere=True)
return has_digit(fixed) and not any(char.isalpha() for char in fixed)
def normalize_caption(text: str | None) -> str:
"""Нижний регистр, `ё → е`, всё кроме букв, цифр и `%` → пробел, пробелы схлопнуты."""
if text is None:
return ""
lowered = text.lower().replace("ё", "е")
return " ".join(_NON_CAPTION.sub(" ", lowered).split())
def normalize_label(text: str | None) -> str:
"""Метки шапки: невидимые удалены, нижний регистр, `ё → е`, пробелы и дефисы/тире → пробел; пунктуация сохранена."""
if text is None:
return ""
lowered = _INVISIBLE.sub("", text).lower().replace("ё", "е")
return _SEPARATORS.sub(" ", lowered).strip()