Verwenden der InternetPCools-FPC-Bibliothek in Delphi

Tatsächlich ist der Artikel etwas breiter - er beschreibt eine Möglichkeit, viele andere Bibliotheken (und nicht nur aus der Free Pascal- Welt) transparent zu verwenden, und InternetTools wurde aufgrund seiner bemerkenswerten Eigenschaften ausgewählt - dies ist der Fall, wenn (überraschenderweise) fehlt Delphi-Version mit den gleichen Funktionen und Benutzerfreundlichkeit.

Diese Bibliothek dient zum Extrahieren von Informationen (Parsen) aus Webdokumenten (XML und HTML). So können Sie Abfragesprachen auf hoher Ebene wie XPath und XQuery verwenden , um die erforderlichen Daten anzugeben, und optional einen direkten Zugriff darauf bereitstellen Elemente eines auf dem Dokument erstellten Baums.

Einführung in InternetTools


Weiteres Material wird auf der Grundlage einer ziemlich einfachen Aufgabe illustriert, bei der die Elemente von Listen mit Aufzählungszeichen und Nummern dieses Artikels abgerufen werden, die Links enthalten. Wenn Sie sich die Dokumentation ansehen, reicht ein so kleiner Code aus (er basiert auf dem vorletzten Beispiel mit kleinen, prinzipienlosen Änderungen ):

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. 

Jetzt kann dieser kompakte und objektorientierte Code jedoch nur in Free Pascal geschrieben werden. Wir müssen jedoch in der Lage sein, alles, was diese Bibliothek bietet, in einer Delphi-Anwendung und vorzugsweise in einem ähnlichen Stil mit denselben Annehmlichkeiten zu verwenden. Es ist auch wichtig zu beachten, dass InternetTools threadsicher ist (der Zugriff von vielen Threads gleichzeitig ist zulässig), daher sollte unsere Option dies bereitstellen.

Implementierungsmethoden


Wenn Sie sich der Aufgabe so weit wie möglich nähern, gibt es verschiedene Möglichkeiten, etwas zu verwenden, das in einer anderen Sprache geschrieben ist - es werden drei große Gruppen gebildet:

  1. Platzieren der Bibliothek in einem separaten Prozess , dessen ausführbare Datei von Forces erstellt wird, in diesem Fall FPC . Diese Methode kann für eine mögliche Netzwerkkommunikation auch in zwei Kategorien unterteilt werden:
  2. Kapselung einer Bibliothek in einer DLL (im Folgenden manchmal als "dynamische Bibliothek" bezeichnet), die per Definition im Rahmen eines einzelnen Prozesses arbeitet. Obwohl COM-Objekte in DLLs platziert werden können, wird in dem Artikel eine einfachere und weniger zeitaufwendige Methode betrachtet, die beim Aufrufen der Funktionalität der Bibliothek den gleichen Komfort bietet.
  3. Portierung Wie in früheren Fällen wird die Angemessenheit dieses Ansatzes - das Umschreiben von Code in eine andere Sprache - durch das Gleichgewicht zwischen Vor- und Nachteilen bestimmt. In der Situation mit InternetTools sind die Nachteile der Portierung jedoch viel größer: Aufgrund der beträchtlichen Menge an Bibliothekscode müssen Sie einige sehr ernsthafte Arbeiten ausführen (auch unter Berücksichtigung der Ähnlichkeit der Programmiersprachen) und aufgrund der Entwicklung der tragbaren Sprache wird auch in regelmäßigen Abständen die Aufgabe auftreten, Korrekturen und neue Funktionen an Delphi zu übertragen.

Dll


Um dem Leser die Möglichkeit zu geben, den Unterschied zu spüren, gibt es zwei Optionen, die sich durch die Bequemlichkeit ihrer Verwendung auszeichnen.

"Klassische" Implementierung


Versuchen wir, InternetTools in einem prozeduralen Stil zu verwenden, der von der Natur einer dynamischen Bibliothek bestimmt wird, die nur Funktionen und Prozeduren exportieren kann. Wir werden die Kommunikation mit der DLL ähnlich wie bei WinAPI gestalten, wenn das Handle einer bestimmten Ressource zum ersten Mal angefordert wird, danach nützliche Arbeit geleistet wird und dann das resultierende Handle zerstört (geschlossen) wird. Es ist nicht notwendig, diese Option als Vorbild für alles zu betrachten - sie wird nur zur Demonstration und zum anschließenden Vergleich mit der zweiten ausgewählt - eine Art armer Verwandter.

Die Zusammensetzung und der Besitz der Dateien der vorgeschlagenen Lösung sehen folgendermaßen aus (Pfeile zeigen die Abhängigkeiten):

Die Zusammensetzung der Dateien der "klassischen" Implementierung


InternetTools.Types-Modul


Da beide Sprachen - Delphi und Free Pascal - in diesem Fall sehr ähnlich sind, ist es sehr sinnvoll, ein solches gemeinsames Modul zuzuweisen, das die in der DLL-Exportliste verwendeten Typen enthält, um ihre Definition in der InternetToolsUsage Anwendung nicht zu duplizieren , die Prototypen von Funktionen aus einer dynamischen Bibliothek enthält:

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

In dieser Implementierung wird nur ein schüchterner Typ definiert, aber in Zukunft wird das Modul "reifen" und seine Nützlichkeit wird unbestreitbar.

Dynamische InternetTools-Bibliothek


Die Zusammensetzung der Prozeduren und Funktionen der DLL wird so gewählt, dass sie minimal ist, aber für die Implementierung der obigen Aufgabe ausreicht:

 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. 

Aufgrund des Demo-Charakters der aktuellen Implementierung wird nicht der vollständige Code angegeben. Viel wichtiger ist, wie diese einfachste API später verwendet wird. Hier müssen Sie einfach nicht die Anforderung der Thread-Sicherheit vergessen, die zwar einige Anstrengungen erfordert, aber nicht kompliziert ist.

InternetToolsUsage-Anwendung


Dank der vorherigen Vorbereitungen wurde es möglich, das Listenbeispiel in Delphi neu zu schreiben:

 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. 

Wenn Sie die Prototypen von Funktionen und Prozeduren aus der dynamischen Bibliothek nicht berücksichtigen, können Sie nicht sagen, dass der Code im Vergleich zur Version auf Free Pascal katastrophal schwerer ist, aber wenn wir die Aufgabe etwas komplizieren und versuchen, einige Elemente herauszufiltern und die darin enthaltenen Linkadressen anzuzeigen verbleibend:

 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. 

Dies ist mit der aktuellen API-DLL möglich, aber die Ausführlichkeit des Ergebnisses ist bereits sehr hoch, was die Lesbarkeit des Codes nicht nur stark verringert, sondern ihn auch (und nicht weniger wichtig) von oben entfernt:

 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. 

Offensichtlich - in realen, komplexeren Fällen wird das Schreibvolumen nur schnell wachsen, und deshalb werden wir zu einer Lösung übergehen, die frei von solchen Problemen ist.

Schnittstellenimplementierung


Der soeben gezeigte prozedurale Arbeitsstil mit der Bibliothek ist möglich, weist jedoch erhebliche Nachteile auf. Aufgrund der Tatsache, dass die DLL als solche die Verwendung von Schnittstellen (als empfangene und zurückgegebene Datentypen) unterstützt, können Sie die Arbeit mit InternetTools auf dieselbe bequeme Weise organisieren wie bei Verwendung mit Free Pascal. In diesem Fall ist es wünschenswert, die Zusammensetzung der Dateien geringfügig zu ändern, um die Deklaration und Implementierung der Schnittstellen in separate Module zu verteilen:

Die Zusammensetzung der Schnittstellenimplementierungsdateien

Nach wie vor werden wir jede der Dateien nacheinander untersuchen.

InternetTools.Types-Modul


Deklariert die in der DLL zu implementierenden Schnittstellen:

 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. 

Bedingte Kompilierungsanweisungen sind erforderlich, da das Modul sowohl in Delphi- als auch in FPC-Projekten unverändert verwendet wird.

Die IXQValueEnumerator Schnittstelle ist grundsätzlich optional. Um jedoch Schleifen der Form " for ... in ... " als Beispiel verwenden zu können , können Sie nicht darauf verzichten. Die zweite Schnittstelle ist die Hauptschnittstelle und ein analoger Wrapper über IXQValue von InternetTools (sie wurde speziell mit demselben Namen erstellt, um die Korrelation des zukünftigen Delphi-Codes mit der Bibliotheksdokumentation auf Free Pascal zu vereinfachen). Wenn wir das Modul in Bezug auf Entwurfsmuster betrachten, dann sind die darin deklarierten Schnittstellen Adapter , wenn auch mit einer kleinen Funktion - ihre Implementierung befindet sich in einer dynamischen Bibliothek.

Die Notwendigkeit, den safecall für alle Methoden safecall , wird hier ausführlich beschrieben. Die Verpflichtung, WideString anstelle von "nativen" Zeichenfolgen zu verwenden, wird ebenfalls nicht begründet, da das Thema des Austauschs dynamischer Datenstrukturen mit DLLs den Rahmen dieses Artikels WideString .

InternetTools.Realization-Modul


Der erste in Bezug auf Wichtigkeit und Umfang - er ist es, der, wie im Titel widergespiegelt, die Implementierung der Schnittstellen aus der vorherigen enthält: Für beide wird der einzigen TXQValue Klasse TXQValue , deren Methoden so einfach sind, dass fast alle aus einer Codezeile bestehen (das ist ziemlich erwartet, da alle notwendigen Funktionen bereits in der Bibliothek enthalten sind - hier müssen Sie nur darauf zugreifen):

 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. 

Es lohnt sich, bei der SafeCallException Methode SafeCallException - ihre Blockierung ist im Großen und Ganzen nicht entscheidend ( TXQValue Leistung von TXQValue ohne sie nicht beeinträchtigt). Der hier angegebene Code ermöglicht jedoch die Übergabe des Ausnahmetextes an die Delphi-Seite, die bei Safecall-Methoden auftritt (Details, wieder kann in einem aktuellen Artikel gefunden werden ).

Diese Lösung ist zusätzlich zu allem anderen threadsicher - vorausgesetzt, IXQValue , das beispielsweise über OpenURL , wird nicht zwischen Threads übertragen. Dies liegt daran, dass die Implementierung der Schnittstelle nur Aufrufe an die bereits threadsicheren InternetTools umleitet.

Dynamische InternetTools-Bibliothek


Aufgrund der in den obigen Modulen geleisteten Arbeit reicht es für die DLL aus, eine einzelne Funktion zu exportieren (vergleiche mit der Option, bei der der prozedurale Stil verwendet wurde):

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

Der Aufruf der SetMultiByteConversionCodePage Prozedur SetMultiByteConversionCodePage konzipiert, dass sie mit Unicode-Zeichenfolgen korrekt funktioniert.

InternetToolsUsage-Anwendung


Wenn wir nun die Delphi-Lösung des ersten Beispiels auf der Grundlage der vorgeschlagenen Schnittstellen formalisieren, wird sie sich kaum von der bei Free Pascal unterscheiden, was bedeutet, dass die am Anfang des Artikels festgelegte Aufgabe als erledigt betrachtet werden kann:

 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. 

Das System.Win.ComObj Modul ist nicht versehentlich verbunden - ohne es wird der Text aller Safecall-Ausnahmen zu einer gesichtslosen „Ausnahme in der Safecall-Methode“ und damit zum ursprünglichen Wert, der in der DLL generiert wird.

Ein etwas kompliziertes Beispiel weist ebenfalls minimale Unterschiede zu Delphi auf:

 ... 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. 

Die verbleibende Funktionalität der Bibliothek


Wenn Sie sich die vollständigen Funktionen der IXQValue- Schnittstelle von InternetTools ansehen , werden Sie feststellen , dass die entsprechende Schnittstelle von InternetTools.Types nur zwei Methoden ( Map und ToString ) aus dem gesamten Rich-Set definiert. Das Hinzufügen des Restes, den der Leser in seinem speziellen Fall für notwendig hält, erfolgt genauso und einfach: Die erforderlichen Methoden werden in InternetTools.Types , wonach sie im InternetTools.Realization Modul mit Code aufgebaut werden (meistens als einzelne Zeile).

Wenn Sie eine etwas andere Funktionalität verwenden müssen, z. B. die Cookie-Verwaltung, ist die Reihenfolge der Schritte sehr ähnlich:

  1. In InternetTools.Types eine neue Schnittstelle InternetTools.Types :

     ... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ... 
  2. Dann wird es im InternetTools.Realization Modul implementiert:

     ... 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. Danach wird eine neue exportierte Funktion an die DLL zurückgegeben, die diese Schnittstelle zurückgibt:

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

Freigabe von Ressourcen


Obwohl die InternetTools-Bibliothek auf Schnittstellen basiert, die die Lebensdauer automatisch steuern, gibt es eine nicht offensichtliche Nuance, die zu Speicherverlusten zu führen scheint: Wenn Sie die nächste Konsolenanwendung ausführen (die auf Delphi erstellt wurde, ändert sich bei FPC nichts). Jedes Mal, wenn Sie die Eingabetaste drücken, wächst der vom Prozess verbrauchte Speicher:

 ... 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. 

Bei der Verwendung von Schnittstellen treten hier keine Fehler auf. Das Problem ist, dass InternetTools seine internen Ressourcen, die während der Analyse des Dokuments zugewiesen wurden (in der OpenURL Methode), nicht OpenURL Dies muss explizit erfolgen, nachdem die Arbeit damit abgeschlossen ist. Zu diesem Zweck stellt das xquery Bibliotheksmodul die Prozedur freeThreadVars , deren Aufruf aus einer Delphi-Anwendung durch Erweitern der DLL-Exportliste logisch sichergestellt werden kann:

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

Nach seiner Verwendung hört der Ressourcenverlust auf:

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

Es ist wichtig, Folgendes zu verstehen: Ein Aufruf von FreeResources führt dazu, dass alle zuvor empfangenen Schnittstellen bedeutungslos werden und Versuche, sie zu verwenden, nicht akzeptabel sind.

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


All Articles