Uso de la biblioteca FPC de InternetPCools en Delphi

De hecho, el artículo es algo más amplio: describe una forma de utilizar de manera transparente muchas otras bibliotecas (y no solo del mundo de Free Pascal ), y se eligió InternetTools debido a su notable propiedad; este es el caso cuando (sorprendentemente) falta Versión Delphi con las mismas características y usabilidad.

Esta biblioteca está diseñada para extraer información (análisis) de documentos web (XML y HTML), lo que le permite utilizar lenguajes de consulta de alto nivel como XPath y XQuery para indicar los datos requeridos y, como opción, proporcionar acceso directo a elementos de un árbol construido en el documento.

Introducción a InternetTools


Se ilustrará más material sobre la base de una tarea bastante simple, lo que implica obtener los elementos de las listas con viñetas y numeradas de este artículo que contienen enlaces, para los cuales, si observa la documentación , un código tan pequeño es suficiente (se basa en el penúltimo ejemplo con pequeños cambios sin principios) ):

uses xquery; const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; var ListValue: IXQValue; begin for ListValue in xqvalue(ArticleURL).retrieve.map(ListXPath) do Writeln(ListValue.toString); end. 

Sin embargo, ahora este código compacto y orientado a objetos solo se puede escribir en Free Pascal, pero necesitamos poder usar todo lo que esta biblioteca proporciona en una aplicación Delphi, y preferiblemente en un estilo similar, con las mismas comodidades; También es importante tener en cuenta que InternetTools es seguro para subprocesos (se permite el acceso desde muchos subprocesos al mismo tiempo), por lo que nuestra opción debe proporcionar esto.

Métodos de implementación


Si aborda la tarea lo más lejos posible, existen varias formas de usar algo escrito en otro idioma : formarán 3 grupos grandes:

  1. Colocando la biblioteca en un proceso separado , cuyo archivo ejecutable es creado por fuerzas, en este caso, FPC . Este método también se puede dividir en dos categorías para una posible comunicación de red:
  2. Encapsulando una biblioteca en un archivo DLL (en lo sucesivo, a veces denominado "biblioteca dinámica"), trabajando, por definición, dentro del marco de un solo proceso. Aunque los objetos COM se pueden colocar en archivos DLL, el artículo considerará un método más simple y que consume menos tiempo, que ofrece, con todo esto, la misma comodidad al llamar a la funcionalidad de la biblioteca.
  3. Portabilidad Como en casos anteriores, la idoneidad de este enfoque (reescribir el código en otro idioma) está determinado por el equilibrio entre sus pros y sus contras, pero en la situación con InternetTools las desventajas de la transferencia son mucho mayores, a saber: debido a la considerable cantidad de código de la biblioteca, tendrá que hacer un trabajo muy serio. (incluso teniendo en cuenta la similitud de los lenguajes de programación), y también periódicamente, debido al desarrollo del portátil , aparecerá la tarea de transferir correcciones y nuevas características a Delphi.

Dll


Además, para brindarle al lector la oportunidad de sentir la diferencia, hay 2 opciones que se distinguen por su facilidad de uso.

Implementación "clásica"


Intentemos comenzar a usar InternetTools en un estilo de procedimiento dictado por la naturaleza misma de una biblioteca dinámica que solo puede exportar funciones y procedimientos; Haremos la forma de comunicarnos con la DLL de manera similar a WinAPI, cuando primero se solicita el identificador de un determinado recurso, después de lo cual se realiza un trabajo útil, y luego se destruye el identificador resultante (cerrado). No es necesario considerar esta opción como un modelo a seguir en todo; se elige solo para demostración y posterior comparación con el segundo, una especie de pariente pobre.

La composición y propiedad de los archivos de la solución propuesta se verá así (las flechas muestran las dependencias):

La composición de los archivos de la implementación "clásica"


InternetTools.Types Module


Dado que ambos idiomas, Delphi y Free Pascal, son muy similares en este caso, es muy razonable asignar un módulo tan común que contenga los tipos utilizados en la lista de exportación de DLL, para no duplicar su definición en la aplicación InternetToolsUsage , que incluye prototipos de funcionalidad de una biblioteca dinámica:

 unit InternetTools.Types; interface type TXQHandle = Integer; implementation end. 

En esta implementación, solo se define un tipo tímido, pero en el futuro, el módulo "madurará" y su utilidad será innegable.

Biblioteca dinámica de InternetTools


La composición de los procedimientos y funciones de la DLL se selecciona para que sea mínima, pero suficiente para la implementación de la tarea anterior:

 library InternetTools; uses InternetTools.Types; function OpenDocument(const URL: WideString): TXQHandle; stdcall; begin ... end; procedure CloseHandle(const Handle: TXQHandle); stdcall; begin ... end; function Map(const Handle: TXQHandle; const XQuery: WideString): TXQHandle; stdcall; begin ... end; function Count(const Handle: TXQHandle): Integer; stdcall; begin ... end; function ValueByIndex(const Handle: TXQHandle; const Index: Integer): WideString; stdcall; begin ... end; exports OpenDocument, CloseHandle, Map, Count, ValueByIndex; begin end. 

Debido a la naturaleza de demostración de la implementación actual, no se proporciona el código completo; mucho más importante es cómo se utilizará esta API más simple más adelante. Aquí simplemente no necesita olvidarse del requisito de seguridad del hilo, que, aunque requerirá un poco de esfuerzo, no será algo complicado.

Aplicación de InternetTools


Gracias a los preparativos anteriores, se hizo posible reescribir el ejemplo de la lista en Delphi:

 program InternetToolsUsage; ... uses InternetTools.Types; const DLLName = 'InternetTools.dll'; function OpenDocument(const URL: WideString): TXQHandle; stdcall; external DLLName; procedure CloseHandle(const Handle: TXQHandle); stdcall; external DLLName; function Map(const Handle: TXQHandle; const XQuery: WideString): TXQHandle; stdcall; external DLLName; function Count(const Handle: TXQHandle): Integer; stdcall; external DLLName; function ValueByIndex(const Handle: TXQHandle; const Index: Integer): WideString; stdcall; external DLLName; const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; var RootHandle, ListHandle: TXQHandle; I: Integer; begin RootHandle := OpenDocument(ArticleURL); try ListHandle := Map(RootHandle, ListXPath); try for I := 0 to Count(ListHandle) - 1 do Writeln( ValueByIndex(ListHandle, I) ); finally CloseHandle(ListHandle); end; finally CloseHandle(RootHandle); end; ReadLn; end. 

Si no tiene en cuenta los prototipos de funciones y procedimientos de la biblioteca dinámica, no puede decir que el código es catastróficamente más pesado en comparación con la versión en Free Pascal, pero que si complicamos un poco la tarea e intentamos filtrar algunos elementos y mostrar las direcciones de enlace contenidas en restante:

 uses xquery; const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; HrefXPath = './a/@href'; var ListValue, HrefValue: IXQValue; begin for ListValue in xqvalue(ArticleURL).retrieve.map(ListXPath) do if {   } then for HrefValue in ListValue.map(HrefXPath) do Writeln(HrefValue.toString); end. 

Es posible hacer esto con la API API actual, pero la verbosidad del resultado ya es muy alta, lo que no solo reduce en gran medida la legibilidad del código, sino que también (y no menos importante) lo aleja de lo anterior:

 program InternetToolsUsage; ... const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; HrefXPath = './a/@href'; var RootHandle, ListHandle, HrefHandle: TXQHandle; I, J: Integer; begin RootHandle := OpenDocument(ArticleURL); try ListHandle := Map(RootHandle, ListXPath); try for I := 0 to Count(ListHandle) - 1 do if {   } then begin HrefHandle := Map(ListHandle, HrefXPath); try for J := 0 to Count(HrefHandle) - 1 do Writeln( ValueByIndex(HrefHandle, J) ); finally CloseHandle(HrefHandle); end; end; finally CloseHandle(ListHandle); end; finally CloseHandle(RootHandle); end; ReadLn; end. 

Obviamente, en casos reales y más complejos, el volumen de escritura solo crecerá rápidamente y, por lo tanto, pasaremos a una solución que esté libre de tales problemas.

Implementación de interfaz


El estilo de procedimiento de trabajar con la biblioteca, como se acaba de mostrar, es posible, pero tiene inconvenientes significativos. Debido a que la DLL como tal admite el uso de interfaces (como los tipos de datos recibidos y devueltos), puede organizar el trabajo con InternetTools de la misma manera conveniente que cuando lo usa con Free Pascal. En este caso, es conveniente cambiar ligeramente la composición de los archivos para distribuir la declaración y la implementación de las interfaces en módulos separados:

La composición de los archivos de implementación de la interfaz.

Como antes, examinaremos cada uno de los archivos en secuencia.

InternetTools.Types Module


Declara las interfaces que se implementarán en la DLL:

 unit InternetTools.Types; {$IFDEF FPC} {$MODE Delphi} {$ENDIF} interface type IXQValue = interface; IXQValueEnumerator = interface ['{781B23DC-E8E8-4490-97EE-2332B3736466}'] function MoveNext: Boolean; safecall; function GetCurrent: IXQValue; safecall; property Current: IXQValue read GetCurrent; end; IXQValue = interface ['{DCE33144-A75F-4C53-8D25-6D9BD78B91E4}'] function GetEnumerator: IXQValueEnumerator; safecall; function OpenURL(const URL: WideString): IXQValue; safecall; function Map(const XQuery: WideString): IXQValue; safecall; function ToString: WideString; safecall; end; implementation end. 

Las directivas de compilación condicional son necesarias debido al uso del módulo sin cambios en los proyectos Delphi- y FPC.

La interfaz IXQValueEnumerator en principio opcional, sin embargo, para poder usar bucles de la forma " for ... in ... " como ejemplo , no puede prescindir de ella; la segunda interfaz es la principal y es una envoltura analógica sobre IXQValue de InternetTools (se hizo especialmente con el mismo nombre para facilitar la correlación del futuro código de Delphi con la documentación de la biblioteca en Free Pascal). Si consideramos el módulo en términos de patrones de diseño, las interfaces declaradas en él son adaptadores , aunque con una pequeña característica: su implementación se encuentra en una biblioteca dinámica.

Aquí se describe bien la necesidad de establecer el tipo de llamada de llamada safecall para todos los métodos. La obligación de usar WideString lugar de cadenas "nativas" tampoco se WideString , ya que el tema del intercambio de estructuras de datos dinámicos con DLL está fuera del alcance de este artículo.

InternetTools.Realization Module


El primero en términos de importancia y volumen: es él quien, como se refleja en el título, contendrá la implementación de las interfaces de la anterior: para ambos, a la única clase TXQValue se le TXQValue , cuyos métodos son tan simples que casi todos consisten en una línea de código (esto es bastante esperado, porque toda la funcionalidad necesaria ya está contenida en la biblioteca, aquí solo necesita acceder a ella):

 unit InternetTools.Realization; {$MODE Delphi} interface uses xquery, InternetTools.Types; type IOriginalXQValue = xquery.IXQValue; TXQValue = class(TInterfacedObject, IXQValue, IXQValueEnumerator) private FOriginalXQValue: IOriginalXQValue; FEnumerator: TXQValueEnumerator; function MoveNext: Boolean; safecall; function GetCurrent: IXQValue; safecall; function GetEnumerator: IXQValueEnumerator; safecall; function OpenURL(const URL: WideString): IXQValue; safecall; function Map(const XQuery: WideString): IXQValue; safecall; function ToString: WideString; safecall; reintroduce; public constructor Create(const OriginalXQValue: IOriginalXQValue); overload; function SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult; override; end; implementation uses sysutils, comobj, w32internetaccess; function TXQValue.MoveNext: Boolean; begin Result := FEnumerator.MoveNext; end; function TXQValue.GetCurrent: IXQValue; begin Result := TXQValue.Create(FEnumerator.Current); end; function TXQValue.GetEnumerator: IXQValueEnumerator; begin FEnumerator := FOriginalXQValue.GetEnumerator; Result := Self; end; function TXQValue.OpenURL(const URL: WideString): IXQValue; begin FOriginalXQValue := xqvalue(URL).retrieve; Result := Self; end; function TXQValue.Map(const XQuery: WideString): IXQValue; begin Result := TXQValue.Create( FOriginalXQValue.map(XQuery) ); end; function TXQValue.ToString: WideString; begin Result := FOriginalXQValue.toJoinedString(LineEnding); end; constructor TXQValue.Create(const OriginalXQValue: IOriginalXQValue); begin FOriginalXQValue := OriginalXQValue; end; function TXQValue.SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult; begin Result := HandleSafeCallException(ExceptObject, ExceptAddr, GUID_NULL, ExceptObject.ClassName, ''); end; end. 

Vale la pena detenerse en el método SafeCallException : su bloqueo, en general, no es vital ( TXQValue rendimiento TXQValue no se verá afectado sin él), sin embargo, el código proporcionado aquí permite pasar el texto de excepción al lado de Delphi que ocurrirá en los métodos de llamada segura (detalles, nuevamente, se puede encontrar en un artículo reciente).

Esta solución, además de todo lo demás, es segura para subprocesos, siempre que IXQValue recibido, por ejemplo, a través de OpenURL , no se transmita entre subprocesos. Esto se debe al hecho de que la implementación de la interfaz solo redirige las llamadas a las herramientas de Internet ya seguras para subprocesos.

Biblioteca dinámica de InternetTools


Debido al trabajo realizado en los módulos anteriores, es suficiente que la DLL exporte una sola función (compárela con la opción donde se utilizó el estilo de procedimiento):

 library InternetTools; uses InternetTools.Types, InternetTools.Realization; function GetXQValue: IXQValue; stdcall; begin Result := TXQValue.Create; end; exports GetXQValue; begin SetMultiByteConversionCodePage(CP_UTF8); end. 

La llamada al procedimiento SetMultiByteConversionCodePage diseñada para funcionar correctamente con cadenas Unicode.

Aplicación de InternetTools


Si ahora formalizamos la solución Delphi del ejemplo inicial sobre la base de las interfaces propuestas, difícilmente diferirá de la de Free Pascal, lo que significa que la tarea establecida al comienzo del artículo puede considerarse completada:

 program InternetToolsUsage; ... uses System.Win.ComObj, InternetTools.Types; const DLLName = 'InternetTools.dll'; function GetXQValue: IXQValue; stdcall; external DLLName; const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; var ListValue: IXQValue; begin for ListValue in GetXQValue.OpenURL(ArticleURL).Map(ListXPath) do Writeln(ListValue.ToString); ReadLn; end. 

El módulo System.Win.ComObj no se conecta accidentalmente; sin él, el texto de todas las excepciones de llamada segura se convertirá en una "Excepción en el método de llamada segura" sin rostro, y con él el valor original generado en la DLL.

Un ejemplo un poco complicado también tiene diferencias mínimas en Delphi:

 ... const ArticleURL = 'https://habr.com/post/415617'; ListXPath = '//div[@class="post__body post__body_full"]//li[a]'; HrefXPath = './a/@href'; var ListValue, HrefValue: IXQValue; begin for ListValue in GetXQValue.OpenURL(ArticleURL).Map(ListXPath) do if {   } then for HrefValue in ListValue.Map(HrefXPath) do Writeln(HrefValue.ToString); ReadLn; end. 

La funcionalidad restante de la biblioteca.


Si observa las capacidades completas de la interfaz IXQValue de InternetTools, verá que la interfaz correspondiente de InternetTools.Types define solo 2 métodos ( Map y ToString ) del conjunto completo completo; agregar el resto que el lector considera necesario en su caso particular se lleva a cabo exactamente de la misma manera y simplemente: los métodos necesarios se escriben en InternetTools.Types , después de lo cual se construyen en el módulo de Realización de InternetTools.Realization .

Si necesita utilizar una funcionalidad ligeramente diferente, por ejemplo, gestión de cookies, la secuencia de pasos es muy similar:

  1. Se InternetTools.Types una nueva interfaz en InternetTools.Types .

     ... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ... 
  2. Luego se implementa en el módulo InternetTools.Realization :

     ... type TCookies = class(TInterfacedObject, ICookies) private procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; public function SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult; override; end; ... implementation uses ..., internetaccess; ... procedure TCookies.Add(const URL, Name, Value: WideString); begin defaultInternet.cookies.setCookie( decodeURL(URL).host, decodeURL(URL).path, Name, Value, [] ); end; procedure TCookies.Clear; begin defaultInternet.cookies.clear; end; ... 
  3. Después de eso, se devuelve una nueva función exportada a la DLL que devuelve esta interfaz:

     ... function GetCookies: ICookies; stdcall; begin Result := TCookies.Create; end; exports ..., GetCookies; ... 

Liberación de recursos


Aunque la biblioteca de InternetTools se basa en interfaces que controlan automáticamente la vida útil, hay un matiz no obvio que parece conducir a pérdidas de memoria: si ejecuta la próxima aplicación de consola (creada en Delphi, nada cambiará en el caso de FPC), luego, cada vez que presiona la tecla Intro, la memoria consumida por el proceso crecerá:

 ... const ArticleURL = 'https://habr.com/post/415617'; TitleXPath = '//head/title'; var I: Integer; begin for I := 1 to 100 do begin Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString ); Readln; end; end. 

No hay errores con el uso de interfaces aquí. El problema es que InternetTools no libera sus recursos internos asignados durante el análisis del documento (en el método OpenURL ); esto debe hacerse explícitamente después de que el trabajo con él haya finalizado; Para estos fines, el módulo de biblioteca xquery proporciona el procedimiento freeThreadVars , cuya llamada desde una aplicación Delphi se puede asegurar lógicamente expandiendo la lista de exportación de DLL:

 ... procedure FreeResources; stdcall; begin freeThreadVars; end; exports ..., FreeResources; ... 

Después de su uso, la pérdida de recursos se detendrá:

 for I := 1 to 100 do begin Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString ); FreeResources; Readln; end; 

Es importante comprender lo siguiente: una llamada a FreeResources lleva al hecho de que todas las interfaces recibidas anteriormente FreeResources tener sentido y cualquier intento de usarlas es inaceptable.

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


All Articles