Карточка компании — документ, который в России присылают друг другу все: поставщики, заказчики, подрядчики. В нём ИНН, КПП, адрес и банковские реквизиты. Формата у карточки нет: кто‑то делает таблицу в Word, кто‑то строку в PDF, кто‑то лист Excel. Подписи пишут как угодно: «Р/с», «Расчетный счет №», «р.сч.», «Счёт получателя».
Я делаю веб‑сервис, где пользователь загружает такую карточку, а программа заполняет реквизиты сама. Ниже — что сработало, а что нет, с кодом.
Почему не работает «искать по подписи»
Первая версия была очевидной: найти слово «БИК» и взять 9 цифр после него. Она ломалась в трёх местах.
Подпись и значение в разных строках. При извлечении текста из таблиц PDF и DOCX ячейки часто приходят отдельными строками: сначала «БИК», потом «044525225».
Подписей много. «Корреспондентский счет №», «К/с», «корр. счёт», «Кор/сч» — это одно поле.
Подстрока «банк» внутри названия. Правило «строка со словом банк — это банк» на «ИНВЕСТТОРГБАНК АО» брало хвост после «банк» и получало «АО, г. Москва».
Выход — опираться на сами цифры: у банковских реквизитов жёсткий формат
Реквизиты по цифрам
Реквизит |
Длина |
Признак |
БИК |
9 |
начинается с 04 |
Корреспондентский счёт |
20 |
начинается с 30 101 |
Расчётный счёт организации |
20 |
начинается с 405–408 |
import re BIK = re.compile(r"(?<!\d)(04\d{7})(?!\d)") CORR = re.compile(r"(?<!\d)(30101\d{15})(?!\d)") ACC = re.compile(r"(?<!\d)(40[5-8]\d{17})(?!\d)") def uniq(xs): return list(dict.fromkeys(xs)) # порядок появления, без повторов def find_numbers(text): return (uniq(BIK.findall(text)), uniq(CORR.findall(text)), uniq(ACC.findall(text)))
(?<!\d) и (?!\d) важны. Без них из 20-значного расчётного счёта легко вытащить «телефон» из 11 цифр. У нас это действительно случилось: в поле «Телефон» попало 70 281 067 000 — середина счёта 40702810670000000181.
Два банка в одной карточке
Когда у компании два банка, карточка выглядит так:

После извлечения текста порядок может перемешаться. Как понять, какой корсчёт к какому БИК?
У корсчёта банка последние три цифры совпадают с последними тремя цифрами БИК: …411 ↔ …411, …593 ↔ …593. Это следует из того, как устроены оба номера, и позволяет собрать пары при любом порядке строк.
def pair_banks(biks, cors, accs): banks = [] for i, b in enumerate(biks): corr = next((c for c in cors if c[-3:] == b[-3:]), cors[i] if i < len(cors) else "") banks.append({"bik": b, "corr": corr, "account": accs[i] if i < len(accs) else ""}) return banks
С расчётным счётом такого признака нет. Его мы проверяем контрольной суммой.
Контрольная сумма: ключевание счёта
Номер счёта в российских банках содержит контрольный разряд (девятая цифра), который считается вместе с БИК. Алгоритм опубликован Банком России:
-
Берём 23 цифры: «приставку» из БИК и 20 цифр счёта.
для расчётного счёта приставка — последние 3 цифры БИК;
для корреспондентского — «0» + 5-я и 6-я цифры БИК.
Умножаем цифры на веса 7, 1, 3, 7, 1, 3, ….
Берём младший разряд каждого произведения и складываем.
Если сумма делится на 10, счёт верный.
WEIGHTS = [7, 1, 3] * 8 def _check(prefix, account): digits = prefix + account if len(digits) != 23 or not digits.isdigit(): return False s = sum((int(d) * w) % 10 for d, w in zip(digits, WEIGHTS)) return s % 10 == 0 def account_ok(bik, account): return _check(bik[-3:], account) def corr_ok(bik, corr): return _check("0" + bik[4:6], corr) Проверка на примере выше: >>> corr_ok("044525593", "30101810200000000593") True >>> account_ok("044525593", "40702810602490003955") True >>> account_ok("044525411", "40702810602490003955") # счёт от другого банка False
Этим же способом можно ставить расчётные счета к своим банкам, если их несколько: перебираем пары «БИК × счёт» и оставляем ту, где контрольная сумма сходится.
def match_accounts(banks, accs): left = list(accs) for b in banks: ok = next((a for a in left if account_ok(b["bik"], a)), None) if ok: b["account"] = ok left.remove(ok) return banks
В интерфейсе это даёт приятный эффект: если пользователь ошибся на одну цифру при ручном вводе, программа подсветит поле сразу, а не после возврата платёжки из банка.
Подпись и значение в разных строках
Для полей без жёсткого формата (ИНН рядом с КПП, ОГРН) ищем значение в той же строке, а если там пусто — в следующей:
def after_label(lines, label_rx, value_rx, skip_rx=None): for i, line in enumerate(lines): if not re.search(label_rx, line, re.I): continue if skip_rx and re.search(skip_rx, line, re.I): continue for s in (line, lines[i + 1] if i + 1 < len(lines) else ""): m = re.search(value_rx, s) if m: return m.group(1) return "" inn = after_label(lines, r"\bИНН\b", r"(?<!\d)(\d{12}|\d{10})(?!\d)") kpp = after_label(lines, r"\bКПП\b", r"(?<!\d)(\d{9})(?!\d)") ogrn = after_label(lines, r"\bОГРН", r"(?<!\d)(\d{15}|\d{13})(?!\d)")
skip_rx нужен для почты: «Электронная почта» нужна, а «Бухгалтерия, buh1_kp@maill.ru» — нет. Без исключения первой находится почта бухгалтерии.
Склеенные названия банков
Из ячеек DOCX названия двух банков могут прийти одной строкой, через двоеточие или пробел:
ФИЛИАЛ «ЦЕНТРАЛЬНЫЙ» БАНКА ВТБ (ПАО): АО «АЛЬФА‑БАНК»
Делим по разделителям и по началу нового названия — организационной форме после закрывающей скобки или кавычки:
SPLIT = re.compile(r'\s*[:|;]\s*|(?<=[)»"])\s+(?=(?:ФИЛИАЛ|ПАО|АО|ООО|АКБ|КБ)\b)') def bank_names(text): lines = [l for l in text.splitlines() if re.search(r"банк", l, re.I) and not re.search(r"\b(бик|сч[её]т|р/с|к/с)\b|реквизит", l, re.I)] joined = " ".join(re.sub(r"(?i)^(наименование\s+банка|банк)\s*[:\-]?\s*", "", l) for l in lines) return [p.strip(" ,") for p in SPLIT.split(joined) if p and len(p.strip(" ,")) > 2]
Названия ставим к парам БИК/корсчёт по порядку. Если банков несколько, мы не угадываем, а показываем пользователю выбор: «В карточке 2 банка — какой поставить?».
Что получилось
На трёх реальных карточках с разной разметкой (строки в PDF, таблица в DOCX, таблица в PDF) после всех правок заполняются все поля. Главные выводы:
Опирайтесь на формат данных, а не на подписи. У реквизитов жёсткая структура, подписи свободны.
Проверяйте контрольные суммы. Это бесплатная защита от опечаток и от перепутанных счетов.
Не угадывайте, когда вариантов несколько. Спросить пользователя дешевле, чем исправлять платёжку.
Тестируйте на настоящих документах. Каждая новая карточка находила новую ошибку, и все они были в эвристиках, а не в коде.
Если у вас есть карточка, которая «точно не разберётся», — делитесь в комментариях, интересно посмотреть, что ещё придумывают.
sshmakov
Скачайте справочник БИК с сайта ЦБ https://cbr.ru/s/newbik
Страница https://cbr.ru/PSystem/payment_system/
Там вам будут и официальные названия банков, и их БИК-и, и корсчета