- PVSM.RU - https://www.pvsm.ru -
Карточка компании — документ, который в России присылают друг другу все: поставщики, заказчики, подрядчики. В нём ИНН, КПП, адрес и банковские реквизиты. Формата у карточки нет: кто‑то делает таблицу в Word, кто‑то строку в PDF, кто‑то лист Excel. Подписи пишут как угодно: «Р/с», «Расчетный счет №», «р.сч.», «Счёт получателя».
Я делаю веб‑сервис, где пользователь загружает такую карточку, а программа заполняет реквизиты сама. Ниже — что сработало, а что нет, с кодом.
Первая версия была очевидной: найти слово «БИК» и взять 9 цифр после него. Она ломалась в трёх местах.
Подпись и значение в разных строках. При извлечении текста из таблиц PDF и DOCX ячейки часто приходят отдельными строками: сначала «БИК», потом «044525225».
Подписей много. «Корреспондентский счет №», «К/с», «корр. счёт», «Кор/сч» — это одно поле.
Подстрока «банк» внутри названия. Правило «строка со словом банк — это банк» на «ИНВЕСТТОРГБАНК АО» брало хвост после «банк» и получало «АО, г. Москва».
Выход — опираться на сами цифры: у банковских реквизитов жёсткий формат
|
Реквизит |
Длина |
Признак |
|
БИК |
9 |
начинается с 04 |
|
Корреспондентский счёт |
20 |
начинается с 30 101 |
|
Расчётный счёт организации |
20 |
начинается с 405–408 |
import re
BIK = re.compile(r"(?<!d)(04d{7})(?!d)")
CORR = re.compile(r"(?<!d)(30101d{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) после всех правок заполняются все поля. Главные выводы:
Опирайтесь на формат данных, а не на подписи. У реквизитов жёсткая структура, подписи свободны.
Проверяйте контрольные суммы. Это бесплатная защита от опечаток и от перепутанных счетов.
Не угадывайте, когда вариантов несколько. Спросить пользователя дешевле, чем исправлять платёжку.
Тестируйте на настоящих документах. Каждая новая карточка находила новую ошибку, и все они были в эвристиках, а не в коде.
Если у вас есть карточка, которая «точно не разберётся», — делитесь в комментариях, интересно посмотреть, что ещё придумывают.
Автор: zkn_stb
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/461404
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1102682/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1102682
Нажмите здесь для печати.