De fato, o artigo é um pouco mais amplo - descreve uma maneira de usar de maneira
transparente muitas outras bibliotecas (e não apenas do mundo
Free Pascal ), e o
InternetTools foi escolhido por causa de sua propriedade notável - é o caso quando (surpreendentemente) está faltando Versão Delphi com os mesmos recursos e usabilidade.
Essa biblioteca foi projetada para extrair informações (análise) de documentos da Web (XML e HTML), permitindo o uso
de linguagens de consulta de alto nível , como
XPath e
XQuery, para indicar os dados necessários e, como opção, fornecendo
acesso direto a elementos de uma árvore construída no documento.
Introdução ao InternetTools
O material adicional será ilustrado com base em uma tarefa bastante simples, que implica obter os elementos de listas numeradas e com marcadores deste artigo que contêm links, para os quais, se você olhar para a
documentação , um código tão pequeno é suficiente (ele é construído com base no penúltimo exemplo com pequenas alterações sem princípios) ):
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.
No entanto, agora esse código compacto e orientado a objetos só pode ser escrito no Free Pascal, mas precisamos poder usar tudo o que essa biblioteca fornece em um aplicativo Delphi, e de preferência em um estilo semelhante, com as mesmas comodidades; Também é importante observar que o InternetTools é
seguro para threads (acessá-lo é permitido a partir de vários threads ao mesmo tempo); portanto, nossa opção deve fornecer isso.
Métodos de implementação
Se você abordar a tarefa o mais longe possível, existem várias maneiras de usar algo escrito em outro
idioma - elas formarão 3 grandes grupos:
- Colocar a biblioteca em um processo separado , cujo arquivo executável é criado por forças, nesse caso, FPC . Este método também pode ser dividido em duas categorias para possível comunicação em rede:
- Encapsular uma biblioteca em uma DLL (a seguir denominada "biblioteca dinâmica"), trabalhando, por definição, na estrutura de um único processo. Embora objetos COM possam ser colocados em DLLs, o artigo considerará um método mais simples e demorado, que oferece, com tudo isso, o mesmo conforto ao chamar a funcionalidade da biblioteca.
- Porting Como nos casos anteriores, a adequação dessa abordagem - reescrever código em outro idioma - é determinada pelo equilíbrio entre seus prós e contras, mas na situação com o InternetTools as desvantagens de portar são muito maiores, a saber: devido à quantidade considerável de código de biblioteca, você precisará realizar um trabalho muito sério (mesmo levando em consideração a semelhança das linguagens de programação) e também periodicamente, devido ao desenvolvimento da portátil , a tarefa de transferir correções e novos recursos para o Delphi aparecerá.
Dll
Além disso, para oferecer ao leitor a oportunidade de sentir a diferença, existem 2 opções que se distinguem pela conveniência de seu uso.
Implementação "clássica"
Vamos tentar começar a usar o InternetTools em um estilo processual ditado pela própria natureza de uma biblioteca dinâmica que pode exportar apenas funções e procedimentos; definiremos o modo de nos comunicar com a DLL semelhante ao WinAPI, quando o identificador de um determinado recurso for solicitado pela primeira vez, após o qual um trabalho útil seja executado e, em seguida, o identificador resultante será destruído (fechado). Não é necessário considerar essa opção como um modelo em tudo - ela é escolhida apenas para demonstração e comparação subsequente com a segunda - uma espécie de parente pobre.
A composição e a propriedade dos arquivos da solução proposta terão a seguinte aparência (as setas mostram as dependências):
Módulo InternetTools.Types
Como os dois idiomas - Delphi e Free Pascal - são muito semelhantes nesse caso, é muito razoável selecionar um módulo comum contendo os tipos usados na lista de exportação da DLL, para não duplicar sua definição no aplicativo
InternetToolsUsage
, que inclui protótipos de funcionalidade de uma biblioteca dinâmica:
unit InternetTools.Types; interface type TXQHandle = Integer; implementation end.
Nesta implementação, apenas um tipo tímido é definido, mas, no futuro, o módulo "amadurecerá" e sua utilidade se tornará inegável.
Biblioteca dinâmica do InternetTools
A composição dos procedimentos e funções da DLL é selecionada para ser mínima, mas suficiente para a implementação da
tarefa acima:
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.
Devido à natureza de demonstração da implementação atual, o código completo não é fornecido - muito mais importante é como essa API mais simples será usada posteriormente. Aqui, você simplesmente não precisa esquecer o requisito de segurança de encadeamento, que, embora exija algum esforço, mas não será algo complicado.
Aplicativo InternetToolsUsage
Graças aos preparativos anteriores, foi possível reescrever o
exemplo da
lista em 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.
Se você não levar em conta os protótipos de funções e procedimentos da biblioteca dinâmica, não poderá dizer que o código se tornou catastroficamente mais pesado em comparação com a versão no Free Pascal, mas que se complicarmos um pouco a tarefa e tentarmos filtrar alguns elementos e exibir os endereços de link contidos em restantes:
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.
É possível fazer isso com a DLL da API atual, mas a verbosidade do resultado já é muito alta, o que não apenas reduz bastante a legibilidade do código, mas também (e não menos importante) o afasta do exposto acima:
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 - em casos reais e mais complexos, o volume de escrita só aumentará rapidamente e, portanto, passaremos para uma solução livre de tais problemas.
Implementação de interface
O estilo procedural de trabalhar com a biblioteca, como acabamos de mostrar, é possível, mas possui desvantagens significativas. Devido ao fato de a DLL oferecer suporte ao uso de interfaces (como tipos de dados recebidos e retornados), você pode organizar o trabalho com o InternetTools da mesma maneira conveniente que ao usá-lo com o Free Pascal. Nesse caso, é desejável alterar ligeiramente a composição dos arquivos para distribuir a declaração e a implementação das interfaces em módulos separados:
Como antes, examinaremos cada um dos arquivos em sequência.
Módulo InternetTools.Types
Declara as interfaces a serem implementadas na 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.
As diretivas de compilação condicional são necessárias devido ao uso do módulo inalterado nos projetos Delphi- e FPC.
A interface
IXQValueEnumerator
em princípio, opcional, no entanto, para poder usar loops do formato "
for ... in ...
" como
exemplo , você não pode ficar sem ele; a segunda interface é a principal e é um invólucro analógico sobre o
IXQValue
do InternetTools (foi criado especialmente com o mesmo nome para facilitar a correlação do futuro código Delphi com a documentação da biblioteca no Free Pascal). Se considerarmos o módulo em termos de padrões de design, as interfaces declaradas nele são
adaptadoras , embora com um pequeno recurso - sua implementação está localizada em uma biblioteca dinâmica.
A necessidade de definir o tipo de chamada de chamada
safecall
para todos os métodos está bem descrita
aqui . A obrigação de usar
WideString
vez de seqüências "nativas" também não será comprovada, pois o tópico de troca de estruturas de dados dinâmicas com DLLs está além do escopo deste artigo.
Módulo InternetTools.Realization
O primeiro em termos de importância e volume - é ele quem, como refletido no título, conterá a implementação das interfaces da anterior: para ambas, a única classe
TXQValue
, cujos métodos são tão simples que quase todos consistem em uma linha de código (isso é bastante esperado, porque toda a funcionalidade necessária já está contida na biblioteca - aqui você só precisa acessá-la):
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 a pena parar no método
SafeCallException
- seu bloqueio, em geral, não é vital (
TXQValue
desempenho
TXQValue
não
TXQValue
afetado sem ele); no entanto, o código fornecido aqui permite que você transmita o texto da exceção para o lado do Delphi que ocorrerá nos métodos de chamada segura (detalhes, novamente, pode ser encontrado em um
artigo recente).
Essa solução, além de tudo o mais, é segura para threads - desde que
IXQValue
recebido, por exemplo, através do
OpenURL
, não seja transmitido entre threads. Isso se deve ao fato de que a implementação da interface redireciona apenas as chamadas para o InternetTools já seguro para threads.
Biblioteca dinâmica do InternetTools
Devido ao trabalho realizado nos módulos acima, é suficiente para a DLL exportar uma única função (compare com a
opção em que o estilo procedural foi usado):
library InternetTools; uses InternetTools.Types, InternetTools.Realization; function GetXQValue: IXQValue; stdcall; begin Result := TXQValue.Create; end; exports GetXQValue; begin SetMultiByteConversionCodePage(CP_UTF8); end.
A chamada para o procedimento
SetMultiByteConversionCodePage
projetada para funcionar corretamente com seqüências de caracteres Unicode.
Aplicativo InternetToolsUsage
Se formalizarmos agora a solução Delphi do
exemplo inicial com base nas interfaces propostas, dificilmente será diferente da do Free Pascal, o que significa que a tarefa definida no início do artigo pode ser considerada concluída:
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.
O módulo
System.Win.ComObj
não está conectado acidentalmente - sem ele, o texto de todas as exceções de safecall se tornará uma "exceção no método de segurança de chamada" sem rosto e, com ele, o valor original gerado na DLL.
Um
exemplo um pouco
complicado também tem diferenças mínimas no 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.
A funcionalidade restante da biblioteca
Se você observar todos os recursos da interface
IXQValue do InternetTools, verá que a
interface correspondente do
InternetTools.Types
define apenas dois métodos (
Map
e
ToString
) de todo o conjunto completo; adicionar o restante que o leitor considerar necessário em seu caso específico é realizado exatamente da mesma maneira e de maneira simples: os métodos necessários são escritos no
InternetTools.Types
, após o qual são criados no módulo
InternetTools.Realization
com código (geralmente como uma única linha).
Se você precisar usar uma funcionalidade ligeiramente diferente, por exemplo, gerenciamento de cookies, a sequência de etapas será muito semelhante:
- Uma nova interface é
InternetTools.Types
no InternetTools.Types
:
... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ...
- Em seguida, é implementado no 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; ...
- Depois disso, uma nova função exportada é retornada para a DLL que retorna essa interface:
... function GetCookies: ICookies; stdcall; begin Result := TCookies.Create; end; exports ..., GetCookies; ...
Liberação de recursos
Embora a biblioteca do InternetTools seja baseada em interfaces que controlam automaticamente a vida útil, há uma nuance não óbvia que parece levar a vazamentos de memória - se você executar o próximo aplicativo de console (criado no Delphi, nada mudará no caso do FPC), toda vez que você pressionar a tecla Enter, a memória consumida pelo processo aumentará:
... 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.
Não há erros com o uso de interfaces aqui. O problema é que o InternetTools não libera seus recursos internos alocados durante a análise do documento (no método
OpenURL
) - isso deve ser feito
explicitamente , após o término do trabalho; para esses propósitos, o módulo da biblioteca
xquery
fornece o procedimento
freeThreadVars
, cuja chamada de um aplicativo Delphi pode ser logicamente garantida, expandindo a lista de exportação da DLL:
... procedure FreeResources; stdcall; begin freeThreadVars; end; exports ..., FreeResources; ...
Após seu uso, a perda de recursos será interrompida:
for I := 1 to 100 do begin Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString ); FreeResources; Readln; end;
É importante entender o seguinte: uma chamada para o
FreeResources
leva ao fato de que todas as interfaces recebidas anteriormente ficam sem sentido e qualquer tentativa de usá-las é inaceitável.