Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
52 lines
2.3 KiB
Python
52 lines
2.3 KiB
Python
"""Совпадение наименований строки формы со справочными (05-domain-rules §11.9, спека «Совпадение наименований»)."""
|
||
|
||
import difflib
|
||
import re
|
||
from collections.abc import Iterable
|
||
|
||
__all__ = ["names_match", "normalize", "section_number"]
|
||
|
||
_NON_ALNUM = re.compile(r"[\W_]+")
|
||
_TOTAL_PREFIX = "итого по разделу"
|
||
_MIN_CONTAINED = 5
|
||
_MIN_RATIO = 0.8
|
||
_ROMAN_LATIN = str.maketrans({"і": "i", "х": "x"}) # кириллические «і», «х» -> латиница
|
||
_ROMAN_HEAD = re.compile(r"[ivx]+")
|
||
|
||
|
||
def normalize(text: str) -> str:
|
||
"""Нижний регистр, `ё` -> `е`, всё кроме букв и цифр — пробел, схлопывание, `strip`."""
|
||
return _NON_ALNUM.sub(" ", text.lower().replace("ё", "е")).strip()
|
||
|
||
|
||
def section_number(normalized: str) -> str | None:
|
||
"""Номер раздела «Итого по разделу …» (строчными буквами i, v, x) из нормализованного имени; остаток слова —
|
||
хвост OCR («v1» -> «v») — отбрасывается. Имя не «итого по разделу» или номер не выделен — `None`."""
|
||
if not normalized.startswith(_TOTAL_PREFIX):
|
||
return None
|
||
words = normalized[len(_TOTAL_PREFIX) :].split()
|
||
if not words:
|
||
return None
|
||
head = _ROMAN_HEAD.match(words[0].translate(_ROMAN_LATIN))
|
||
return head.group() if head else None
|
||
|
||
|
||
def _match_pair(a: str, b: str) -> bool:
|
||
if a.startswith(_TOTAL_PREFIX) or b.startswith(_TOTAL_PREFIX):
|
||
na, nb = section_number(a), section_number(b)
|
||
return a == b if na is None or nb is None else na == nb
|
||
if a == b:
|
||
return True
|
||
short, long_ = sorted((a, b), key=len)
|
||
if len(short) >= _MIN_CONTAINED and short in long_:
|
||
return True
|
||
return difflib.SequenceMatcher(None, a, b).ratio() >= _MIN_RATIO
|
||
|
||
|
||
def names_match(recognized: str | None, references: Iterable[str]) -> bool:
|
||
"""Наименование совпало хотя бы с одним справочным; пустое имя или нет справочных — `False`."""
|
||
name = normalize(recognized or "")
|
||
if not name:
|
||
return False
|
||
return any(ref and _match_pair(name, ref) for ref in map(normalize, references))
|