
Привет, Хабр!
Каждый, кто работал с бухгалтерией, CRM или просто заводил контрагента вручную, знает эту боль. Вам присылают карточку компании в PDF, договор в DOCX или просто текстовый файл с реквизитами. Задача: достать оттуда ИНН, КПП, расчётный счёт и БИК, чтобы не схлопотать штраф за неверные данные.
Можно нанимать стажёра, который будет перепечатывать это в Excel. А можно довериться машине.
Мы в нашей компании долгое время решали эту проблему для своих внутренних задач (интеграция с 1С и автоматизация документооборота), а в итоге обкатали решение и выпилили в отдельный публичный сервис. Сегодня расскажу, как наш API извлечения реквизитов работает под капотом, покажу примеры кода на 6 языках (включая 1С, куда без него) и честно расскажу о таймаутах и подводных камнях.
Зачем это всё? (или проблема 20-й карточки)
Представьте: у вас интернет-магазин или B2B-платформа. Новый клиент регистрируется и вбивает реквизиты своей фирмы. Статистика неумолима: каждый третий пользователь ошибается при ручном вводе хотя бы в одной цифре ИНН или расчётного счёта. Дальше — сбой в 1С, невыставленный счёт, потерянная сделка.
Наш API решает эту задачу одним POST-запросом:
-
Из карточки компании — извлекаем все юридически значимые реквизиты.
-
Из файлов документов — договоров, счетов, актов, накладных.
Поддерживаемые форматы: PDF (только с текстовым слоем), DOCX, DOC, TXT, RTF, HTML.
⚠️ Важное уточнение: Сервис работает только с текстовыми файлами. Отсканированные PDF-изображения, картинки и фотографии не поддерживаются. Если у вас PDF без текстового слоя — перед отправкой нужно распознать его отдельным OCR-инструментом.
Как это работает (очень коротко)
Многие думают, что мы просто гоним текст через регулярные выражения. Но нет. Основная магия — в контекстном анализе и валидации.
-
Приём файла: Вы шлёте
multipart/form-data. Мы принимаем бинарник. -
Извлечение текста: PDF с текстовым слоем парсится напрямую. DOCX распарсивается через внутренний конвертер. TXT, RTF, HTML обрабатываются соответствующими парсерами.
-
Нормализация: Убираем мусор, восстанавливаем разорванные слова, чиним кодировки. (Да, есть ещё люди, которые присылают документы в CP1251 без BOM).
-
NER (Named Entity Recognition): Своя модель ищёт паттерны. ИНН — это 10 или 12 цифр, но не любая последовательность. ОГРН — тоже не просто цифры, у них своя структура.
-
Валидация: Мы не просто выдёргиваем числа. Мы проверяем контрольные суммы ИНН, ОГРН, логику БИК. Если сумма не сходится — поле не вернётся, чтобы вы не сохранили в базу заведомо мусор.
Техническая документация по API
Всё максимально RESTful, без гимнастики.
Эндпоинт
POST https://api-k.ru/api/rekvizit_json
Заголовки
|
Ключ |
Значение |
Обязательность |
|---|---|---|
|
|
Ваш ключ (получаете в личном кабинете) |
Да |
|
|
|
Да |
Параметры запроса
В теле — одно поле: 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 |
|
Что делать? |
|---|---|---|
|
400 |
|
Вы прислали пустой файл, неверный формат или отсутствует тело запроса |
|
401 |
|
Проверьте ключ |
|
408 |
|
Увеличьте таймаут до 120 секунд. Документ слишком сложный |
|
413 |
|
Файл весит больше 20 МБ — уменьшите или порежьте |
|
500 |
|
Упал наш демон. Такое бывает редко, но мы мониторим 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/