"""Очистка 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()