- PVSM.RU - https://www.pvsm.ru -
На самом деле, статья несколько шире – она описывает способ, позволяющий прозрачно задействовать и многие другие библиотеки (причём не только из мира Free Pascal [1]), а InternetTools [2] выбрана из-за своего замечательного свойства – это тот случай, когда (как ни удивительно) отсутствует Delphi-вариант с такими же широкими возможностями и удобством использования.
Эта библиотека предназначена для извлечения информации (парсинга) из веб-документов (XML и HTML), позволяя использовать для указания нужных данных как языки запросов высокого уровня [3], такие как XPath [4] и XQuery [5], так и, в качестве одного из вариантов, предоставляя прямой доступ [6] к элементам дерева, построенного по документу.
Дальнейший материал будет иллюстрироваться на основе довольно простой задачи, подразумевающей получение тех элементов маркированных и нумерованных списков данной статьи, что содержат ссылки, для чего, если обратиться к документации [7], достаточно такого небольшого кода (он построен на основе предпоследнего примера с внесением небольших, непринципиальных изменений):
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.
Однако сейчас этот компактный и объектно-ориентированный код может быть написан лишь на Free Pascal, нам же требуется получить возможность задействовать всё, что предоставляет эта библиотека, в Delphi-приложении, причём желательно в аналогичном стиле, с теми же удобствами; также важно отметить, что InternetTools потокобезопасна [8] (обращение к ней допустимо из многих потоков одновременно), поэтому и наш вариант должен обеспечивать это.
Если подходить к задаче максимально издалека, то можно выделить несколько способов задействовать что-то, написанное на другом ЯП, – они составят 3 большие группы:
Далее, с целью предоставить читателю возможность ощутить разницу, приводятся 2 варианта, отличающиеся удобством своего применения.
Попробуем для начала использовать InternetTools в процедурном стиле, диктуемом самой природой динамической библиотеки, способной экспортировать лишь функции и процедуры; манеру общения с DLL сделаем похожей на WinAPI, когда сначала запрашивается дескриптор (handle) некоего ресурса, после чего выполняется полезная работа, а затем идёт уничтожение (закрытие) полученного дескриптора. Не нужно во всём рассматривать этот вариант как образец для подражания – он выбран лишь для демонстрации и последующего сравнения со вторым – своего рода бедный родственник.
Состав и принадлежность файлов предложенного решения будут выглядеть так (стрелками показаны зависимости):
Т. к. в данном случае оба языка – Delphi и Free Pascal – являются очень похожими, то весьма разумно выделить такой общий модуль, содержащий типы, используемые в списке экспорта DLL, – это для того, чтобы затем не дублировать их определение в приложении InternetToolsUsage
, включающем в себя прототипы функционала из динамической библиотеки:
unit InternetTools.Types;
interface
type
TXQHandle = Integer;
implementation
end.
В данной реализации определён всего лишь один стыдливый тип, но в последующем модуль «повзрослеет» и его полезность станет несомненной.
Состав процедур и функций DLL выбран минимальным, но достаточным для осуществления поставленной выше задачи [17]:
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.
Ввиду демонстрационного характера текущей реализации, полный код не приводится – много важнее то, как это простейшее API будет использоваться далее. Здесь только не нужно забывать о требовании потокобезопасности, которое пусть и потребует определённых усилий, но не явится чем-то сложным.
Благодаря предыдущим приготовлениям, стало возможно переписать пример со списками [17] на 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.
Если не принимать во внимание прототипы функций и процедур из динамической библиотеки, то нельзя сказать, что код катастрофически утяжелился по сравнению с вариантом на Free Pascal, но что, если мы совсем немного усложним задачу и попробуем отфильтровать некоторые элементы и вывести адреса ссылок, содержащиеся в оставшихся:
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.
Сделать подобное с текущим API DLL возможно, но многословность получающегося уже весьма велика, что не только сильно снижает читаемость кода, но также (и это не менее важно) отдаляет его от вышеприведённого:
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.
Очевидно – в реальных, более комплексных случаях, объём написанного станет лишь стремительно расти, в связи с чем перейдём к решению, избавленному от подобных проблем.
Процедурный стиль работы с библиотекой, как только что было показано, возможен, но имеет существенные недостатки. Благодаря тому, что DLL как таковая поддерживает использование интерфейсов (в качестве принимаемых и возвращаемых типов данных), можно организовать работу с InternetTools в той же удобной манере, что и при её применении с Free Pascal. Состав файлов при этом желательно немного поменять, чтобы распределить объявление и реализацию интерфейсов по отдельным модулям:
Как и до этого, последовательно рассмотрим каждый из файлов.
Объявляет интерфейсы, подлежащие реализации в 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.
Директивы условной компиляции необходимы из-за использования модуля в неизменном виде как в Delphi-, так и в FPC-проекте.
Интерфейс IXQValueEnumerator
в принципе необязателен, однако, чтобы иметь возможность использовать циклы вида «for ... in ...
» как из примера [17], без него не обойтись; второй интерфейс основной и является обёрткой-аналогом над IXQValue
[18] из InternetTools (он специально сделан одноимённым, чтобы было проще соотносить будущий Delphi-код с библиотечной документацией на Free Pascal). Если рассматривать модуль в терминах шаблонов проектирования, то объявленные в нём интерфейсы представляют собой адаптеры [19], пусть и с небольшой особенностью – их реализация располагается в динамической библиотеке.
Необходимость задавать для всех методов тип вызова safecall
хорошо описана здесь [20]. Обязательность применения WideString
вместо «родных» строк также не будет обосновываться, ибо тема по обмену динамическими структурами данных с DLL выходит за рамки статьи.
Первый и по важности, и по объёму – именно он, как отражено в названии, станет содержать реализацию интерфейсов из предыдущего: за оба из них ответственным назначен единственный класс TXQValue
, методы которого настолько просты, что почти все состоят из одной строки кода (это вполне ожидаемо, ведь весь нужный функционал уже содержится в библиотеке – здесь всего-навсего требуется обратиться к нему):
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.
Стоит остановиться на методе SafeCallException
– его перекрытие, по большому счёту, не является жизненно необходимым (работоспособность TXQValue
ничуть без него не пострадает), однако приведённый здесь код позволяет передать на Delphi-сторону текст исключений, что будут возникать в safecall-методах (подробности, опять же, можно найти в уже приводившейся недавно статье [20]).
Данное решение ко всему прочему является потокобезопасным – при условии, что IXQValue
, полученный, например, через OpenURL
, не передаётся между потоками. Это достигнуто за счёт того, что реализация интерфейса только перенаправляет вызовы уже потокобезопасной InternetTools.
Из-за проделанной в модулях выше работы, DLL достаточно экспортировать единственную функцию (сравните с вариантом [21], где применялся процедурный стиль):
library InternetTools;
uses
InternetTools.Types, InternetTools.Realization;
function GetXQValue: IXQValue; stdcall;
begin
Result := TXQValue.Create;
end;
exports
GetXQValue;
begin
SetMultiByteConversionCodePage(CP_UTF8);
end.
Вызов процедуры SetMultiByteConversionCodePage
предназначен для корректной работы с юникодовыми строками.
Если теперь оформить Delphi-решение изначального примера [17] на основе предложенных интерфейсов, то оно почти не будет отличаться от такового на Free Pascal, а значит поставленная в самом начале статьи задача может считаться выполненной:
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.
Модуль System.Win.ComObj
подключен не случайно – без него текст всех safecall-исключений станет представлять собой безликое «Exception in safecall method», а с ним – исходное значение, сгенерированное в DLL.
Чуть усложнённый пример [22] аналогично имеет минимальные отличия на 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.
Если взглянуть на полные возможности интерфейса IXQValue [23] из InternetTools, то станет видно, что соответствующий интерфейс [24] из InternetTools.Types
определяет лишь 2 метода (Map
и ToString
) из всего богатого набора; добавление оставшихся, что читатель сочтёт нужными в своём конкретном случае, выполняется абсолютно аналогично и просто: необходимые методы прописываются в InternetTools.Types
, после чего в модуле InternetTools.Realization
они наращиваются кодом (чаще всего в виде одной строки).
Если требуется задействовать несколько иную функциональность, для примера – управление куками, то последовательность шагов очень похожа:
InternetTools.Types
:
...
ICookies = interface
['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}']
procedure Add(const URL, Name, Value: WideString); safecall;
procedure Clear; safecall;
end;
...
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;
...
...
function GetCookies: ICookies; stdcall;
begin
Result := TCookies.Create;
end;
exports
...,
GetCookies;
...
Хотя библиотека InternetTools и основана на интерфейсах, подразумевающих автоматическое управление временем жизни, но имеется один неочевидный нюанс, приводящий, казалось бы, к утечкам памяти – если запустить следующее консольное приложение (созданное на Delphi, но ничего не изменится и в случае с FPC), то при каждом нажатии клавиши ввода память, потребляемая процессом, станет расти:
...
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.
Каких-либо ошибок с применением интерфейсов здесь нет. Проблема заключается в том, что InternetTools не освобождает свои внутренние ресурсы, выделенные при анализе документа (в методе OpenURL
), – это необходимо проделать явно [25], после того, как работа с ним закончена; для этих целей библиотечный модуль xquery
предоставляет процедуру freeThreadVars
[26], вызов которой из Delphi-приложения логично обеспечить за счёт расширения списка экспорта DLL:
...
procedure FreeResources; stdcall;
begin
freeThreadVars;
end;
exports
...,
FreeResources;
...
После её задействования потеря ресурсов прекратится:
for I := 1 to 100 do
begin
Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString );
FreeResources;
Readln;
end;
Важно понимать следующее – вызов FreeResources
приводит к тому, что все ранее полученные интерфейсы становятся бессмысленными и любые попытки их использования недопустимы.
Автор: SergeyPyankov
Источник [27]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/delphi/285093
Ссылки в тексте:
[1] Free Pascal: https://www.freepascal.org
[2] InternetTools: http://www.benibela.de/sources_en.html#internettools
[3] языки запросов высокого уровня: http://www.benibela.de/documentation/internettools/#AnchorNamePXP
[4] XPath: https://ru.wikipedia.org/wiki/XPath
[5] XQuery: https://ru.wikipedia.org/wiki/XQuery
[6] прямой доступ: http://www.benibela.de/documentation/internettools/#AnchorNameTree
[7] документации: http://www.benibela.de/documentation/internettools/#AnchorNameStarted
[8] потокобезопасна: https://ru.wikipedia.org/wiki/Thread-safety
[9] межпроцессного взаимодействия: https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D0%B6%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BD%D0%BE%D0%B5_%D0%B2%D0%B7%D0%B0%D0%B8%D0%BC%D0%BE%D0%B4%D0%B5%D0%B9%D1%81%D1%82%D0%B2%D0%B8%D0%B5
[10] разделяемую память: https://ru.wikipedia.org/wiki/%D0%A0%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D1%8F%D0%B5%D0%BC%D0%B0%D1%8F_%D0%BF%D0%B0%D0%BC%D1%8F%D1%82%D1%8C
[11] неименованные каналы: https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%B8%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BA%D0%B0%D0%BD%D0%B0%D0%BB
[12] именованные каналы: https://ru.wikipedia.org/wiki/%D0%98%D0%BC%D0%B5%D0%BD%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D0%BA%D0%B0%D0%BD%D0%B0%D0%BB
[13] RPC: https://ru.wikipedia.org/wiki/%D0%A3%D0%B4%D0%B0%D0%BB%D1%91%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B2%D1%8B%D0%B7%D0%BE%D0%B2_%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D0%B4%D1%83%D1%80
[14] DCOM: https://ru.wikipedia.org/wiki/DCOM
[15] могут быть размещены: http://www.delphikingdom.ru/asp/viewitem.asp?catalogid=1113
[16] развития портируемого: https://github.com/benibela/internettools/pulse/monthly
[17] задачи: #FirstTask
[18] IXQValue
: http://www.benibela.de/documentation/internettools/xquery.IXQValue.html
[19] адаптеры: https://ru.wikipedia.org/wiki/%D0%90%D0%B4%D0%B0%D0%BF%D1%82%D0%B5%D1%80_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[20] здесь: http://www.delphikingdom.ru/asp/viewitem.asp?catalogid=1392#SubSubHeader_1_2_3
[21] вариантом: #ClassicLibrary
[22] усложнённый пример: #SecondTask
[23] IXQValue: https://github.com/benibela/internettools/blob/9c54cd77352bd4204d5bc438d29300fabd28d267/data/xquery.pas#L344
[24] соответствующий интерфейс: #InternetTools_Types
[25] явно: https://github.com/benibela/internettools/issues/16#issuecomment-369726703
[26] freeThreadVars
: http://www.benibela.de/documentation/internettools/xquery.html#freeThreadVars
[27] Источник: https://habr.com/post/415617/?utm_campaign=415617
Нажмите здесь для печати.