📊

OCR российских документов

Извлечение данных из сканов российских удостоверяющих документов (паспорт, СНИЛС, ИНН, водительское удостоверение, ОГРН/ОГРНИП) с валидацией по контрольным суммам

Системный промпт

OCR российских удостоверяющих документов

Этот skill содержит паттерны извлечения данных из сканов российских документов с использованием vision-модели и валидацией по контрольным суммам.

Поддерживаемые документы:

  • Паспорт гражданина РФ
  • СНИЛС (страховой номер)
  • ИНН (физлицо / юрлицо)
  • Водительское удостоверение
  • ОГРН / ОГРНИП

БЛОК 1: Паспорт гражданина РФ

Структура документа

Основной разворот (стр. 2-3):

┌─────────────────────────────────────────────────────────┐
│  ПАСПОРТ ГРАЖДАНИНА                                     │
│  РОССИЙСКОЙ ФЕДЕРАЦИИ              [Серия] [Номер]      │
│                                     XX XX   XXXXXX      │
│  [Фото]    Фамилия: ИВАНОВ                              │
│            Имя: ИВАН                                     │
│            Отчество: ИВАНОВИЧ                           │
│            Пол: МУЖ.  Дата рождения: 01.01.1990         │
│            Место рождения: ГОР. МОСКВА                  │
│                                                          │
│  Дата выдачи: 15.01.2020                                │
│  Кем выдан: ОТДЕЛЕНИЕМ ПО РАЙОНУ ХХХХХ                  │
│  УФМС РОССИИ ПО ГОР. МОСКВЕ                             │
│  Код подразделения: 770-001                              │
└─────────────────────────────────────────────────────────┘

Поля для извлечения

ПолеФорматРасположение
СерияXX XX (4 цифры, 2+2)Верхний правый угол
НомерXXXXXX (6 цифр)После серии
ФамилияКириллица, UPPERCASEПосле "Фамилия"
ИмяКириллица, UPPERCASEПосле "Имя"
ОтчествоКириллица, UPPERCASEПосле "Отчество"
ПолМУЖ. / ЖЕН.После "Пол"
Дата рожденияДД.ММ.ГГГГПосле "Дата рождения"
Место рожденияТекстПосле "Место рождения"
Дата выдачиДД.ММ.ГГГГЛевый нижний блок
Кем выданТекст (2-3 строки)После "Кем выдан"
Код подразделенияXXX-XXXПосле "Код подразделения"

Паттерны поиска в тексте

import re

# Серия и номер паспорта
passport_pattern = r"(\d{2})\s*(\d{2})\s+(\d{6})"
# Группы: серия_1, серия_2, номер

# Код подразделения
code_pattern = r"(\d{3})-(\d{3})"

# Дата (ДД.ММ.ГГГГ)
date_pattern = r"(\d{2})\.(\d{2})\.(\d{4})"

# ФИО (после ключевых слов)
surname_pattern = r"[Фф]амилия[:\s]*([А-ЯЁ][А-ЯЁа-яё\-]+)"
name_pattern = r"[Ии]мя[:\s]*([А-ЯЁ][А-ЯЁа-яё\-]+)"
patronymic_pattern = r"[Оо]тчество[:\s]*([А-ЯЁ][А-ЯЁа-яё\-]+)"

# Пол
gender_pattern = r"[Пп]ол[:\s]*(МУЖ\.?|ЖЕН\.?|[Мм]ужской|[Жж]енский)"

Промпт для vision-модели

Извлеки данные из скана паспорта РФ. Верни JSON со следующими полями:

{
  "series": "XX XX",        // серия паспорта (4 цифры)
  "number": "XXXXXX",       // номер паспорта (6 цифр)
  "surname": "ФАМИЛИЯ",     // фамилия (кириллица)
  "name": "ИМЯ",            // имя (кириллица)
  "patronymic": "ОТЧЕСТВО", // отчество (кириллица), null если отсутствует
  "gender": "М" или "Ж",    // пол
  "birth_date": "ДД.ММ.ГГГГ", // дата рождения
  "birth_place": "...",     // место рождения
  "issue_date": "ДД.ММ.ГГГГ", // дата выдачи
  "issuer": "...",          // кем выдан (полный текст)
  "issuer_code": "XXX-XXX"  // код подразделения
}

Если поле не читается или отсутствует — верни null.
Серия паспорта: первые 2 цифры — код региона, вторые 2 — год бланка.

Валидация серии паспорта

def validate_passport_series(series: str) -> dict:
    """
    Валидация серии паспорта РФ.
    Серия: XXYY, где XX — код региона (01-99), YY — год бланка.
    """
    series_clean = series.replace(" ", "")
    if not re.match(r"^\d{4}$", series_clean):
        return {"valid": False, "error": "Серия должна содержать 4 цифры"}

    region_code = int(series_clean[:2])
    year_code = int(series_clean[2:])

    # Коды регионов: 01-99 (некоторые не используются)
    valid_regions = set(range(1, 100)) - {20, 91}  # исключения

    if region_code not in valid_regions:
        return {"valid": False, "error": f"Недействительный код региона: {region_code:02d}"}

    # Год бланка: обычно 97-26 (1997-2026)
    if year_code < 97 and year_code > 30:
        return {"valid": False, "error": f"Подозрительный год бланка: {year_code:02d}"}

    return {
        "valid": True,
        "region_code": f"{region_code:02d}",
        "year_code": f"{year_code:02d}",
    }

Справочник кодов регионов

PASSPORT_REGION_CODES = {
    "01": "Республика Адыгея",
    "02": "Республика Башкортостан",
    "03": "Республика Бурятия",
    "04": "Республика Алтай",
    "05": "Республика Дагестан",
    "06": "Республика Ингушетия",
    "07": "Кабардино-Балкарская Республика",
    "08": "Республика Калмыкия",
    "09": "Карачаево-Черкесская Республика",
    "10": "Республика Карелия",
    "11": "Республика Коми",
    "12": "Республика Марий Эл",
    "13": "Республика Мордовия",
    "14": "Республика Саха (Якутия)",
    "15": "Республика Северная Осетия — Алания",
    "16": "Республика Татарстан",
    "17": "Республика Тыва",
    "18": "Удмуртская Республика",
    "19": "Республика Хакасия",
    "21": "Чувашская Республика",
    "22": "Алтайский край",
    "23": "Краснодарский край",
    "24": "Красноярский край",
    "25": "Приморский край",
    "26": "Ставропольский край",
    "27": "Хабаровский край",
    "28": "Амурская область",
    "29": "Архангельская область",
    "30": "Астраханская область",
    "31": "Белгородская область",
    "32": "Брянская область",
    "33": "Владимирская область",
    "34": "Волгоградская область",
    "35": "Вологодская область",
    "36": "Воронежская область",
    "37": "Ивановская область",
    "38": "Иркутская область",
    "39": "Калининградская область",
    "40": "Калужская область",
    "41": "Камчатский край",
    "42": "Кемеровская область",
    "43": "Кировская область",
    "44": "Костромская область",
    "45": "Курганская область",
    "46": "Курская область",
    "47": "Ленинградская область",
    "48": "Липецкая область",
    "49": "Магаданская область",
    "50": "Московская область",
    "51": "Мурманская область",
    "52": "Нижегородская область",
    "53": "Новгородская область",
    "54": "Новосибирская область",
    "55": "Омская область",
    "56": "Оренбургская область",
    "57": "Орловская область",
    "58": "Пензенская область",
    "59": "Пермский край",
    "60": "Псковская область",
    "61": "Ростовская область",
    "62": "Рязанская область",
    "63": "Самарская область",
    "64": "Саратовская область",
    "65": "Сахалинская область",
    "66": "Свердловская область",
    "67": "Смоленская область",
    "68": "Тамбовская область",
    "69": "Тверская область",
    "70": "Томская область",
    "71": "Тульская область",
    "72": "Тюменская область",
    "73": "Ульяновская область",
    "74": "Челябинская область",
    "75": "Забайкальский край",
    "76": "Ярославская область",
    "77": "г. Москва",
    "78": "г. Санкт-Петербург",
    "79": "Еврейская автономная область",
    "83": "Ненецкий автономный округ",
    "86": "Ханты-Мансийский автономный округ — Югра",
    "87": "Чукотский автономный округ",
    "89": "Ямало-Ненецкий автономный округ",
    "92": "г. Севастополь",
    "94": "Территории за пределами РФ",
}

Извлечение MRZ (машиночитаемая зона)

MRZ паспорта РФ — 2 строки по 44 символа в нижней части страницы с данными.

import re

def parse_passport_mrz(mrz_text: str) -> dict | None:
    """
    Парсинг MRZ паспорта РФ из текста (2 строки по 44 символа).
    """
    mrz_lines = [line.strip() for line in mrz_text.split("\n") if len(line.strip()) >= 40]

    if len(mrz_lines) < 2:
        return None

    line1 = mrz_lines[0].replace(" ", "")
    line2 = mrz_lines[1].replace(" ", "")

    return {
        "тип_документа": line1[0:2].replace("<", ""),
        "код_страны": line1[2:5],
        "фамилия": line1[5:].split("<<")[0].replace("<", " ").strip(),
        "имя_отчество": line1[5:].split("<<")[1].replace("<", " ").strip() if "<<" in line1[5:] else "",
        "номер_паспорта": line2[0:9].replace("<", ""),
        "гражданство": line2[10:13],
        "дата_рождения_mrz": line2[13:19],  # YYMMDD
        "пол_mrz": line2[20],
        "срок_действия": line2[21:27],  # YYMMDD
    }

Промпт для vision-модели: MRZ зона

Извлеки MRZ (машиночитаемую зону) из нижней части скана паспорта РФ.
MRZ содержит 2 строки по 44 символа. Верни их как есть, без изменений:

{
  "mrz_line1": "P<RUS...",  // первая строка MRZ
  "mrz_line2": "...",       // вторая строка MRZ
}

Если MRZ не видна или не читается — верни null.

БЛОК 2: СНИЛС

Формат номера

XXX-XXX-XXX YY
│   │   │   └── контрольное число (2 цифры)
└───┴───┴────── номер лицевого счёта (9 цифр)

Пример: 123-456-789 64

Алгоритм проверки контрольной суммы

def validate_snils(snils: str) -> dict:
    """
    Валидация СНИЛС по контрольному числу.

    Алгоритм:
    1. Берём 9 цифр номера
    2. Умножаем каждую цифру на её позицию справа (9, 8, 7, ..., 1)
    3. Суммируем произведения
    4. Вычисляем остаток от деления на 101
    5. Если остаток < 100 — это контрольное число
    6. Если остаток 100 или 101 — контрольное число = 00
    """
    # Очищаем от разделителей
    snils_clean = re.sub(r"[\s\-]", "", snils)

    if not re.match(r"^\d{11}$", snils_clean):
        return {"valid": False, "error": "СНИЛС должен содержать 11 цифр"}

    number = snils_clean[:9]
    checksum = int(snils_clean[9:])

    # Номера ≤ 001-001-998 не проверяются по контрольной сумме
    if int(number) <= 1001998:
        return {"valid": True, "formatted": f"{number[:3]}-{number[3:6]}-{number[6:9]} {checksum:02d}"}

    # Вычисляем контрольную сумму
    total = sum(int(d) * (9 - i) for i, d in enumerate(number))

    # Определяем контрольное число
    remainder = total % 101
    if remainder in (100, 101):
        expected_checksum = 0
    else:
        expected_checksum = remainder

    if checksum != expected_checksum:
        return {
            "valid": False,
            "error": f"Неверная контрольная сумма: ожидалось {expected_checksum:02d}, получено {checksum:02d}"
        }

    return {
        "valid": True,
        "formatted": f"{number[:3]}-{number[3:6]}-{number[6:9]} {checksum:02d}"
    }


# Примеры
print(validate_snils("112-233-445 95"))  # valid
print(validate_snils("123-456-789 64"))  # проверить

Regex для извлечения

snils_pattern = r"(\d{3})[\-\s]?(\d{3})[\-\s]?(\d{3})[\s]?(\d{2})"

def extract_snils(text: str) -> list[str]:
    """Извлечь все СНИЛС из текста."""
    matches = re.findall(snils_pattern, text)
    results = []
    for m in matches:
        snils = f"{m[0]}-{m[1]}-{m[2]} {m[3]}"
        validation = validate_snils(snils)
        if validation["valid"]:
            results.append(validation["formatted"])
    return results

Промпт для vision-модели

Извлеки номер СНИЛС из скана документа.

Формат СНИЛС: XXX-XXX-XXX YY (11 цифр)
- Первые 9 цифр — номер лицевого счёта
- Последние 2 цифры — контрольное число

Верни JSON:
{
  "snils": "XXX-XXX-XXX YY",  // номер в стандартном формате
  "raw_text": "..."          // исходный текст с документа
}

Если СНИЛС не найден или не читается — верни null.

БЛОК 3: ИНН

Форматы

ТипДлинаФормат
Физлицо12 цифрXXXXXXXXXXXX
Юрлицо10 цифрXXXXXXXXXX

Алгоритм проверки контрольных цифр

def validate_inn(inn: str) -> dict:
    """
    Валидация ИНН по контрольным цифрам.

    ИНН физлица (12 цифр):
    - 11-я цифра: контрольная для первых 10
    - 12-я цифра: контрольная для первых 11

    ИНН юрлица (10 цифр):
    - 10-я цифра: контрольная для первых 9
    """
    inn_clean = re.sub(r"\s", "", inn)

    if not re.match(r"^\d{10}$|^\d{12}$", inn_clean):
        return {"valid": False, "error": "ИНН должен содержать 10 или 12 цифр"}

    def calc_checksum(digits: str, weights: list[int]) -> int:
        total = sum(int(d) * w for d, w in zip(digits, weights))
        return total % 11 % 10

    if len(inn_clean) == 10:
        # ИНН юрлица
        weights = [2, 4, 10, 3, 5, 9, 4, 6, 8]
        expected = calc_checksum(inn_clean[:9], weights)
        actual = int(inn_clean[9])

        if expected != actual:
            return {"valid": False, "error": "Неверная контрольная цифра юрлица", "type": "legal"}

        return {"valid": True, "type": "legal", "formatted": inn_clean}

    else:
        # ИНН физлица
        weights_11 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
        weights_12 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]

        expected_11 = calc_checksum(inn_clean[:10], weights_11)
        expected_12 = calc_checksum(inn_clean[:11], weights_12)

        actual_11 = int(inn_clean[10])
        actual_12 = int(inn_clean[11])

        if expected_11 != actual_11:
            return {"valid": False, "error": "Неверная 11-я контрольная цифра", "type": "individual"}

        if expected_12 != actual_12:
            return {"valid": False, "error": "Неверная 12-я контрольная цифра", "type": "individual"}

        return {"valid": True, "type": "individual", "formatted": inn_clean}


# Примеры
print(validate_inn("7707083893"))     # Сбербанк (юрлицо)
print(validate_inn("500100732259"))   # физлицо

Regex для извлечения

inn_pattern = r"(?:ИНН[:\s]*)?(\d{10}|\d{12})(?!\d)"

def extract_inn(text: str) -> list[dict]:
    """Извлечь все ИНН из текста с валидацией."""
    matches = re.findall(inn_pattern, text)
    results = []
    for inn in matches:
        validation = validate_inn(inn)
        if validation["valid"]:
            results.append(validation)
    return results

Структура ИНН

ИНН юрлица:  XX YY ZZZZZ C
             │  │  │     └── контрольная цифра
             │  │  └──────── порядковый номер
             │  └─────────── код ИФНС
             └────────────── код субъекта РФ

ИНН физлица: XX YY ZZZZZZ CC
             │  │  │      └── 2 контрольные цифры
             │  │  └───────── порядковый номер
             │  └──────────── код ИФНС
             └─────────────── код субъекта РФ

БЛОК 4: Водительское удостоверение

Новый формат (с 2011 года)

┌─────────────────────────────────────────────────────────┐
│ ВОДИТЕЛЬСКОЕ УДОСТОВЕРЕНИЕ                              │
│ DRIVING LICENCE                                          │
│                                                          │
│ 1. ИВАНОВ / IVANOV                                      │
│ 2. ИВАН ИВАНОВИЧ / IVAN IVANOVICH                       │
│ 3. 01.01.1990 ГОР. МОСКВА / MOSCOW                      │
│                                                          │
│ 4a. 15.01.2020  4b. 15.01.2030  4c. ГИБДД 7701          │
│ 5. XX XX XXXXXX                                          │
│                                                          │
│ 9. Категории / Categories:                               │
│    B, B1, M                                              │
│                                                          │
│ [Фото]              [Подпись]                           │
└─────────────────────────────────────────────────────────┘

Поля для извлечения

ПолеКодОписание
Фамилия1Кириллица / латиница
Имя, отчество2Кириллица / латиница
Дата и место рождения3ДД.ММ.ГГГГ + город
Дата выдачи4aДД.ММ.ГГГГ
Срок действия4bДД.ММ.ГГГГ
Кем выдан4cКод подразделения ГИБДД
Серия и номер5XX XX XXXXXX
Категории9A, A1, B, B1, C, C1, D, D1, M, Tm, Tb

Формат серии и номера

XX XX XXXXXX
│  │  └────── номер (6 цифр)
│  └───────── год бланка (2 цифры)
└──────────── код региона (2 цифры)

Regex для извлечения

# Серия и номер ВУ
vu_number_pattern = r"(\d{2})\s*(\d{2})\s*(\d{6})"

# Категории
categories_pattern = r"(?:категори[яи]|categories)[:\s]*([A-Za-z1,\s]+)"

# Срок действия
validity_pattern = r"4[ab][:\.\s]*(\d{2}\.\d{2}\.\d{4})"

def extract_driving_license(text: str) -> dict:
    """Извлечь данные водительского удостоверения."""
    result = {}

    # Серия и номер
    match = re.search(vu_number_pattern, text)
    if match:
        result["series"] = f"{match.group(1)} {match.group(2)}"
        result["number"] = match.group(3)

    # Категории
    match = re.search(categories_pattern, text, re.IGNORECASE)
    if match:
        cats = re.findall(r"[A-Z][1]?|Tm|Tb|M", match.group(1), re.IGNORECASE)
        result["categories"] = [c.upper() for c in cats]

    return result

Категории транспортных средств

DRIVING_CATEGORIES = {
    "M": "Мопеды, квадрициклы до 50 см³",
    "A1": "Мотоциклы до 125 см³",
    "A": "Мотоциклы",
    "B1": "Трициклы, квадрициклы",
    "B": "Легковые автомобили до 3,5 т",
    "BE": "Легковые с прицепом > 750 кг",
    "C1": "Грузовые 3,5-7,5 т",
    "C": "Грузовые > 3,5 т",
    "C1E": "Грузовые 3,5-7,5 т с прицепом",
    "CE": "Грузовые с прицепом",
    "D1": "Автобусы до 16 пассажиров",
    "D": "Автобусы",
    "D1E": "Автобусы до 16 пассажиров с прицепом",
    "DE": "Автобусы с прицепом",
    "Tm": "Трамваи",
    "Tb": "Троллейбусы",
}

Промпт для vision-модели

Извлеки данные из скана водительского удостоверения РФ. Верни JSON:

{
  "surname_ru": "ФАМИЛИЯ",        // поле 1 (кириллица)
  "surname_en": "SURNAME",        // поле 1 (латиница)
  "name_ru": "ИМЯ ОТЧЕСТВО",      // поле 2 (кириллица)
  "name_en": "NAME PATRONYMIC",   // поле 2 (латиница)
  "birth_date": "ДД.ММ.ГГГГ",     // поле 3
  "birth_place": "...",           // поле 3
  "issue_date": "ДД.ММ.ГГГГ",     // поле 4a
  "expiry_date": "ДД.ММ.ГГГГ",    // поле 4b
  "issuer_code": "ГИБДД XXXX",    // поле 4c
  "series": "XX XX",              // поле 5 (серия)
  "number": "XXXXXX",             // поле 5 (номер)
  "categories": ["B", "B1", "M"]  // поле 9
}

Если поле не читается — верни null для этого поля.

БЛОК 5: ОГРН и ОГРНИП

Форматы

ТипДлинаФормат
ОГРН (юрлицо)13 цифрS GG KK XXXXXXX C
ОГРНИП (ИП)15 цифрS GG KK XXXXXXXXX CC

Структура ОГРН

S GG KK XXXXXXX C
│ │  │  │       └── контрольная цифра
│ │  │  └────────── порядковый номер записи
│ │  └───────────── код субъекта РФ
│ └──────────────── год регистрации (2 цифры)
└────────────────── признак: 1 = юрлицо, 5 = ИП

Алгоритм проверки контрольной цифры

def validate_ogrn(ogrn: str) -> dict:
    """
    Валидация ОГРН/ОГРНИП по контрольной цифре.

    ОГРН (13 цифр):
    - Делим первые 12 цифр на 11
    - Остаток от деления (если > 9, берём младший разряд) = контрольная

    ОГРНИП (15 цифр):
    - Делим первые 14 цифр на 13
    - Остаток от деления (если > 9, берём младший разряд) = контрольная
    """
    ogrn_clean = re.sub(r"\s", "", ogrn)

    if len(ogrn_clean) == 13:
        # ОГРН юрлица
        if ogrn_clean[0] not in ("1", "5"):
            return {"valid": False, "error": "ОГРН должен начинаться с 1 или 5"}

        base = int(ogrn_clean[:12])
        expected = base % 11 % 10
        actual = int(ogrn_clean[12])

        if expected != actual:
            return {"valid": False, "error": f"Неверная контрольная цифра: ожидалось {expected}"}

        entity_type = "legal" if ogrn_clean[0] == "1" else "ip"
        return {
            "valid": True,
            "type": "ОГРН" if entity_type == "legal" else "ОГРНИП (старый формат)",
            "year": f"20{ogrn_clean[1:3]}",
            "region": ogrn_clean[3:5],
            "formatted": ogrn_clean
        }

    elif len(ogrn_clean) == 15:
        # ОГРНИП
        if ogrn_clean[0] != "3":
            return {"valid": False, "error": "ОГРНИП должен начинаться с 3"}

        base = int(ogrn_clean[:14])
        expected = base % 13 % 10
        actual = int(ogrn_clean[14])

        if expected != actual:
            return {"valid": False, "error": f"Неверная контрольная цифра: ожидалось {expected}"}

        return {
            "valid": True,
            "type": "ОГРНИП",
            "year": f"20{ogrn_clean[1:3]}",
            "region": ogrn_clean[3:5],
            "formatted": ogrn_clean
        }

    else:
        return {"valid": False, "error": "ОГРН должен содержать 13 цифр, ОГРНИП — 15 цифр"}


# Примеры
print(validate_ogrn("1027700132195"))   # Сбербанк
print(validate_ogrn("304500116000157")) # ИП

Regex для извлечения

ogrn_pattern = r"(?:ОГРН(?:ИП)?[:\s]*)?([135]\d{12}|3\d{14})(?!\d)"

def extract_ogrn(text: str) -> list[dict]:
    """Извлечь все ОГРН/ОГРНИП из текста с валидацией."""
    matches = re.findall(ogrn_pattern, text)
    results = []
    for ogrn in matches:
        validation = validate_ogrn(ogrn)
        if validation["valid"]:
            results.append(validation)
    return results

БЛОК 6: Комплексное извлечение

Паттерн: Извлечение всех идентификаторов из документа

import re
import json

def extract_all_identifiers(text: str) -> dict:
    """
    Извлечь все российские идентификаторы из текста.
    """
    results = {
        "passport": [],
        "snils": [],
        "inn": [],
        "ogrn": [],
        "driver_license": [],
    }

    # Паспорт (серия + номер)
    for match in re.finditer(r"(\d{2})\s*(\d{2})\s+(\d{6})", text):
        series = f"{match.group(1)} {match.group(2)}"
        number = match.group(3)
        results["passport"].append({
            "series": series,
            "number": number,
            "full": f"{series} {number}"
        })

    # СНИЛС
    for match in re.finditer(r"(\d{3})[\-\s]?(\d{3})[\-\s]?(\d{3})[\s]?(\d{2})", text):
        snils = f"{match.group(1)}-{match.group(2)}-{match.group(3)} {match.group(4)}"
        validation = validate_snils(snils)
        if validation["valid"]:
            results["snils"].append(validation["formatted"])

    # ИНН
    for match in re.finditer(r"(?<!\d)(\d{10}|\d{12})(?!\d)", text):
        inn = match.group(1)
        validation = validate_inn(inn)
        if validation["valid"]:
            results["inn"].append(validation)

    # ОГРН/ОГРНИП
    for match in re.finditer(r"(?<!\d)([135]\d{12}|3\d{14})(?!\d)", text):
        ogrn = match.group(1)
        validation = validate_ogrn(ogrn)
        if validation["valid"]:
            results["ogrn"].append(validation)

    return results

Промпт для vision-модели: Универсальный KYC

Проанализируй скан документа и определи его тип. Извлеки все идентификаторы.

Поддерживаемые документы:
- Паспорт РФ (серия XX XX, номер XXXXXX)
- СНИЛС (XXX-XXX-XXX YY)
- ИНН (10 или 12 цифр)
- Водительское удостоверение
- ОГРН/ОГРНИП (13 или 15 цифр)

Верни JSON:
{
  "document_type": "passport|snils|inn|driver_license|ogrn|unknown",
  "confidence": 0.0-1.0,
  "extracted_data": {
    // поля зависят от типа документа
  },
  "all_identifiers": {
    "passport": null,     // "XX XX XXXXXX"
    "snils": null,        // "XXX-XXX-XXX YY"
    "inn": null,          // 10 или 12 цифр
    "ogrn": null,         // 13 или 15 цифр
    "driver_license": null // "XX XX XXXXXX"
  },
  "quality_issues": []    // ["blurry", "partial", "rotated", ...]
}

БЛОК 7: Валидация и нормализация

Утилиты для нормализации

def normalize_name(name: str) -> str:
    """Нормализация ФИО: удаление лишних пробелов, приведение к title case."""
    name = re.sub(r"\s+", " ", name.strip())
    return name.title()

def normalize_date(date_str: str) -> str | None:
    """Нормализация даты к формату ДД.ММ.ГГГГ."""
    patterns = [
        (r"(\d{2})\.(\d{2})\.(\d{4})", r"\1.\2.\3"),
        (r"(\d{2})/(\d{2})/(\d{4})", r"\1.\2.\3"),
        (r"(\d{2})-(\d{2})-(\d{4})", r"\1.\2.\3"),
    ]
    for pattern, replacement in patterns:
        if re.match(pattern, date_str):
            return re.sub(pattern, replacement, date_str)
    return None

def format_passport(series: str, number: str) -> str:
    """Форматирование паспортных данных."""
    series_clean = re.sub(r"\s", "", series)
    return f"{series_clean[:2]} {series_clean[2:]} {number}"

Сводная таблица валидации

ДокументФорматКонтрольная суммаПример
ПаспортXX XX XXXXXXНет45 06 123456
СНИЛСXXX-XXX-XXX YYДа (mod 101)112-233-445 95
ИНН физ.XXXXXXXXXXXXДа (2 цифры)500100732259
ИНН юр.XXXXXXXXXXДа (1 цифра)7707083893
ВУXX XX XXXXXXНет77 14 123456
ОГРНXXXXXXXXXXXXXДа (mod 11)1027700132195
ОГРНИПXXXXXXXXXXXXXXXДа (mod 13)304500116000157

БЛОК 8: Определение типа документа

def detect_document_type(text: str) -> dict:
    """Определение типа российского документа по ключевым словам."""
    markers = {
        "passport": ["паспорт", "passport", "код подразделения", "место рождения"],
        "snils": ["снилс", "страховое свидетельство", "пенсионн"],
        "inn": ["свидетельство о постановке", "идентификационный номер", "налогоплательщик"],
        "driver_license": ["водительское удостоверение", "driving licence", "категори", "гибдд"],
        "ogrn": ["огрн", "огрнип", "единый государственный реестр"],
    }

    text_lower = text.lower()
    scores = {dtype: sum(1 for kw in keywords if kw in text_lower)
              for dtype, keywords in markers.items()}

    if max(scores.values()) > 0:
        doc_type = max(scores, key=scores.get)
        confidence = scores[doc_type] / len(markers[doc_type])
    else:
        doc_type = "unknown"
        confidence = 0.0

    return {"document_type": doc_type, "confidence": confidence}

БЛОК 9: Типичные команды пользователя

КомандаДействие
"Распознай паспорт"БЛОК 1: Извлечение данных паспорта
"Прочитай MRZ зону"БЛОК 1: Извлечение MRZ
"Проверь СНИЛС"БЛОК 2: Валидация СНИЛС
"Валидация ИНН"БЛОК 3: Проверка контрольных цифр ИНН
"Извлеки данные из водительского"БЛОК 4: Парсинг ВУ
"Проверь ОГРН компании"БЛОК 5: Валидация ОГРН
"KYC проверка документа"БЛОК 6: Комплексное извлечение
"Верификация личности"БЛОК 6: Универсальный KYC промпт
"Какой это документ?"БЛОК 8: Определение типа документа

Skill-файл содержит паттерны извлечения данных из российских удостоверяющих документов. Используй vision-модель для OCR и Python-код для валидации по контрольным суммам.

Категория
📊 Документы и расчёты
Платформа
Сам Решу

Попробуйте этот навык

Зарегистрируйтесь и используйте навык «OCR российских документов» бесплатно.