En fait, l'article est un peu plus large - il décrit un moyen d'utiliser de manière
transparente de nombreuses autres bibliothèques (et pas seulement du monde de
Free Pascal ), et
InternetTools a été choisi en raison de sa propriété remarquable - c'est le cas quand (étonnamment) est manquant Version Delphi avec les mêmes fonctionnalités et facilité d'utilisation.
Cette bibliothèque est conçue pour extraire des informations (analyse) de documents Web (XML et HTML), vous permettant d'utiliser
des langages de requête de haut niveau tels que
XPath et
XQuery pour indiquer les données requises et, en option, offrant
un accès direct à éléments d'un arbre construit sur le document.
Introduction aux InternetTools
D'autres éléments seront illustrés sur la base d'une tâche assez simple, ce qui implique d'obtenir les éléments des listes à puces et numérotées de cet article qui contiennent des liens, pour lesquels, si vous regardez la
documentation , un si petit code suffit (il est construit sur la base de l'avant-dernier exemple avec de petites modifications sans principes) ):
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.
Cependant, maintenant ce code compact et orienté objet ne peut être écrit qu'en Free Pascal, mais nous devons être en mesure d'utiliser tout ce que cette bibliothèque fournit dans une application Delphi, et de préférence dans un style similaire, avec les mêmes commodités; Il est également important de noter qu'InternetTools est
sûr pour les threads (y accéder est autorisé à partir de plusieurs threads en même temps), donc notre option devrait le fournir.
Méthodes d'implémentation
Si vous abordez la tâche le plus loin possible, il y a plusieurs façons d'utiliser quelque chose écrit dans une autre
langue - ils formeront 3 grands groupes:
- Placer la bibliothèque dans un processus séparé , dont le fichier exécutable est créé par les forces, dans ce cas, FPC . Cette méthode peut également être divisée en deux catégories pour une communication réseau possible:
- Encapsulation d'une bibliothèque dans une DLL (ci-après parfois dénommée «bibliothèque dynamique»), travaillant, par définition, dans le cadre d'un processus unique. Bien que les objets COM puissent être placés dans des DLL, l'article considérera une méthode plus simple et moins longue, qui offre, avec tout cela, le même confort lors de l'appel des fonctionnalités de la bibliothèque.
- Portage Comme dans les cas précédents, la pertinence de cette approche - réécrire du code dans une autre langue - est déterminée par l'équilibre entre ses avantages et ses inconvénients, mais dans la situation avec InternetTools, les inconvénients du portage sont beaucoup plus importants, à savoir: en raison de la quantité considérable de code de bibliothèque, vous devrez effectuer un travail très sérieux (même en tenant compte de la similitude des langages de programmation), et aussi périodiquement, en raison du développement du portable , la tâche de transférer les corrections et les nouvelles fonctionnalités vers Delphi apparaîtra.
Dll
De plus, afin de donner au lecteur la possibilité de ressentir la différence, il existe 2 options qui se distinguent par la commodité de leur utilisation.
Implémentation "classique"
Essayons de commencer à utiliser InternetTools dans un style procédural dicté par la nature même d'une bibliothèque dynamique qui ne peut exporter que des fonctions et des procédures; nous allons rendre la façon de communiquer avec la DLL similaire à WinAPI, lorsque le handle d'une certaine ressource est d'abord demandé, après quoi un travail utile est effectué, puis le handle résultant est détruit (fermé). Il n'est pas nécessaire de considérer cette option comme un modèle dans tout - elle est choisie uniquement pour la démonstration et la comparaison ultérieure avec la seconde - une sorte de parent pauvre.
La composition et la propriété des fichiers de la solution proposée ressembleront à ceci (les flèches indiquent les dépendances):
Module InternetTools.Types
Étant donné que les deux langages - Delphi et Free Pascal - sont très similaires dans ce cas, il est très raisonnable de sélectionner un tel module commun contenant les types utilisés dans la liste d'exportation DLL, afin de ne pas dupliquer leur définition dans l'application
InternetToolsUsage
. , qui inclut des prototypes de fonctionnalités d'une bibliothèque dynamique:
unit InternetTools.Types; interface type TXQHandle = Integer; implementation end.
Dans cette implémentation, un seul type timide est défini, mais à l'avenir, le module «mûrira» et son utilité deviendra indéniable.
Bibliothèque dynamique InternetTools
La composition des procédures et fonctions de la DLL est choisie pour être minimale, mais suffisante pour la mise en œuvre de la
tâche ci-dessus:
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.
En raison de la nature démo de l'implémentation actuelle, le code complet n'est pas donné - beaucoup plus important est de savoir comment cette API la plus simple sera utilisée plus tard. Ici, vous n'avez tout simplement pas besoin d'oublier l'exigence de sécurité des threads, qui, bien que cela nécessitera des efforts, mais ce ne sera pas quelque chose de compliqué.
InternetToolsUsage Application
Grâce aux préparatifs précédents, il est devenu possible de réécrire l'
exemple de liste dans 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 vous ne prenez pas en compte les prototypes de fonctions et procédures de la bibliothèque dynamique, vous ne pouvez pas dire que le code est catastrophiquement plus lourd par rapport à la version sur Free Pascal, mais que si nous compliquons un peu la tâche et essayons de filtrer certains éléments et d'afficher les adresses de liens contenues dans restant:
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.
Il est possible de le faire avec la DLL API actuelle, mais la verbosité du résultat est déjà très élevée, ce qui réduit non seulement considérablement la lisibilité du code, mais aussi (et non moins important) l'éloigne de ce qui précède:
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.
De toute évidence - dans des cas réels et plus complexes, le volume d'écriture ne fera que croître rapidement, et nous allons donc passer à une solution qui est exempte de tels problèmes.
Implémentation de l'interface
Le style procédural de travailler avec la bibliothèque, comme on vient de le montrer, est possible, mais présente des inconvénients importants. Étant donné que la DLL en tant que telle prend en charge l'utilisation d'interfaces (comme les types de données reçus et renvoyés), vous pouvez organiser le travail avec InternetTools de la même manière pratique que lorsque vous l'utilisez avec Free Pascal. Dans ce cas, il est souhaitable de modifier légèrement la composition des fichiers afin de répartir la déclaration et l'implémentation des interfaces dans des modules séparés:
Comme précédemment, nous examinerons chacun des fichiers en séquence.
Module InternetTools.Types
Déclare les interfaces à implémenter dans 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.
Les directives de compilation conditionnelle sont nécessaires en raison de l'utilisation du module inchangé dans les projets Delphi et FPC.
L'interface
IXQValueEnumerator
en principe facultative, cependant, afin de pouvoir utiliser des boucles de la forme "
for ... in ...
" comme
exemple , vous ne pouvez pas vous en passer; la deuxième interface est la principale et est un wrapper analogique sur
IXQValue
d'InternetTools (il a été spécialement conçu du même nom pour faciliter la corrélation du futur code Delphi avec la documentation de la bibliothèque sur Free Pascal). Si nous considérons le module en termes de modèles de conception, les interfaces déclarées sont des
adaptateurs , bien qu'avec une petite fonctionnalité - leur implémentation est située dans une bibliothèque dynamique.
La nécessité de définir le type d'appel
safecall
pour toutes les méthodes est bien décrite
ici . L'obligation d'utiliser
WideString
au lieu de chaînes «natives» ne sera pas non plus étayée, car le sujet de l'échange de structures de données dynamiques avec des DLL dépasse le cadre de cet article.
Module InternetTools.Realization
La première en termes d'importance et de volume - c'est lui qui, comme le reflète le titre, contiendra l'implémentation des interfaces de la précédente: pour chacune d'elles, la seule classe
TXQValue
se voit
TXQValue
, dont les méthodes sont si simples que presque toutes consistent en une seule ligne de code (c'est assez attendu, car toutes les fonctionnalités nécessaires sont déjà contenues dans la bibliothèque - ici, il vous suffit d'y accéder):
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.
Cela vaut la peine de s'arrêter à la méthode
SafeCallException
- son blocage, dans l'ensemble, n'est pas vital (
TXQValue
performances
TXQValue
n'en souffriront pas), cependant, le code donné ici vous permet de passer le texte d'exception du côté Delphi qui se produira dans les méthodes de safecall (détails, encore une fois, se trouve dans un
article récent).
Cette solution, en plus de tout le reste, est
IXQValue
threads - à condition que
IXQValue
reçue, par exemple via
OpenURL
, ne soit pas transmise entre les threads. Cela est dû au fait que la mise en œuvre de l'interface ne redirige que les appels vers les InternetTools déjà thread-safe.
Bibliothèque dynamique InternetTools
En raison du travail effectué dans les modules ci-dessus, il suffit que la DLL exporte une seule fonction (comparer avec l'
option où le style procédural a été utilisé):
library InternetTools; uses InternetTools.Types, InternetTools.Realization; function GetXQValue: IXQValue; stdcall; begin Result := TXQValue.Create; end; exports GetXQValue; begin SetMultiByteConversionCodePage(CP_UTF8); end.
L'appel à la procédure
SetMultiByteConversionCodePage
conçu pour fonctionner correctement avec les chaînes Unicode.
InternetToolsUsage Application
Si nous formalisons maintenant la solution Delphi de l'
exemple initial sur la base des interfaces proposées, elle ne différera guère de celle de Free Pascal, ce qui signifie que la tâche définie au tout début de l'article peut être considérée comme terminée:
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.
Le module
System.Win.ComObj
n'est pas connecté accidentellement - sans lui, le texte de toutes les exceptions safecall deviendra une «exception dans la méthode safecall» sans visage, et avec lui la valeur d'origine générée dans la DLL.
Un
exemple légèrement
compliqué a également des différences minimes sur 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.
Les fonctionnalités restantes de la bibliothèque
Si vous regardez toutes les capacités de l'interface
IXQValue d'InternetTools, vous verrez que l'
interface correspondante d'
InternetTools.Types
définit seulement 2 méthodes (
Map
et
ToString
) de l'ensemble riche; ajouter le reste que le lecteur juge nécessaire dans son cas particulier s'effectue exactement de la même manière et simplement: les méthodes nécessaires sont écrites dans
InternetTools.Types
, après quoi elles sont construites dans le module
InternetTools.Realization
avec du code (le plus souvent sur une seule ligne).
Si vous devez utiliser une fonctionnalité légèrement différente, par exemple, la gestion des cookies, la séquence des étapes est très similaire:
- Une nouvelle interface est
InternetTools.Types
dans InternetTools.Types
:
... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ...
- Il est ensuite implémenté dans le module
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; ...
- Après cela, une nouvelle fonction exportée est retournée à la DLL qui renvoie cette interface:
... function GetCookies: ICookies; stdcall; begin Result := TCookies.Create; end; exports ..., GetCookies; ...
Libération de ressources
Bien que la bibliothèque InternetTools soit basée sur des interfaces qui contrôlent automatiquement la durée de vie, il existe une nuance non évidente qui semblerait entraîner des fuites de mémoire - si vous exécutez la prochaine application console (créée sur Delphi, rien ne changera dans le cas de FPC), puis à chaque fois que vous appuyez sur la touche entrée, la mémoire consommée par le processus augmente:
... 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.
Il n'y a pas d'erreur avec l'utilisation des interfaces ici. Le problème est qu'InternetTools ne libère pas ses ressources internes allouées pendant l'analyse du document (dans la méthode
OpenURL
) - cela doit être fait
explicitement une fois le travail avec lui terminé; à ces fins, le module de bibliothèque
xquery
fournit la procédure
freeThreadVars
, dont l'appel à partir d'une application Delphi peut être logiquement assuré en développant la liste d'exportation DLL:
... procedure FreeResources; stdcall; begin freeThreadVars; end; exports ..., FreeResources; ...
Après son utilisation, la perte de ressources s'arrêtera:
for I := 1 to 100 do begin Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString ); FreeResources; Readln; end;
Il est important de comprendre ce qui suit - un appel à
FreeResources
conduit au fait que toutes les interfaces précédemment reçues
FreeResources
sens et toute tentative de les utiliser est inacceptable.