Automatización de la obtención de información de USRLE usando Freepascal



En mi trabajo (legal), estoy listo para automatizar todo lo que solo se presta a esto. Pero hasta que los robots bombeados por las redes neuronales de la utopía del alemán Gref no aparecieran y no tomaran todo el trabajo de los abogados ordinarios, la rutina seguirá siendo nuestro principal compañero durante mucho tiempo. La automatización de esta rutina es algo que he estado haciendo periódicamente durante los últimos años, ya sea numerosas tablas en Excel con un montón de fórmulas que le permiten imprimir rápidamente cien de los mismos documentos de correo en Word, o bien, informes generados automáticamente. Pero hay cosas que no puedes hacer con simples fórmulas y sustituciones. Aquí es donde la programación viene al rescate, lo que me ha gustado desde la infancia, y sucedió que comenzó con Delphi. Ahora es más fácil para mí que en C # o Python, que comencé a aprender recientemente, hacer rápidamente algún tipo de proyecto en el entorno de Lazarus usando Freepascal. Y sí, creo seriamente que las capacidades de este entorno son más que suficientes. Por lo tanto, automatizar el USRLE, lo has adivinado, debe hacerse usando pascal.

Un abogado de una oficina de consultoría que realiza negocios de docenas de entidades legales, un abogado de pan corporativo y cualquier otro abogado que se enfrente a garantizar las actividades de las organizaciones: todos saben cómo docenas y cientos de nombres diferentes, números TIN, PSRN se mezclan en su cabeza, cómo es fácil olvidar quién es el gerente, y cuando su plazo de renovación es apropiado, ¿hay algún problema con las acciones de la LLC y con el pago de su capital social? Bueno, la necesidad de hacer rápidamente algún tipo de documento, que incluye muchos detalles que cambian constantemente, implica errores periódicos y errores tipográficos. Para automatizar esos procesos, necesitaba una solución de base de datos que me permitiera crear documentos usando plantillas, mantener varios registros, rastrear cambios y no perder ningún plazo. Bueno, una de las simplificaciones necesarias de la vida es la recepción rápida de un archivo nuevo con información de la USRLE del sitio web del Servicio de Impuestos Federales . Por supuesto, nadie dice que usar el sitio directamente es largo y difícil, pero está de acuerdo en que hacer clic en un botón sin salir de la aplicación es mucho más divertido, y puede hacerlo sin interrumpir la llamada telefónica (o una taza de café).

Entonces, para empezar, decidiremos qué queremos obtener. El sitio le permite buscar en el registro oficial de la USRLE un número OGRN o TIN único y dar un resultado relevante en forma de una breve información sobre la persona y un enlace para descargar el archivo pdf con un extracto. Además, la búsqueda puede ser difusa por nombre con un filtro adicional por región (tema de la Federación de Rusia). Y en este caso, el sitio emite una tabla con todas las personas adecuadas y con el mismo conjunto de datos, incluidos enlaces a pdf.

Por lo tanto, en un caso particular, la función preparada debe devolver pdf en forma de archivo (o, mejor, una secuencia), que tiene un OGRN o TIN en la entrada. Pero para la universalización y la posibilidad de una mayor expansión, no descuidaremos todas las características del sitio y tampoco realizaremos una función de búsqueda difusa con el retorno de un conjunto de datos encontrados por el nombre de la organización, teniendo en cuenta el filtro por región o sin él. Intentemos describir las interfaces de estas funciones:

IEGRULstreamer = interface procedure GetExtractByOGRN(OGRN: string; ; isLegal: boolean; var Extract: TStream); procedure GetLegalsListByName(Name, Region: string; ; var LegalsList: TCollection); end; 

Para entender cuál es el misterioso parámetro X y la colección de la cual regresará la segunda función, veremos cómo el sitio ejecuta la solicitud.

1. El sitio tiene un formulario con campos de entrada para identificadores para buscar y verificar captcha:



2. Captcha se genera utilizando un campo oculto pregenerado con el nombre captchaToken, que utiliza un script Java para generar una imagen captcha para este token.

3. Después de hacer clic en el botón "buscar", se envía una solicitud POST al servidor, en cuyos resultados de procesamiento se devuelve JSON con una matriz de objetos. Esta respuesta JSON utiliza otro script Java para completar la tabla que vemos en los resultados de búsqueda.

Entonces, el primer inconveniente es la verificación de captcha. Para no sobrecargar nuestros métodos de interacción con el sitio con exceso de funcionalidad, tomaremos medidas para procesar captcha como una función separada. Y en X, tendremos un parámetro para el método de devolución de llamada, que tiene una imagen captcha en la entrada y una cadena con captcha reconocido en la salida:

 TCapthcaRecognizeFunc = function(Captha: TStream): string of object; ... procedure GetExtractByOGRN(OGRN: string; CaptchaFunc: TCapthcaRecognizeFunc; isLegal: boolean; var Extract: TStream); 

La función que procesa el captcha puede hacerlo a su gusto: permita que el usuario lo ingrese manualmente, envíe la imagen a un servidor de pago para su reconocimiento automático, de manera independiente, hágalo mediante el conocimiento único del algoritmo. Para simplificar la imagen, y dado que en mi caso no se espera un flujo de captcha a escala industrial, seleccionamos la primera opción:

 function TForm1.RecognizeFunc(captcha: TStream): string; begin CaptchaImg.Picture.LoadFromStream(captcha); Result := InputBox('','    ', ''); end; 

La segunda pregunta es el contenido de la respuesta JSON del servidor. Aquí hay un ejemplo de lo que viene en él:

Respuesta con formato 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"}, ] } 


Como puede ver, el resultado devuelve un objeto de consulta que contiene los parámetros de búsqueda iniciales (para que permanezcan en los campos del formulario para su reutilización) y una matriz de filas. El enlace al archivo pdf se combina con un script java usando la expresión:
  "https://egrul.nalog.ru/download/" 
y el valor clave "T" del objeto. La vida útil del archivo pdf generado es de unos minutos.

Las dos dificultades principales que encontré al crear la solicitud http son los valores de encabezado correctos y la combinación de la cadena con los parámetros de la solicitud POST. Pero un simple análisis de la página utilizando las herramientas integradas del navegador (en Chrome se llama presionando F12) le dio todo lo que necesita. Aquí hay un ejemplo de encabezados con los que el servidor da la respuesta correcta en lugar de 400 Solicitud incorrecta:

 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 

Y aquí está la línea con los parámetros:

 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 

Armados con estos datos iniciales, procedemos a la implementación de la tarea. Usaré las siguientes bibliotecas para freepascal:

Synapse es una biblioteca muy conveniente con la función más simplificada (para uso) de enviar solicitudes http al servidor, también funciona con SSL, pero esto requiere la presencia de bibliotecas openSSL en la carpeta o sistema del proyecto, así como la conexión de un módulo adicional. Es suficiente conectar los siguientes módulos de biblioteca a nuestro proyecto: httpsend, ssl_openssl, synautil.

La biblioteca incorporada fcl-json - los módulos necesarios: fpjson y fpjsonrtti - para la máxima conveniencia de procesar objetos devueltos a JSON.

Módulos separados de la biblioteca incorporada fcl-xml : para algunas funciones, deberá trabajar con partes de HTML como objetos DOM, por lo que conectaremos los módulos SAX_HTML, DOM_HTML, DOM.

Describamos los tipos y clases de objetos que finalmente resultaron:

 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; 

En esta clase empacaremos objetos que se devolverán en la matriz de filas en la respuesta JSON del servidor. Los leeremos usando JSONToCollection, pero para esto necesitamos hacer de cada objeto un elemento de la colección y declarar todas las propiedades relacionadas como se publican. Las funciones RTTI en freepascal (así como en delphi) obtienen acceso a los nombres de propiedad solo cuando se declaran en este ámbito. Y la función JSONToCollection del módulo fpjsonrtti es solo una función RTTI que compara los nombres de las claves del objeto JSON con los nombres de las propiedades de la clase.

También hay una función GetPdfLink en la interfaz de clase, que devuelve un enlace para descargar un archivo pdf con información de la USRLE mediante la concatenación de la dirección web y el valor de la propiedad "T".


La clase principal que implementa la interfaz declarada anteriormente será así:

  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; 


Como puede ver, además de la implementación de las dos funciones principales de la interfaz, todas las demás propiedades y métodos de la clase estarán ocultos y serán necesarios solo para la implementación interna. En general, podrían incluirse dentro de los métodos principales, pero ya hemos revisado las lecciones sobre código duplicado, visualización y refactorización en general.

Dada la encapsulación de acciones preparatorias, los métodos principales generalmente diferirán solo formando una cadena de parámetros de solicitud http y el tipo de datos devuelto.

código de método 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', TEGRULItem(GetLegalsList.Items[0]).GetPdfLink) then Extract := HTTPSender.Document else Extract := nil; 


Aquí, como podemos ver, el método también utiliza el parámetro booleano isLegal, y si no se establece en verdadero, la búsqueda pasa por la base de datos de empresarios en lugar de entidades legales.

código de método 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; 


El papel de los métodos de utilidad es el siguiente:

ProcessCaptcha : descarga la página html inicial del Servicio de Impuestos Federales, busca un token de captcha, descarga una imagen generada por este token y lo redirige a un método de devolución de llamada para el reconocimiento de captcha. Al final, el método también establece los encabezados correctos para la solicitud POST posterior.

GetCaptchaToken : carga todos los campos de entrada de la página en la estructura DOM, busca un campo oculto con el identificador capthcaToken y devuelve su valor.

GetLegalsList : el uso de la función RTTI JSONToCollection devuelve una colección de objetos de tipo TEGRULItem descritos anteriormente.

GetPdfLink : para buscar por OGRN o TIN, en el caso correcto, siempre se devolverá un solo resultado, por lo tanto, en GetExtractByOGRN se llama a la función para el primer elemento de la colección.

Como esta es mi primera experiencia con una red en freepascal, estoy muy contento de que todo haya salido exactamente como lo pretendía. En una forma de trabajo, la biblioteca se hizo en menos de un día (gracias a los usuarios del foro de freepascal.ru, que hablaron sobre la sinapsis).

Un archivo con una prueba de la biblioteca resultante y su código está aquí .

Como siempre, me complacerá cualquier crítica constructiva tanto sobre el proyecto como sobre la implementación. Entiendo que hay muchos factores que aún pueden tenerse en cuenta: la demora en responder a la solicitud http, como resultado de lo cual la aplicación se congelará; Respuestas http incorrectas y otras situaciones.

En el futuro, planeo conectar una biblioteca en línea con la base de datos de direcciones FIAS y darme cuenta de la capacidad de generar plantillas de aplicaciones completas, que generalmente se editan en el Programa de preparación de documentos para el registro estatal .


PD: Lo siento, Sberbank, por el papel del conejo experimental y cientos de veces el extracto descargado. Todo en nombre de la ciencia, por supuesto.

Source: https://habr.com/ru/post/es419063/


All Articles