У нас есть форма, куда пользователь вводит ИНН контрагента, а мы по нему идём за данными в ЕГРЮЛ. Если заглянуть в логи такой формы, видно сколько туда прилетает мусора. ИНН из одиннадцати цифр (кто-то добавил лишнюю), номера со срезанными ведущими нулями, ОГРН в поле для ИНН, и классика жанра — две соседние цифры переставлены местами. Каждый такой ввод это поход в чужой API, таймаут, ожидание, и в конце честное «ничего не найдено». А пользователь в этот момент уверен, что сломались мы.
Обиднее всего, что добрую половину этого мусора можно отсечь прямо в браузере, не отправляя ни одного запроса. У ИНН, ОГРН и СНИЛС есть контрольные цифры. Никакой магии ФНС там нет, это обычная контрольная сумма по открытой формуле: берёшь первые цифры, считаешь по ним последнюю и сравниваешь. Не сошлось — номер битый, в реестре его нет и не будет.
Ниже разбор каждого алгоритма с кодом, и отдельно несколько неочевидных мест, на которых такой валидатор обычно спотыкается. Весь код собран в маленькую библиотеку без зависимостей, ссылка в конце.
Зачем вообще проверять на клиенте
Можно возразить: ну отправит человек битый ИНН, реестр и так вернёт пусто, в чём проблема. Пока запросов мало — проблемы и правда нет. Когда их становится много, от проверки на стороне формы появляется вполне ощутимая польза.
Во-первых, мы не дёргаем внешний реестр впустую. Это и нагрузка, и деньги если API платный, и лишние секунды ожидания для живого человека. Во-вторых, ошибка получается честной. Фраза «в ИНН должно быть 10 или 12 цифр, а у вас 11» куда полезнее, чем «контрагент не найден»: во втором случае человек решает, что номер верный, а сервис кривой. И в-третьих, ловятся опечатки, которые иначе ушли бы дальше. Перестановка двух соседних цифр — самая частая ошибка ручного ввода, и контрольная сумма её почти всегда замечает.
Сразу важная оговорка, чтобы не было лишних ожиданий. Контрольная сумма проверяет только то, что номер корректен математически. Существует ли такая компания, она не знает. Номер может быть валидным по формуле и при этом не принадлежать вообще никому. Это фильтр от мусора и опечаток, а не замена запросу в реестр.
ИНН: на самом деле два алгоритма
Первая ловушка в том, что ИНН это два разных формата. Десять цифр у юрлиц, двенадцать у физлиц и индивидуальных предпринимателей. И контрольные суммы у них считаются по-разному: у десятизначного одна контрольная цифра в конце, у двенадцатизначного две. Если написать одну проверку «на ИНН» и про это забыть, она будет либо ругаться на половину нормальных номеров, либо пропускать мусор.
Для десяти цифр так. Берём первые девять, каждую умножаем на свой коэффициент из набора [2, 4, 10, 3, 5, 9, 4, 6, 8], складываем. Сумму делим по модулю 11, потом ещё раз по модулю 10 — это для редкого случая, когда остаток вышел равным 10. Результат должен совпасть с десятой цифрой.
def validate_inn_ul(value: str) -> bool: """ИНН юрлица — ровно 10 цифр.""" if len(value) != 10 or not value.isdigit(): return False weights = (2, 4, 10, 3, 5, 9, 4, 6, 8) total = sum(int(d) * w for d, w in zip(value, weights)) control = total % 11 % 10 return control == int(value[9])
Для двенадцати цифр то же самое, только проделанное дважды. Сначала по первым десяти цифрам считаем одиннадцатую (коэффициенты [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]), потом по первым одиннадцати — двенадцатую ([3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]). Второй набор это первый, к которому спереди дописали тройку. Совпасть должны обе цифры.
def validate_inn_fl(value: str) -> bool: """ИНН физлица или ИП — ровно 12 цифр, две контрольные цифры.""" if len(value) != 12 or not value.isdigit(): return False w11 = (7, 2, 4, 10, 3, 5, 9, 4, 6, 8) w12 = (3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8) d11 = sum(int(d) * w for d, w in zip(value, w11)) % 11 % 10 d12 = sum(int(d) * w for d, w in zip(value, w12)) % 11 % 10 return d11 == int(value[10]) and d12 == int(value[11]) def validate_inn(value: str) -> bool: """Принимает и юрлицо, и физлицо/ИП — выбирает алгоритм по длине.""" if len(value) == 10: return validate_inn_ul(value) if len(value) == 12: return validate_inn_fl(value) return False
Ещё момент, который иногда полезен. Первые две цифры ИНН это код региона, где номер выдан. К контрольной сумме он отношения не имеет, но в форме можно показать что-то вроде «ИНН московский», и человек сам заметит, если вставил не тот номер.
ОГРН и ОГРНИП: тут проще
С ОГРН логика другая и заметно короче. Никаких коэффициентов: берём число из всех цифр кроме последней, делим по модулю и сравниваем остаток с последней цифрой.
У ОГРН (13 цифр, юрлица) берём первые 12 цифр как одно число и считаем остаток от деления на 11. Если он равен 10, берём 0. У ОГРНИП (15 цифр, ИП) то же самое, только первые 14 цифр и деление на 13. Отличаются по сути лишь длина и делитель.
def validate_ogrn(value: str) -> bool: """ОГРН юрлица — 13 цифр.""" if len(value) != 13 or not value.isdigit(): return False control = int(value[:12]) % 11 % 10 return control == int(value[12]) def validate_ogrnip(value: str) -> bool: """ОГРНИП индивидуального предпринимателя — 15 цифр.""" if len(value) != 15 or not value.isdigit(): return False control = int(value[:14]) % 13 % 10 return control == int(value[14])
Здесь Python снимает боль, которая есть в языках с фиксированной разрядностью. Запись int(value[:12]) спокойно делает из строки 12-значное число и никаких переполнений. Если переписывать это, скажем, на чём-то с 32-битным int, придётся считать остаток по цифрам вручную: 14-значный ОГРНИП туда уже не влезет.
СНИЛС: самый коварный из трёх
СНИЛС это 11 цифр, обычно записанных как XXX-XXX-XXX YY, где последние две — контрольные. Считается так: берём первые девять цифр, умножаем на убывающие веса от 9 до 1 (первая цифра на 9, вторая на 8 и так далее до девятой на 1), складываем.
А дальше идёт правило, на котором обычно и пишут битую реализацию. Контрольное число определяется так:
если сумма меньше 100, контрольное число равно самой сумме; если сумма равна 100 или 101, контрольное число равно 00 (не сто и не сто один, а именно ноль); если сумма больше 101, берём остаток от деления на 101, и если этот остаток снова оказался 100 — опять 00.
Наивная версия просто считает «сумма % 101» и сравнивает. На большинстве СНИЛС это работает, поэтому ошибка особенно вредная: она вылезает только на тех номерах, где остаток упирается в 100. До такого номера всё выглядит рабочим.
def validate_snils(value: str) -> bool: """СНИЛС — 11 цифр, последние две контрольные.""" if len(value) != 11 or not value.isdigit(): return False total = sum(int(d) * (9 - i) for i, d in enumerate(value[:9])) if total < 100: control = total elif total in (100, 101): control = 0 else: control = total % 101 if control == 100: control = 0 return control == int(value[9:])
Грабли, на которые стоит заложиться заранее
Этих штук в формулах не видно, но именно из-за них валидатор «работает на тестах и падает в проде».
Первое это ведущие нули. У ИНН и СНИЛС они в начале бывают. Стоит таким данным разок пройти через Excel или Google Sheets, и нули срезаются — десятизначный ИНН превращается в девятизначный. Если на входе выгрузка, число уже испорчено до того как попало в валидатор, а виноватым окажется код. Вывод банальный: храните и принимайте идентификаторы строками, никогда не через int.
Второе это длина. Её надо проверять до контрольной суммы, а не считать мелочью. У ИНН допустимы 10 и 12, но не 11, у ОГРН 13, у ОГРНИП 15. Если длину не проверить заранее, на номере неправильной длины zip молча обрежется по короткому, и на выходе получится «валидный» мусор.
Третье — мусорные символы. Люди вставляют номера с пробелами, дефисами, иногда с прилипшими буквами из скопированного текста. Чистить вход стоит, но аккуратно: если тупо выкинуть все нецифровые символы, то 123abc456 превратится в правдоподобное число. Надёжнее другой подход — если в исходной строке были буквы, сразу считать номер невалидным, а не «почистил и проверил».
И четвёртое, повторю отдельно, потому что на нём попадаются даже опытные. Валидный не значит существующий. Сгенерировать номер, проходящий контрольную сумму, может кто угодно за минуту. Контрольная сумма это первый фильтр, дешёвый и мгновенный. Реальность номера проверяется только обращением к реестру.
Собираем вместе
На вводе получается двухступенчатая схема, и она экономит кучу бессмысленных запросов.
def is_valid_identifier(value: str) -> bool: """Быстрая проверка перед запросом в реестр.""" value = value.strip() if not value.isdigit(): return False return ( validate_inn(value) or validate_ogrn(value) or validate_ogrnip(value) or validate_snils(value) )
Сначала эта функция, мгновенно и бесплатно. Прошло — идём в реестр. Не прошло — сразу говорим человеку, что именно не так, и не тратим ни его время, ни чужой API.
Код целиком
Все функции вынесены в небольшую библиотеку без внешних зависимостей, чистый Python, лицензия MIT. Там же лежит validate_kpp, разбор кода региона по ИНН, тесты и маленький CLI, чтобы проверять номер прямо из терминала.
Репозиторий: https://github.com/kontragentpro/inn-tools
Если знаете другие грабли по контрольным суммам российских идентификаторов, напишите в комментариях, дополню. Особенно интересны крайние случаи, на которых ломались чужие реализации.
