Автоматизация получения сведений из ЕГРЮЛ с помощью Freepascal

В своей работе (юридической) я готов автоматизировать всё, что только поддаётся этому. Но пока прокачанные нейросетями роботы из утопии Германа Грефа не появились и не отняли всю работу у рядовых юристов, рутина надолго останется нашим главным спутником. Автоматизация этой рутины — то, чем я периодически занимаюсь на протяжении последних лет, будь то многочисленные таблицы в excel с кучей формул, позволяющих быстро распечатать сотню однотипных документов-рассылок в word’е, ну или автоматически генерируемые отчеты. Но есть и такие вещи, которые простыми формулами и подстановками не сделаешь. Здесь на помощь приходит программирование, которым я увлекаюсь с детства, и так уж вышло, что началось это с delphi. Сейчас мне проще, чем в C# или python, осваивать которые начал недавно, сделать быстро какой-то проект в среде Lazarus, используя freepascal. И да, я на полном серьёзе считаю, что возможностей этой среды более, чем достаточно. Поэтому автоматизировать ЕГРЮЛ, как вы догадались, предстоит с помощью паскаля.
Юрист консалтинговой конторы, ведущей дела десятков юридических лиц, юрист-корпоративщик на вольных хлебах, да и любой другой юрист, сталкивающийся с обеспечением деятельности организаций — все они знают, как легко в голове смешиваются десятки и сотни разных наименований, номеров ИНН, ОГРН, как легко забыть, кто где руководитель, и когда у него подходит срок продления полномочий, нет ли проблем с долями в ООО и с оплатой его уставного капитала. Ну и необходимость сделать быстро какой-то документ, включающий в себя множество постоянно меняющихся реквизитов, влечет периодические ошибки и опечатки. Для автоматизации именно таких процессов мне было нужно решение с базой данных, позволяющее делать документы по шаблонам, вести различные реестры, отслеживать изменения и не пропускать какие-то сроки. Ну и одно из необходимых упрощений жизни — быстрое получение свежего файла со сведениями из ЕГРЮЛ с сайта Федеральной налоговой службы. Конечно, никто не говорит, что воспользоваться сайтом напрямую — это долго и трудно, но согласитесь, что нажать на одну кнопку, не выходя из приложения, гораздо веселее, и сделать это можно, не отрываясь от телефонного звонка (или чашки кофе).
Итак, для начала определимся, что мы хотим получить. Сайт позволяет провести поиск в официальной базе ЕГРЮЛ по уникальному номеру ОГРН или ИНН и выдать один релевантный результат в виде краткой справки о лице и ссылки на скачивание pdf-файла с выпиской. Также поиск может быть нечёткий по названию с дополнительным фильтром по региону (субъекту РФ). И в таком случае сайт выдает таблицу со всеми подходящими лицами и с тем же набором данных, включая ссылки на pdf.
Значит, в конкретном случае готовая функция должна возвращать pdf в виде файла (а лучше — потока), имея на входе ОГРН или ИНН лица. Но для универсализации и возможности дальнейшего расширения не будем пренебрегать всеми возможностями сайта и сделаем также функцию нечёткого поиска с возвращением набора данных, найденных по названию организации с учётом фильтра по региону или без такового. Попробуем описать интерфейсы этих функций:
IEGRULstreamer = interface procedure GetExtractByOGRN(OGRN: string; ХХХХХХ; isLegal: boolean; var Extract: TStream); procedure GetLegalsListByName(Name, Region: string; ХХХХХХ; var LegalsList: TCollection); end;
Для того, чтобы понять, что за таинственный параметр Х и коллекцию чего вернёт вторая функция, разберемся, как именно сайт исполняет запрос.
1. На сайте размещена форма с полями ввода для идентификаторов поиска и проверки капчи:
2. Капча формируется с помощью заранее сгенерированного скрытого поля с именем captchaToken, которое использует ява-скрипт для генерации изображения капчи по данному токену.
3. После нажатия на кнопку «найти» на сервер отправляется POST-запрос, в результатах обработки которого возвращается JSON с массивом объектов. Этот JSON-ответ использует другой ява-скрипт, заполняющий таблицу, которую мы видим в результатах поиска.
Итак, первая загвоздка — это проверка капчи. Чтобы не нагружать наши методы, занимающиеся взаимодействием с сайтом, лишним функционалом, мы вынесем в отдельную функцию действия по обработке капчи. И в Х у нас будет параметр для callback-метода, который на входе имеет поток с изображением капчи, а на выходе — строку с распознанной капчей:
TCapthcaRecognizeFunc = function(Captha: TStream): string of object; ... procedure GetExtractByOGRN(OGRN: string; CaptchaFunc: TCapthcaRecognizeFunc; isLegal: boolean; var Extract: TStream);
Функция, обрабатывающая капчу, может делать это как угодно: дать пользователю ввести её вручную, отправить изображение на платный сервер автоматического распознавания, самостоятельно распознать с помощью уникального ноу-хау алгоритма. Для простоты картины, и поскольку в моем случае потока капчи в промышленных масштабах не предвидится, выбираем первый вариант:
function TForm1.RecognizeFunc(captcha: TStream): string; begin CaptchaImg.Picture.LoadFromStream(captcha); Result := InputBox('Капча','Введите текст капчи с картинки', ''); end;
Второй вопрос — содержимое JSON-ответа сервера. Вот пример того, что в нём приходит:
{ "query": {"captcha":"382915", "ogrninnfl":null, "fam":null, "nam":null, "otch":null, "region":null, "ogrninnul":null, "namul":"правительство", "regionul":"73", "kind":"ul", "ul":true, "searchByOgrn":false, "nameEq":false, "searchByOgrnip":true}, "rows": [ {"T":"ED346E713D4A1AC851F9B589C6D2AECD1D809D5B6B5D1B98E697B6E0FD873E137B828AC59A60D159BB2894F11D00AB5639E2ACEE4E2ED5B7AC7A6EFE28FD987BC288B93C4D3D3EC1008DA0F128BA7E5E", "INN":"7325001144", "NAME":"ПРАВИТЕЛЬСТВО УЛЬЯНОВСКОЙ ОБЛАСТИ", "OGRN":"1027301175110", "ADRESTEXT":"432017, ОБЛАСТЬ УЛЬЯНОВСКАЯ, ГОРОД УЛЬЯНОВСК, ПЛОЩАДЬ СОБОРНАЯ, 1", "CNT":"4", "DTREG":"03.12.2002", "KPP":"732501001"}, {"T":"2ECB284C7682E5F1D1129AA3074FABB4B74BB28EA426AF79C091CEDEA0D9E391CA26FF405A7C9742466E19C78FBE5A59BDCBCD21268FFD8AFD3A8509CCA84541", "INN":"7303007375", "NAME":"СПЕЦИАЛИЗИРОВАННОЕ ГОСУДАРСТВЕННОЕ УЧРЕЖДЕНИЕ ПРИ ПРАВИТЕЛЬСТВЕ ОБЛАСТИ \"ФОНД ИМУЩЕСТВА УЛЬЯНОВСКОЙ ОБЛАСТИ\"", "OGRN":"1027301173283", "ADRESTEXT":"432063, ОБЛАСТЬ УЛЬЯНОВСКАЯ, ГОРОД УЛЬЯНОВСК, УЛИЦА ДМИТРИЯ УЛЬЯНОВА, 7", "CNT":"4", "DTREG":"27.11.2002", "KPP":"732501001", "DTEND":"01.09.2010"}, ] }
Как видно, результат возвращает объект «query», который содержит исходные параметры поиска (для того, чтобы они остались в полях формы для повторного использования) и массив объектов «rows». Ссылка на файл pdf комбинируется ява-скриптом с помощью выражения:
"https://egrul.nalog.ru/download/"
и значения ключа «Т» объекта. Время жизни сгенерированного файла pdf — несколько минут.
Две главные трудности, с которыми я столкнулся при создании http-запроса, это правильные значения заголовков и комбинирование строки с параметрами POST-запроса. Но простой анализ страницы с помощью встроенных средств браузера (в хроме вызываются по нажатию F12) дал всё необходимое. Вот пример заголовков, с которыми сервер дает правильный ответ вместо 400 Bad request:
POST / HTTP/1.1 Host: egrul.nalog.ru Connection: keep-alive Accept: application/json, text/javascript, */*; q=0.01 Origin: https://egrul.nalog.ru X-Requested-With: XMLHttpRequest User-Agent: Chrome/67.0.3396.99 Safari/537.36 Content-Type: application/x-www-form-urlencoded Referer: https://egrul.nalog.ru/ Accept-Encoding: gzip, deflate, br Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
А вот строка с параметрами:
kind=ul&srchUl=name&ogrninnul=7716819629&namul=%D0%BF%D1%80%D0%B0%D0%B2% D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D1%82%D0%B2%D0%BE®ionul=73 &srchFl=ogrn&ogrninnfl=&fam=&nam=&otch=®ion=&captcha=449023&captchaToken=DAEDA 7504CACAC82CF09E08319B68DF5F9BD62B2F44D33DD679DDE55B5CF58B17FEC84E78CEEB9639 84D2B2BD8C3AA15
Вооружившись этими исходными данными, приступим к реализации задачи. Я буду использовать следующие библиотеки для freepascal:
Synapse — очень удобная библиотека с максимально упрощенной (для использования) функцией отправки http-запросов на сервер, также работает и с SSL, но для этого необходимо наличие библиотек openSSL в папке проекта или системе, а также подключение дополнительного модуля. В наш проект достаточно подключить следующие модули библиотеки: httpsend, ssl_openssl, synautil.
Встроенную библиотеку fcl-json — нужные модули: fpjson и fpjsonrtti — для максимального удобства обработки возвращаемых в JSON объектов.
Отдельные модули встроенной библиотеки fcl-xml — для некоторых функций потребуется работа с частями HTML как DOM-объектами, поэтому подключим модули SAX_HTML, DOM_HTML, DOM.
Опишем типы и классы объектов, которые в итоге получились:
TEGRULItem = class(TCollectionItem) private fT, fINN, fNAME, fOGRN, fADRESTEXT, fCNT, fDTREG, fDTEND, fKPP: string; public function GetPdfLink: string; published property T: string read fT write fT; property INN: string read fINN write fINN; property NAME: string read fNAME write fNAME; property OGRN: string read fOGRN write fOGRN; property ADRESTEXT: string read fADRESTEXT write fADRESTEXT; property CNT: string read fCNT write fCNT; property DTREG: string read fDTREG write fDTREG; property DTEND: string read fDTEND write fDTEND; property KPP: string read fKPP write fKPP; end;
В этот класс мы запакуем объекты, которые будут возвращаться в массиве rows в JSON-ответе сервера. Считывать мы будем их с помощью JSONToCollection, но для этого нужно сделать каждый объект элементом коллекции и все соотносимые свойства объявить как published. RTTI функции в freepascal (как и в delphi) получают доступ к наименованиям свойств только в том случае, когда они объявлены именно в такой области видимости. А функция JSONToCollection из модуля fpjsonrtti — как раз RTTI-функция, которая сопоставляет названия ключей из JSON объекта с названиями свойств класса.
Также в интерфейсе класса имеется функция GetPdfLink, которая возвращает ссылку для скачивания pdf-файла со сведениями из ЕГРЮЛ с помощью конкатенации web-адреса и значения свойства «Т».
Основной класс, реализующий объявленный выше интерфейс, будет таким:
TEGRULStreamer = class(TInterfacedObject, IEGRULStreamer) private HTTPSender: THTTPSend; Doc: THTMLDocument; Inputs: TDOMNodeList; captchaURL, captchaToken, captcha, Params: string; function GetCaptchaToken: string; function GetLegalsList: TCollection; procedure PrepareHeaders; procedure ProcessCaptcha(CaptchaFunc: TCapthcaRecognizeFunc); public procedure GetExtractByOGRN(OGRN: string; CaptchaFunc: TCapthcaRecognizeFunc; isLegal: boolean; var Extract: TStream); procedure GetLegalsListByName(Name, Region: string; CaptchaFunc: TCapthcaRecognizeFunc; var LegalsList: TCollection); destructor Destroy; override; end;
Как видно, кроме реализации двух основных функций интерфейса все остальные свойства и методы класса будут скрытыми и нужны только для внутренней реализации. Их вообще можно было бы включить внутрь основных методов, но мы уже проходили уроки про дублирующийся код, наглядность и в целом рефакторинг.
С учетом инкапсуляции подготовительных действий основные методы вообще будут различаться только формированием строки параметров http-запроса и возвращаемым типом данных.
procedure TEGRULStreamer.GetExtractByOGRN(OGRN: string; CaptchaFunc: TCapthcaRecognizeFunc; isLegal: boolean; var Extract: TStream); begin ProcessCaptcha(CaptchaFunc); if isLegal then Params := 'kind=ul' else Params := 'kind=fl'; Params += '&srchUl=ogrn&srchFl=ogrn&ogrninnul='; if isLegal then Params += OGRN; Params += '&namul=®ionul=&ogrninnfl='; if not isLegal then Params += OGRN; Params += '&fam=&nam=&otch=®ion&captcha=' + captcha + '&captchaToken=' + captchaToken; WriteStrToStream(HTTPSender.Document, Params); if not HTTPSender.HTTPMethod('POST', EGRUL_URL) then raise Exception.Create('Сайт ИФНС не открывается'); HTTPSender.Headers.Clear; if HTTPSender.HTTPMethod('GET', TEGRULItem(GetLegalsList.Items[0]).GetPdfLink) then Extract := HTTPSender.Document else Extract := nil;
Здесь, как мы видим, метод также использует логический параметр isLegal, и если он не установлен в true, поиск идет по базе предпринимателей вместо юридических лиц.
procedure TEGRULStreamer.GetLegalsListByName(Name, Region: string; CaptchaFunc: TCapthcaRecognizeFunc; var LegalsList: TCollection); begin ProcessCaptcha(CaptchaFunc); Params := 'kind=ul&srchUl=name&srchFl=ogrn&ogrninnul=&namul='; Params += Name + '®ionul=' + Region + '&ogrninnfl=&fam=&nam=&otch=®ion'; Params += '&captcha=' + captcha + '&captchaToken=' + captchaToken; WriteStrToStream(HTTPSender.Document, Params); if not HTTPSender.HTTPMethod('POST', EGRUL_URL) then raise Exception.Create('Сайт ИФНС не открывается'); LegalsList := GetLegalsList; end;
Роль служебных методов сводится к следующему:
ProcessCaptcha — загружает первоначальную html страницу сервиса ФНС, ищет токен капчи, скачивает картинку, сгенерированную по этому токену, и перенаправляет её в callback-метод для распознавания капчи. В конце метод также устанавливает правильные заголовки для последующего POST-запроса.
GetCaptchaToken — загружает в DOM структуру все поля input со страницы, ищет скрытое поле с идентификатором capthcaToken и возвращает его значение.
GetLegalsList — с помощью RTTI функции JSONToCollection возвращает коллекцию объектов типа TEGRULItem, описанного выше.
GetPdfLink — для поиска по ОГРН или ИНН в правильном случае всегда будет возвращен только один результат, поэтому в GetExtractByOGRN функция вызывается для первого элемента в коллекции.
Поскольку этой мой первый опыт работы с сетью в freepascal, я очень рад, что всё получилось именно так, как я и задумывал. В работоспособном виде библиотека была изготовлена менее, чем за один день (спасибо форумчанам с freepascal.ru, рассказавшим о synapse).
Архив с тестом получившейся библиотеки и её кодом находится здесь.
Как всегда буду рад любой конструктивной критике как по проекту, так и по реализации. Понимаю, что есть много факторов, которые еще можно учесть: задержка с ответом на http-запрос, в результате чего подвиснет приложение; неверные http-ответы и другие ситуации.
В дальнейшем я планирую подключить онлайн-библиотеку с адресной базой ФИАС и реализовать возможность генерировать заполненные шаблоны заявлений, которые в общем случае редактируются в Программе подготовки документов для государственной регистрации.
P.S. Извини, Сбербанк, за роль подопытного кролика и сотни раз скачанную выписку. Всё во имя науки конечно же.

