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

в 12:30, , рубрики: Delphi, freepascal, lazarus, nalog.ru, автоматизация бизнеса, ЕГРЮЛ

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

В своей работе (юридической) я готов автоматизировать всё, что только поддаётся этому. Но пока прокаченные нейросетями роботы из утопии Германа Грефа не появились и не отняли всю работу у рядовых юристов, рутина надолго останется нашим главным спутником. Автоматизация этой рутины — то, чем я периодически занимаюсь на протяжении последних лет, будь то многочисленные таблицы в 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. На сайте размещена форма с полями ввода для идентификаторов поиска и проверки капчи:

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

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-ответа сервера. Вот пример того, что в нём приходит:

Ответ в отформатированном формате 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&regionul=73
&srchFl=ogrn&ogrninnfl=&fam=&nam=&otch=&region=&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-запроса и возвращаемым типом данных.

код метода TEGRULStreamer.GetExtractByOGRN

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=&regionul=&ogrninnfl=';
  if not isLegal then Params += OGRN;
  Params += '&fam=&nam=&otch=&region&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, поиск идет по базе предпринимателей вместо юридических лиц.

код метода TEGRULStreamer.GetLegalsListByName

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 + '&regionul=' + Region + '&ogrninnfl=&fam=&nam=&otch=&region';
  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-ответы и другие ситуации.

В дальнейшем я планирую подключить онлайн-библиотеку с адресной базой ФИАС и реализовать возможность генерировать заполненные шаблоны заявлений, которые в общем случае редактируются в Программе подготовки документов для государственной регистрации.

Автоматизация получения сведений из ЕГРЮЛ с помощью Freepascal - 3
P.S. Извини, Сбербанк, за роль подопытного кролика и сотни раз скачанную выписку. Всё во имя науки конечно же.

Автор: java73

Источник

Поделиться

* - обязательные к заполнению поля