📊
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 | Код подразделения ГИБДД |
| Серия и номер | 5 | XX XX XXXXXX |
| Категории | 9 | A, 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 российских документов» бесплатно.