Spaces:
Runtime error
Runtime error
| """Нормализация номеров РФ с позиционной коррекцией 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)) |