В своей работе (юридической) я готов автоматизировать всё, что только поддаётся этому. Но пока прокаченные нейросетями роботы из утопии Германа Грефа не появились и не отняли всю работу у рядовых юристов, рутина надолго останется нашим главным спутником. Автоматизация этой рутины — то, чем я периодически занимаюсь на протяжении последних лет, будь то многочисленные таблицы в 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;
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 объекта с названиями свойств класса.
Основной класс, реализующий объявленный выше интерфейс, будет таким:
TEGRULStreamer = class(TInterfacedObject, IEGRULStreamer)
private
HTTPSender: THTTPSend;
Doc: THTMLDocument;
Inputs: TDOMNodeList;
captchaURL, captchaToken, captcha, Params: string;
function GetCaptchaToken: string;
function GetPdfLink(index: integer): 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', GetPdfLink(0)) then
Extract := HTTPSender.Document
else
Extract := nil;
end;
Здесь, как мы видим, метод также использует логический параметр 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 — возвращает ссылку для скачивания pdf-файла со сведениями из ЕГРЮЛ в отношении n-элемента коллекции (для поиска по ОГРН или ИНН в правильном случае всегда будет возвращен только один результат, поэтому в GetExtractByOGRN данный метод вызывается с параметром 0 — т.к. нужен первый и единственный элемент массива).
Поскольку этой мой первый опыт работы с сетью в freepascal, я очень рад, что всё получилось именно так, как я и задумывал. В работоспособном виде библиотека была изготовлена менее, чем за один день (спасибо форумчанам с freepascal.ru, рассказавшим о synapse).
Архив с тестом получившейся библиотеки и её кодом находится здесь.
Как всегда буду рад любой конструктивной критике как по проекту, так и по реализации. Понимаю, что есть много факторов, которые еще можно учесть: задержка с ответом на http-запрос, в результате чего подвиснет приложение; неверные http-ответы и другие ситуации.
В дальнейшем я планирую подключить онлайн-библиотеку с адресной базой ФИАС и реализовать возможность генерировать заполненные шаблоны заявлений, которые в общем случае редактируются в Программе подготовки документов для государственной регистрации.
P.S. Извини, Сбербанк, за роль подопытного кролика и сотни раз скачанную выписку. Всё во имя науки конечно же.
Автор: java73