Разбираем банковские реквизиты из карточки компании: БИК, корсчёт и контрольные суммы

Карточка компании — документ, который в России присылают друг другу все: поставщики, заказчики, подрядчики. В нём ИНН, КПП, адрес и банковские реквизиты. Формата у карточки нет: кто‑то делает таблицу в 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) после всех правок заполняются все поля. Главные выводы:
Опирайтесь на формат данных, а не на подписи. У реквизитов жёсткая структура, подписи свободны.
Проверяйте контрольные суммы. Это бесплатная защита от опечаток и от перепутанных счетов.
Не угадывайте, когда вариантов несколько. Спросить пользователя дешевле, чем исправлять платёжку.
Тестируйте на настоящих документах. Каждая новая карточка находила новую ошибку, и все они были в эвристиках, а не в коде.
Если у вас есть карточка, которая «точно не разберётся», — делитесь в комментариях, интересно посмотреть, что ещё придумывают.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.