"""Нормализация номеров РФ с позиционной коррекцией OCR (ГОСТ Р 50577, без региона).""" import re ALLOWED_CYR = "АВЕКМНОРСТУХ" LAT2CYR = dict(zip("ABEKMHOPCTYX", ALLOWED_CYR)) # ГОСТ-формат без региона: буква + 3 цифры + 2 буквы PLATE_RE = re.compile(rf"^[{ALLOWED_CYR}]\d{{3}}[{ALLOWED_CYR}]{{2}}$") # Коррекция «цифра -> похожая буква» (для буквенных позиций) DIGIT_TO_LETTER = { "0": "О", "8": "В", "4": "А", # иногда 4 читается как А "6": "В", "1": "Т", # реже } # Коррекция «буква -> похожая цифра» (для цифровых позиций) LETTER_TO_DIGIT = { "О": "0", "O": "0", # русская и латинская О "В": "8", "B": "8", "З": "3", "Ч": "4", "Б": "6", "Т": "1", "А": "4", "Е": "0", } def latin_to_cyrillic(text: str) -> str: return "".join(LAT2CYR.get(ch, ch) for ch in text.upper()) def clean_ocr_output(raw: str) -> str: """Убираем padding, пробелы, точки, дефисы; латиницу -> кириллицу.""" s = raw.upper() for ch in ("_", " ", ".", "-", "·", "•"): s = s.replace(ch, "") return latin_to_cyrillic(s) def _fix_to_letter(ch: str) -> str | None: """Привести символ к букве из алфавита ГОСТ (если возможно).""" if ch in ALLOWED_CYR: return ch return DIGIT_TO_LETTER.get(ch) def _fix_to_digit(ch: str) -> str | None: """Привести символ к цифре (если возможно).""" if ch.isdigit(): return ch return LETTER_TO_DIGIT.get(ch) def normalize_plate(raw: str) -> tuple[str, bool]: """ Возвращает (номер_без_региона, прошёл_ли_ГОСТ). Стратегия: 1. Очистить строку. 2. Применить ПОЗИЦИОННУЮ МАСКУ: пройти по символам, собирая структуру [буква][цифра][цифра][цифра][буква][буква], корректируя путаницу 0<->О, 8<->В и т.п. по ожидаемому типу позиции. 3. Если собрали 6 валидных символов — это номер, ГОСТ = True. """ cleaned = clean_ocr_output(raw) # Прямое совпадение if PLATE_RE.match(cleaned): return cleaned, True # Позиционная сборка: пытаемся начать с каждой позиции, где стоит буква # (на случай мусора в начале строки) for start in range(len(cleaned)): result = _try_build_plate(cleaned[start:]) if result: return result, True # Не собралось — вернём очищенную строку как есть, ГОСТ = False return cleaned, False def _try_build_plate(s: str) -> str | None: """ Пытается собрать номер по маске Б-Ц-Ц-Ц-Б-Б из начала строки s, исправляя ошибки OCR по типу позиции. Возвращает 6-символьный номер или None. """ # Маска: True = ожидаем букву, False = ожидаем цифру mask = [True, False, False, False, True, True] out = [] si = 0 # индекс в строке for expect_letter in mask: if si >= len(s): return None ch = s[si] if expect_letter: fixed = _fix_to_letter(ch) else: fixed = _fix_to_digit(ch) if fixed is None: # Символ не подходит и не корректируется — этот старт неудачный return None out.append(fixed) si += 1 plate = "".join(out) return plate if PLATE_RE.match(plate) else None def is_valid_gost(text: str) -> bool: """ГОСТ = формат «буква + 3 цифры + 2 буквы».""" return bool(PLATE_RE.match(text))