Распознаём реквизиты из карточки контрагентов: Как мы сделали API для извлечения реквизитов из документов

от автора

Привет, Хабр!

Каждый, кто работал с бухгалтерией, CRM или просто заводил контрагента вручную, знает эту боль. Вам присылают карточку компании в PDF, договор в DOCX или просто текстовый файл с реквизитами. Задача: достать оттуда ИНН, КПП, расчётный счёт и БИК, чтобы не схлопотать штраф за неверные данные.

Можно нанимать стажёра, который будет перепечатывать это в Excel. А можно довериться машине.

Мы в нашей компании долгое время решали эту проблему для своих внутренних задач (интеграция с 1С и автоматизация документооборота), а в итоге обкатали решение и выпилили в отдельный публичный сервис. Сегодня расскажу, как наш API извлечения реквизитов работает под капотом, покажу примеры кода на 6 языках (включая 1С, куда без него) и честно расскажу о таймаутах и подводных камнях.

Зачем это всё? (или проблема 20-й карточки)

Представьте: у вас интернет-магазин или B2B-платформа. Новый клиент регистрируется и вбивает реквизиты своей фирмы. Статистика неумолима: каждый третий пользователь ошибается при ручном вводе хотя бы в одной цифре ИНН или расчётного счёта. Дальше — сбой в 1С, невыставленный счёт, потерянная сделка.

Наш API решает эту задачу одним POST-запросом:

  1. Из карточки компании — извлекаем все юридически значимые реквизиты.

  2. Из файлов документов — договоров, счетов, актов, накладных.

Поддерживаемые форматы: PDF (только с текстовым слоем), DOCX, DOC, TXT, RTF, HTML.

⚠️ Важное уточнение: Сервис работает только с текстовыми файлами. Отсканированные PDF-изображения, картинки и фотографии не поддерживаются. Если у вас PDF без текстового слоя — перед отправкой нужно распознать его отдельным OCR-инструментом.

Как это работает (очень коротко)

Многие думают, что мы просто гоним текст через регулярные выражения. Но нет. Основная магия — в контекстном анализе и валидации.

  1. Приём файла: Вы шлёте multipart/form-data. Мы принимаем бинарник.

  2. Извлечение текста: PDF с текстовым слоем парсится напрямую. DOCX распарсивается через внутренний конвертер. TXT, RTF, HTML обрабатываются соответствующими парсерами.

  3. Нормализация: Убираем мусор, восстанавливаем разорванные слова, чиним кодировки. (Да, есть ещё люди, которые присылают документы в CP1251 без BOM).

  4. NER (Named Entity Recognition): Своя модель ищёт паттерны. ИНН — это 10 или 12 цифр, но не любая последовательность. ОГРН — тоже не просто цифры, у них своя структура.

  5. Валидация: Мы не просто выдёргиваем числа. Мы проверяем контрольные суммы ИНН, ОГРН, логику БИК. Если сумма не сходится — поле не вернётся, чтобы вы не сохранили в базу заведомо мусор.

Техническая документация по API

Всё максимально RESTful, без гимнастики.

Эндпоинт

POST https://api-k.ru/api/rekvizit_json

Заголовки

Ключ

Значение

Обязательность

X-API-Key

Ваш ключ (получаете в личном кабинете)

Да

Content-Type

multipart/form-data

Да

Параметры запроса

В теле — одно поле: file. Туда кладёте ваш PDF, DOCX, DOC, TXT, RTF или HTML.

⚠️ Важно по таймауту: Не ставьте стандартные 30 секунд. Некоторые «тяжёлые» PDF с графикой или документы большого объёма могут обрабатываться до 2 минут. Ставьте 120 секунд и спите спокойно.

Примеры кода (для ленивых и занятых)

Показывать только cURL — моветон. На Хабре любят глазами кушать код. Поэтому накидал примеры для самых популярных стеков плюс отдельный блок для тех, кто мучается с 1С.

Python (самый родной)

import requestsimport osdef extract_requisites(api_key, file_path):    url = "https://***.ru/api/rekvizit_json"    headers = {'X-API-Key': api_key}        with open(file_path, 'rb') as f:        files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}        response = requests.post(url, headers=headers, files=files, timeout=120)        return response.json()# Вжухresult = extract_requisites("ваш_ключ", "dogovor.pdf")print(result['data']['inn'], result['data']['checking_account'])

cURL (для консольных магов)

curl -X POST https://***.ru/api/rekvizit_json \  -H "X-API-Key: ваш_ключ" \  -F "file=@/home/user/contract.docx" \  --max-time 120

JavaScript (Node.js)

const axios = require('axios');const FormData = require('form-data');const fs = require('fs');async function extract(apiKey, filePath) {    const form = new FormData();    form.append('file', fs.createReadStream(filePath));        const response = await axios.post('https://***.ru/api/rekvizit_json', form, {        headers: { ...form.getHeaders(), 'X-API-Key': apiKey },        timeout: 120000    });    console.log(response.data);}

PHP (без фреймворков)

$ch = curl_init();$cFile = new CURLFile('contract.docx');curl_setopt_array($ch, [    CURLOPT_URL => 'https://***.ru/api/rekvizit_json',    CURLOPT_POST => true,    CURLOPT_POSTFIELDS => ['file' => $cFile],    CURLOPT_HTTPHEADER => ['X-API-Key: ваш_ключ'],    CURLOPT_TIMEOUT => 120,    CURLOPT_RETURNTRANSFER => true]);$result = curl_exec($ch);print_r(json_decode($result, true));

1С (боль и страдания)

Отдельная любовь — наши клиенты на 1С. Там нет нативной поддержки multipart/form-data «из коробки» так, как хочется. Пришлось поколдовать с формированием границы и ЗаписьДанных.

Ниже рабочий фрагмент для типовой конфигурации. Обратите внимание на ручное добавление

Процедура ОтправитьФайлНаСервере()    // 1. Проверка - выбран ли файл    Если ПустаяСтрока(ЭтотОбъект.ПутьКФайлу) Тогда        Сообщить("Ошибка: Не выбран файл для отправки", СтатусСообщения.Важное);        Возврат;    КонецЕсли;        // 2. Проверка наличия API-ключа    Если ПустаяСтрока(ЭтотОбъект.APIКлюч) Тогда        Сообщить("Ошибка: Не указан API-ключ", СтатусСообщения.Важное);        Возврат;    КонецЕсли;           // 3. Проверка существования файла    ФайлДляПроверки = Новый Файл(ЭтотОбъект.ПутьКФайлу);    Если Не ФайлДляПроверки.Существует() Тогда        Сообщить("Ошибка: Файл не найден по пути " + ЭтотОбъект.ПутьКФайлу, СтатусСообщения.Важное);        Возврат;    КонецЕсли;        // 4. Чтение файла в двоичные данные    ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ЭтотОбъект.ПутьКФайлу);    ИмяФайла = ФайлДляПроверки.Имя;    // 5. Формирование тела запроса (multipart/form-data) — ИСПРАВЛЕННЫЙ вариантГраница = "----WebKitFormBoundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");Тело = Новый ПотокВПамяти;ЗаписьДанных = Новый ЗаписьДанных(Тело, , , Символы.ВК + Символы.ПС, "");  // Ключ: CRLF и пустой РазделительСтрокЗаписьДанных.ЗаписатьСтроку("--" + Граница);ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайла + """");ЗаписьДанных.ЗаписатьСтроку("Content-Type: application/octet-stream");ЗаписьДанных.ЗаписатьСтроку("");  // Пустая строка перед даннымиЗаписьДанных.Записать(ДвоичныеДанныеФайла);  // Двоичные данные файлаЗаписьДанных.ЗаписатьСтроку("");  // Пустая строка после данныхЗаписьДанных.ЗаписатьСтроку("--" + Граница + "--");  // Закрывающий разделительЗаписьДанных.Закрыть();ДвоичныеДанныеТела = Тело.ЗакрытьИПолучитьДвоичныеДанные();        // 6. Формирование HTTP-запроса    ИмяСервера = "***.ru";    Порт = 443;    ЗащищенноеСоединение = Истина;        // Создаем HTTP-соединение    Попытка        HTTPСоединение = Новый HTTPСоединение(ИмяСервера, Порт, "", "", , ЗащищенноеСоединение);    Исключение        Сообщить("Ошибка при создании HTTP-соединения: " + ОписаниеОшибки(), СтатусСообщения.Важное);        Возврат;    КонецПопытки;        // Создаем HTTP-запрос    HTTPЗапрос = Новый HTTPЗапрос("/api/rekvizit_json");    HTTPЗапрос.Заголовки.Вставить("X-API-Key", ЭтотОбъект.APIКлюч);    HTTPЗапрос.Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Граница);      HTTPЗапрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеТела); HTTPЗапрос.Заголовки.Вставить("Content-Length", XMLСтрока(ДвоичныеДанныеТела.Размер()));         // Отправляем запрос    Попытка        Ответ = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);                Если Ответ.КодСостояния = 200 Тогда            СтрокаОтвета = Ответ.ПолучитьТелоКакСтроку();            РазобратьИОтобразитьРезультат(СтрокаОтвета);            ЭтотОбъект.ОтветСервера = СтрокаОтвета;            Сообщить("Успешно! Файл обработан, реквизиты получены.", СтатусСообщения.Информация);        Иначе            СтрокаОшибки = Ответ.ПолучитьТелоКакСтроку();            ЭтотОбъект.ОтветСервера = СтрокаОшибки;            Сообщить("Ошибка HTTP " + Строка(Ответ.КодСостояния) + ": " + СтрокаОшибки, СтатусСообщения.Важное);        КонецЕсли;            Исключение        Сообщить("Ошибка запроса: " + ОписаниеОшибки(), СтатусСообщения.Важное);    КонецПопытки;   КонецПроцедуры

Go

package mainimport (    "bytes"    "encoding/json"    "io"    "mime/multipart"    "net/http"    "os"    "path/filepath"    "time")type Response struct {    Success bool   `json:"success"`    Error   string `json:"error,omitempty"`    // другие поля с реквизитами}func ExtractRequisites(apiKey, filePath string) (*Response, error) {    file, err := os.Open(filePath)    if err != nil {        return nil, err    }    defer file.Close()    body := &bytes.Buffer{}    writer := multipart.NewWriter(body)        part, err := writer.CreateFormFile("file", filepath.Base(filePath))    if err != nil {        return nil, err    }        _, err = io.Copy(part, file)    if err != nil {        return nil, err    }    writer.Close()    req, err := http.NewRequest("POST", "https://***.ru/api/rekvizit_json", body)    if err != nil {        return nil, err    }        req.Header.Set("X-API-Key", apiKey)    req.Header.Set("Content-Type", writer.FormDataContentType())    client := &http.Client{        Timeout: 120 * time.Second,    }        resp, err := client.Do(req)    if err != nil {        return nil, err    }    defer resp.Body.Close()    var result Response    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {        return nil, err    }        return &result, nil}

На что похож ответ?

Вы получаете JSON. Если всё ок — "success": true и объект data. Если нет — читайте message.

Успех:

{  "success": true,  "filename": "act_123.pdf",  "file_size": 20480,  "data": {    "organization_name": "ООО «Ромашка»",    "inn": "7725123456",    "kpp": "772501001",    "ogrn": "1027700123456",    "address": "г. Москва, ул. Тверская, д. 1",    "phone": "+7(495) 123-45-67",    "bank_name": "Т-Банк",    "bik": "044525974",    "checking_account": "40802810123456789012",    "correspondent_account": "30101810123456789012",    "type_doc": "договор",    "nom_doc": "45/А",    "signatory": "Петров А.А."  }}

Ошибка (например, файл слишком большой):

{  "success": false,  "error": "payload_too_large",  "message": "Размер файла превышает 20 МБ"}

Политика ошибок (чтобы не было сюрпризов)

Мы стараемся отвечать стандартными HTTP кодами, но всегда смотрите на success в теле ответа — там надёжнее.

HTTP

error

Что делать?

400

invalid_request

Вы прислали пустой файл, неверный формат или отсутствует тело запроса

401

unauthorized

Проверьте ключ X-API-Key. Он есть в заголовках?

408

timeout

Увеличьте таймаут до 120 секунд. Документ слишком сложный

413

payload_too_large

Файл весит больше 20 МБ — уменьшите или порежьте

500

internal_error

Упал наш демон. Такое бывает редко, но мы мониторим 24/7

Бенчмарки и ограничения (начистоту)

Мы прогнали через API 10 000 реальных документов из открытых бухгалтерских архивов.

  • Точность распознавания ИНН/ОГРН: 99.7% (ошибки в основном на документах с нестандартной вёрсткой).

  • Точность распознавания расчётного счёта: 98.9% (иногда путаем цифры, если шрифт «плывёт»).

  • Скорость: Обычный DOCX на 2 страницы — 1.5 секунды. Тяжёлый PDF с большим количеством графики — до 45 секунд.

  • Лимиты: По умолчанию — 20 МБ на файл и 120 секунд на операцию.

  • Поддерживаемые форматы: PDF (только текстовый слой), DOCX, DOC, TXT, RTF, HTML.

Вместо заключения

Этот API уже месяц как используется в продакшне. К нам стучатся как интернет-магазины (чтобы автоматически регистрировать юрлиц), так и банки (для проверки карточек клиентов). Если вы устали парсить реквизиты регулярками или нанимать людей для перепечатки — попробуйте. Тестовый доступ к АПИ на 30 дней (100 запрсов к сервису) — с промокодом: 1MPROMO2026. Промокод действует до 30.05.2026 г.

ссылка на оригинал статьи https://habr.com/ru/articles/1023562/