实际上,本文的范围更广-它描述了一种
透明地使用许多其他库的方法(不仅限于
Free Pascal世界),并且因其非凡的性能
而选择了
InternetTools-当(令人惊讶地)缺少这种情况具有相同功能和可用性的Delphi版本。
该库旨在从Web文档(XML和HTML)中提取信息(解析),使您可以使用
高级查询语言(例如
XPath和
XQuery)来指示所需的数据,并且可以选择
直接访问以下内容:在文档上构建的树的元素。
InternetTools简介
在一个相当简单的任务的基础上,将进一步说明材料,这意味着获得包含链接的本文项目符号列表和编号列表中的那些元素,对于这些元素,如果您查看
文档 ,则只需要一个很小的代码(它是基于倒数第二个示例而进行的,小的,无原则的更改) ):
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是
线程安全的 (允许同时从多个线程访问它),因此我们的选项应该提供这一点。
实施方法
如果您尽可能远地完成任务,那么有几种方法可以使用另一种
语言编写的内容-它们将组成3个大类:
- 将库放在一个单独的进程中 ,该进程的可执行文件由强制创建,在这种情况下为FPC 。 对于可能的网络通信,此方法也可以分为两类:
- 根据定义,将库封装在DLL中 (以下有时称为“动态库”),可以在同一过程中工作。 尽管可以将 COM对象放置在DLL中,但本文将考虑一种更简单且耗时更少的方法,在调用库的功能时,所有这些方法都具有相同的舒适性。
- 移植 与以前的情况一样,这种方法的适当性(将代码重写为另一种语言)取决于其优缺点之间的平衡,但是在使用InternetTools的情况下,移植的缺点要大得多,即:由于大量的库代码,您将需要做一些非常认真的工作(甚至考虑到编程语言的相似性),而且由于便携式语言的发展,周期性地出现将更正和新功能转移到Delphi的任务。
Dll
此外,为了使读者有机会感受到不同,有2种选择以使用方便为特色。
“经典”实施
让我们尝试以过程样式开始使用InternetTools,该样式由动态库的本质决定,该动态库只能导出函数和过程。 当首先请求某个资源的句柄,然后执行有用的工作,然后销毁(关闭)所得的句柄时,我们将采用类似于WinAPI的方式与DLL进行通信。 不必将此选项视为所有事物中的榜样-只是为了演示和与第二个对象进行比较而选择-一种可怜的亲戚。
拟议解决方案的文件组成和所有权如下所示(箭头表示依赖关系):
InternetTools.Types模块
由于在这种情况下,Delphi和Free Pascal这两种语言非常相似,因此分配这样一个包含DLL导出列表中使用的类型的通用模块是非常合理的,以免在
InternetToolsUsage
应用程序中重复它们的定义,其中包括动态库中的功能原型:
unit InternetTools.Types; interface type TXQHandle = Integer; implementation end.
在此实现中,仅定义了一种害羞类型,但是在将来,该模块将“成熟”,其实用性将不可否认。
InternetTools动态库
DLL的过程和功能的组成被选择为最小,但足以实现上述
任务 :
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。 在这里,您不需要忘记线程安全性的要求,尽管这需要一些努力,但并不会很复杂。
InternetTools使用申请
由于之前的准备工作,因此可以在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本身支持接口(接收和返回的数据类型)的使用,因此您可以以与Free Pascal一起使用的便捷方式来组织InternetTools的工作。 在这种情况下,最好稍微更改文件的组成,以便将接口的声明和实现分发到单独的模块中:
和以前一样,我们将依次检查每个文件。
InternetTools.Types模块
声明要在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 ...
”形式的循环作为
示例 ,您不能没有它。 第二个接口是主要接口,它是InternetTools上
IXQValue
的模拟包装器(它是用相同的名称专门制作的,以使将来的Delphi代码与Free Pascal上的库文档更容易关联)。 如果我们从设计模式的角度考虑模块,则在模块中声明的接口是
适配器 ,尽管具有很小的功能-它们的实现位于动态库中。
此处详细介绍
了为所有方法设置安全呼叫调用类型的需求。 使用
WideString
代替“本地”字符串的义务也不会得到证实,因为与DLL交换动态数据结构的主题不在本文的讨论范围之内。
InternetTools.Realization模块
就重要性和数量而言,第一个-正是他所包含的内容,如标题所示,他将包含上一个接口的实现:对于这两个接口,唯一的
TXQValue
类被
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
方法-
SafeCallException
阻止它是至关重要的(没有它,
TXQValue
性能
TXQValue
不会受到影响),但是,此处提供的代码允许将异常文本传递给在Delphi端发生的安全调用方法(详细信息,再次,可以在最近的
文章中找到)。
除其他所有功能外,该解决方案也是线程安全的-前提是,例如通过
OpenURL
接收到的
IXQValue
不会在线程之间传输。 这是由于该接口的实现仅将调用重定向到已经是线程安全的InternetTools。
InternetTools动态库
由于在上述模块中完成了工作,因此DLL导出单个函数就足够了(与使用过程样式的
选项进行比较):
library InternetTools; uses InternetTools.Types, InternetTools.Realization; function GetXQValue: IXQValue; stdcall; begin Result := TXQValue.Create; end; exports GetXQValue; begin SetMultiByteConversionCodePage(CP_UTF8); end.
对
SetMultiByteConversionCodePage
过程的调用旨在正确使用Unicode字符串。
InternetTools使用申请
如果现在我们根据所提出的接口来形式化
初始示例的Delphi解决方案,那么它与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异常的文本将变成一个匿名的“ safecall方法中的异常”,并带有DLL中生成的原始值。
一个稍微
复杂的示例在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.
库的其余功能
如果您查看InternetTools的
IXQValue接口的全部功能,您将看到
InternetTools.Types
的
相应接口从整个丰富集中仅定义了2个方法(
Map
和
ToString
)。 以完全相同的方式和简单的方式添加读者认为在其特定情况下必要的其余部分:将必要的方法写在
InternetTools.Types
,然后在代码的
InternetTools.Realization
模块中构建它们(通常是一行)。
如果您需要使用稍微不同的功能(例如cookie管理),则步骤顺序非常相似:
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; ...
- 之后,将新的导出函数返回到返回此接口的DLL:
... function GetCookies: ICookies; stdcall; begin Result := TCookies.Create; end; exports ..., GetCookies; ...
释放资源
尽管InternetTools库基于自动控制生命周期的接口,但是似乎有一个不明显的细微差别似乎会导致内存泄漏-如果您运行下一个控制台应用程序(在Delphi上创建,那么在FPC的情况下不会发生任何变化),然后,每按一次Enter键,该进程消耗的内存就会增加:
... 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
方法中)-必须在完成其工作后
明确地执行此操作; 为此,
xquery
库模块提供了
freeThreadVars
过程,可以通过扩展DLL导出列表来从逻辑上确保从Delphi应用程序进行调用:
... 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
会导致一个事实,即以前接收的所有接口都变得毫无意义,并且使用它们的任何尝试都是不可接受的。