61 lines
3.0 KiB
Python
61 lines
3.0 KiB
Python
"""Очистка 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()
|