StupidBeauty
Read times:1587Posted at: - no title specified

O.Reilly教程翻译:10分钟XPath教程——使用Perl 进行自动化的系统管理,The 10-Minute XPath Tutorial - Automating System Administration with Perl

作者 David N. Blank-Edelman

在我们一头扎进XPath的世界之前,需要先接受3条警告。

首先 要想理解 这篇附录, 妳需要最少 对XML 这个主题有一定程度的认识。如果 妳不太了解的话,先读一读 附录 A,  8分钟的 XML教程

其次 XPath 是一门复杂的语言。 XPath 1.0规范 中包含着 34 页密密麻麻的文字; XPath 2.0规范 更是长达 118 页。 本附录不会试图 去评价XPath (尤其 v2.0)的丰富性、表达能力和复杂性。 我们只会专注于 XPath 中能够立即被Perl 程序猿用到的子集。

最后,本附录会专注于XPath 1.0。截止到本文的撰写时间为止,我还不知道有哪个稳定的Perl 模块能够支持XPath 2.0。

好了,现在让我们来研究一下,“XPath 是什么?,更重要的是,为什么我要关心这个?” XPath是一个W3C规范,一门用来定位XML 文档中的某些部分的语言。”如果妳曾经编写过从XML 文档中提取特定部分的代码,那么,XPath能够让妳的生活变得更加轻松。对于这个特定任务来说,它是一门简洁而又强大的语言。如果妳能够使用 XPath 语言来说明妳想要获取到什么数据(通常都能够做到),那么,XPath 解析器就能够为妳获取到那砣数据,或者让妳的程序指向XML 文档中的正确位置。这种事通常使用一行Perl 代码就能搞定。

XPath基本概念

在开始使用XPath 之前,需要理解几个基本概念。让我们先从简单的开始。

基本的位置路径

要理解XPath,妳首先应当明白,XML 文档可以被解析为一个树结构。文档 中的元素(当然 还有其它东西,但是我们暂时不研究那个 )会成为树中的节点。 为了说得清楚一点,我们 看一看 6 , 处理配置文件 的示例XML 文件。 其内容是这样的:

<?xml version="1.0" encoding="UTF-8"?>

<network>

<description name="Boston">

This is the configuration of our network in the Boston office.

</description>

<host name="agatha" type="server" os="linux">

<interface name="eth0" type="Ethernet">

<arec>agatha.example.edu</arec>

<cname>mail.example.edu</cname>

<addr>192.168.0.4</addr>

</interface>

<service>SMTP</service>

<service>POP3</service>

<service>IMAP4</service>

</host>

<host name="gil" type="server" os="linux">

<interface name="eth0" type="Ethernet">

<arec>gil.example.edu</arec>

<cname>www.example.edu</cname>

<addr>192.168.0.5</addr>

</interface>

<service>HTTP</service>

<service>HTTPS</service>

</host>

<host name="baron" type="server" os="linux">

<interface name="eth0" type="Ethernet">

<arec>baron.example.edu</arec>

<cname>dns.example.edu</cname>

<cname>ntp.example.edu</cname>

<cname>ldap.example.edu</cname>

<addr>192.168.0.6</addr>

</interface>

<service>DNS</service>

<service>NTP</service>

<service>LDAP</service>

<service>LDAPS</service>

</host>

<host name="mr-tock" type="server" os="openbsd">

<interface name="fxp0" type="Ethernet">

<arec>mr-tock.example.edu</arec>

<cname>fw.example.edu</cname>

<addr>192.168.0.1</addr>

</interface>

<service>firewall</service>

</host>

<host name="krosp" type="client" os="osx">

<interface name="en0" type="Ethernet">

<arec>krosp.example.edu</arec>

<addr>192.168.0.100</addr>

</interface>

<interface name="en1" type="AirPort">

<arec>krosp.wireless.example.edu</arec>

<addr>192.168.100.100</addr>

</interface>

</host>

<host name="zeetha" type="client" os="osx">

<interface name="en0" type="Ethernet">

<arec>zeetha.example.edu</arec>

<addr>192.168.0.101</addr>

</interface>

<interface name="en1" type="AirPort">

<arec>zeetha.wireless.example.edu</arec>

<addr>192.168.100.101</addr>

</interface>

</host>

</network>

如果 我们将这个文档解析为一个节点树的话,看起来将会是  B.1, “XML文档节点 这个样子。

树的根节点指向文档的根元素( <network></network> )。文档 中其它的元素都被挂接在根元素之下。每个元素节点 都会有相关联的属性节点(如果 它有任何属性的话 ),和一个子代文本节点,代表着 该元素的内容( 如果它有任何字符数据的话 )。例如,假设XML 的内容是 <element attrib="value">something</element> 则,XPath 解析完毕之后 ,会有一个 <element></element> 节点,该节点中包含一个名为 attrib 的属性节点,和一个文本节点,其中包含着字符串 something 请狠狠地盯着  B.1, “XML文档节点 看,直到 这个 XML文档节点 树概念被深深地铬印到脑海 中,因为 它是此文档中至关重要 的概念。

如果 妳看到这张图的时候联想到了  2 文件系统 那个树状图的话,那么,狠好。 我们就是故意让它们看起来相似的。 XPath使用 定位路径 的概念来导航到文档中某个节点或者某一组节点。定位路径 可能以树的根节点开始 (绝对路径),也可能以树中某个别的节点作为开始(相对路径)。 就像在一个文件系统中一样, “/”开 ,表示“ 从树的根节点开始, “.” ( 一个小数点 )表示当前节点( 也被称作 上下文节点 ”) “..” (两个小数 )表示 上下文节点的亲代节点。

如果 妳愿意的话,可以将定位路径看作 一种 用来指向 图中某个特点节点或者一组节点的方式。例如, 我们想要指向 <description> </description> 节点的话,则,定位路径就是 /network/description 。如果 我们用 /network/host 作为定位路径的话,则, 我们将会引用到 树中对应级别上的所有 <host></host> 节点。如果 妳还想要指向 树中更 深层次的某个节点 的话,则, 妳就需要区分出 不同的 <host></host> 节点了。至于如何 做到这一点,将是另一个不同的 XPath 话题; 我们暂且将那个问题放下, 先多看几个 对节点树进行遍历的导航示例。

 B.1. XML文档节点

我们的示例文件中不仅仅包含着标记标签; 这个文件中带有真实的数据。 这些元素本身通常 都具有属性(例如 <interface name="en1" type="AirPort"> ),或者充当数据标签 的角色(例如, <addr>192.168.0.4 </addr> )。 我们如何获取到文档中这一部分的信息呢? 要想获取某个元素的属性,则,在属性前面加上一个 @ 。例如,通过 /network/description/@name 能够获取 name="Boston" 要想获取某个元素的文本节点的内容,则, 在定位路径的末尾加上一个 text() ,例如 /network/description/text() 它将会返回 This is the configuration... .

XPath 中的通配符与文件系统中的通配符类似。 /network/host/*/arec/text() ,会定位到某个 <host></host> 节点下面所有具有 <arec> </arec> 子代节点 的元素节点,然后 ,返回那些 <arec></arec> 元素 的内容。 在这个例子中, 我们将获取到 与每个网卡相关联的DNS A 资源记录名字:

agatha.example.edu

gil.example.edu

baron.example.edu

mr-tock.example.edu

krosp.example.edu

krosp.wireless.example.edu

zeetha.example.edu

zeetha.wireless.example.edu

也可以对属性使用通配符,具体就是 @* /network/host/@* ,会返回那些 <host></host> 元素的所有属性。

在研究下一小节之前,还要说个语法点。 XPath 中还有一个特殊的定位路径操作符,我称之为“魔法”定位路径操作符。如果 妳在定位路径中的任何位置使用两个斜杠( // ),则, 它会从该位置开始,向树中查找 ,以定位到后续 的路径元素。例如,假如 我们写 //arec/text() ,则, 会像前面 个例子一样,获取到那些网卡的A 资源记录名字,因为 这个操作符会 从树的根节点开始向下搜索,找到所有具有文本节点 <arec> </arec> 元素。 妳也可以将双斜杠放置在定位路径的中间,例如 /network//service/text() 我们的示例文件中的节点树深度较小,但是, 请想像一下, 在描述路径的时候,不需要 写上树中所有的中间部分,这是个多么方便的功能。

判定

在上一小节中,我们优雅地抛出咯一个问题 当树中某个层级上有多个具有相同名字的元素的时候, 我们如何指定要跟进哪个分支或哪些分支? 在我们的示例文档中, 在树中的第三个层级,有5个 <host></host> 元素 。如果具有 不同的属性,并且其中的数据也不同,但是,如果定位路径中只包含元素名字的话,我们仍然无法区分它们。例如, 我们写 /network/host 的时候,其中 的单词 host (按照规范的说法)是起到“节点测试”的作用的。 当我们沿着定位路径向树的下部导航时,它能够 为我们选择使用哪个 或哪些 网络(network)分支。 但是,这个示例中的节点测试还不足以提供足够细的粒度,使得我们能够选择单个分支。

这就是XPath 的 判定 派上用场的时候了。使 用判定,可以对节点测试 所产生的节点集合进行过滤 以获取到 妳所关心的那些节点。 /network/host 会返回所有 的主机(host)节点; 我们需要采用一种手段来将这个集合缩小。判定条件 ,是在定位路径中加入方括号( [] )来表示的。 妳只需要将判定条件插入到需要进行过滤动作 的位置就可以了。

最简单的判定条件示例就是,数字下标,例如 /network/host[2]/interface/arec/text() 这个定位路径,会返回第二 个主机(host)节点(按照文档顺序 的第二个 )的网卡名字列表。假设 妳当前正位于 主机(host)节点处的话, 这个判定条件会告诉妳,应当选择 树中的哪个分支: 在这个例子中,就是位于第二个位置的那个分支。

警告

Perl程序猿应该对这种下标语法狠熟悉了,但是,请打起精神。与Perl 不同的是,XPath 中的下标是从1 开始数的,而不是0。

如果下标数字 是仅有能够使用的判定条件的话,那么,我们只能呵呵了。但是, 这就是XPath 的强大之处。 XPath 中带有一组强大的判定条件。 当我们将判定条件的复杂性再增加一点的话,将是这样的: /network/host[@name="agatha"] 这种写法,会检测是否存在一个具有特定值的特定属性,以此来选择正确的 <host></host>

当然 ,判定条件并不总是出现在定位路径的末尾。 妳可以使用它们来构造出一个狠长的定位字符串。例如, 我们想要找到网络中所有Linux 服务器的名字。 我们可以这样写: /network/host[@os="linux"]/service/../@name 这个定位路径,使用了一个判定条件来选择所有其 os 属性为 linux <host></host> 元素。然后 ,它跟进 到其中每个拥有 <service></service> 子代元素的节点中( 也就是说,只选择那些是服务器(server)的主机(host) )。 在这个时刻,我们已经深入到了树中的 <service></service> 节点了,所以 我们使用 ../@name 来获取到其亲代节点(包含 着我们刚刚找到的 <service></service> 元素的 <host></host> 元素 )的 name 属性。

我们可以这样测试节点的内容: //host/service[text()='DNS'] 这个定位路径的意思是, 从树的根节点开始,寻找那些 在其中嵌套有 <service></service> 节点的 <host></host> 节点。 一旦XPath 找到了一个符合这个条件的分支, 就会将每个这样的服务器(service)节点的内容与“DNS”作比较,找到相匹配的节点。

这个定位路径中,我们调用了 text() ,这是为了让解析器的工作更容易完成。如果 我们不写 text() ,只写一个“.” (小数点)的话(表示当前节点),则,XPath 会对 它的 内容进行比较。

相等 性比较,只是其中的一个比较操作符。 我们的示例数据中狠难展示出这一点,但是, [price > 31337] 这样的判定条件,也是可以用来选择节点的。

是不是感觉它有点像一门真正的计算机语言了? 当我们再来讲一讲函数的时候,妳会觉得它更像了。 XPath定义 了一大波函数,可用来处理节点集合 、字符串 、逻辑操作和数字。实际 上,我们已经见过其中的一些函数了,因为, /network/host[2]/interface/arec/text() 实际 上表示的是 /network/host[position()=2]/interface/arec/text()

为了让妳再感受一下这个东西,我们再举个定位路径的例子,它会选择那些HTTP 和 HTTPS 服务节点(允许 在服务名字的首尾有任意的空白字符 ): //host/service[starts-with(normalize-space(.),'HTTP')] 这里的字符串函数 starts-with() 其作用就跟妳所想像的一样:如果参与比较 的那个东西(当前节点 的内容 )就是以第二个参数中提供的字符串开头的,那么,它返回真(true)。XPath 规范 中有一个函数列表,只是对于初学 者不那么友好。 请在网络上搜索“XPath判定条件”,妳将找到 一些更适合阅读的内容。

缩写及方向轴

这篇附录,从 XPath 中最简单的核心功能开始, 在后续小节中逐步加入更多复杂性和细节。 让我们再最后说点复杂的东西,回头看一下之前说的定位路径。 我们会发现,至今 为止,我们所写的那些定位路径 以规范的说法来看,都是“缩写语法”。 未缩写的语法形式呢, 妳可能永远不会用上,但是, 一旦妳真的需要的话,妳就得用上了。 我们在这里快速地看一下这个东西,这样,让妳知道,一旦妳遇到相应的问题了,妳还是可以用这个东西来解决的。

那么 ,我们到现在所写的那些定位路径里,都缩写了些什么? 当我们写 /network/host[2]/service[1]/text() 的时候,实际上是这个意思:

  1. 1. 从树的根节点开始。

  2. 2. 向着根节点的子代方向移动(也就是说,沿着树向下移动),寻找那些元素名字 network 的子代节点。

  3. 3. 到达 <network></network> 节点 。现在,它成为上下文节点了。

  4. 4. 向着上下文节点的子代方向移动,寻找那些元素名字 host 的子代节点。

  5. 5. 到达 树中存在着多个 <host></host> 节点的层级。选择位于第二 个位置的那个节点。现在,它成为上下文节点了。

  6. 6. 向着上下文节点的子代方向移动,寻找那些元素名字 service 的子代节点。

  7. 7. 到达 树中存在着多个 <service></service> 节点的层级。选择位于第 个位置的那个节点。现在,它成为上下文节点了。

  8. 8. 向着与上下文节点相关联的文本节点移动。完毕。

如果我们以未缩写的语法来写这个定位路径的话,会是这样的(如果妳看到的是两行,那是因为被自动换行了,实际上是一整个长长的定位路径):

/child::network/child::host[position()=2]/child::service[position()=1]/child::text()

这个路径里加入的关键内容就是方向轴。对于定位路径 中的每一步,我们都可以包含一个方向轴, 以向解析器告知, 在树中以相对于上下文的什么方向来移动。 在这个例子中, 每一步,我们都是告诉 它向着 child:: 方向轴移动, 也就是说,向着上下文节点的子代方向移动。 我们已经习惯了文件系统 的路径, 它就是表示着从目录 向着子目录和目标文件移动,以至于, 当我们见到 /dir/sub-dir/file 这种语法时,想都不会多想。 这就是为什么缩写的XPath 语法 这么好用的原因。但是,XPath 并不限制我们一定要从亲代节点走向子代节点。 我们已经 // 语法中体验过这种自由了。 当我们这样写的时候, /network//cname ,实际 上表示的意思是 /child::network/descendant-or-self::cname 也就是说:

  1. 1. 从根节点开始。

  2. 2. 向子代节点方向移动 ,找到那些 <network></network> 节点。 每找到一个,它就会成为上下文节点。

  3. 3. 在上下文节点及后代节点中寻找,直到找到那些 <cname></cname> 节点。

还有其它3个方向轴,妳已经见过了: self::  ( . ) parent::  ( .. )和 attribute::  ( @ )。如果 妳使用未缩写的语法,那么,妳还能使用另外8个方向轴 ,颤抖吧 ancestor:: following-sibling:: preceding-sibling:: following:: preceding:: namespace:: descendant:: ancestor-or-self::

其中 following-sibling:: 可能是最有用的,所以,我来说一下这个,并且举个例子。 本附录的引用小节中,列出了一些其它的内容,它们 会对其它的方向轴做说明。 following-sibling:: ,这个方向轴,告知解析器,应当向着树中同一层级的下一个(或多个)元素移动。 也就是,引用到了上下文节点的邻居节点。如果 我们想要找到所有拥有 多个网卡的主机,那么,可以这样写(仍然 是长长的一行 ):

/child::network/child::host/child::interface/following-sibling::interface/parent::host/attribute::name

用人话来说,就是: 从网络(network)节点开始向下,直到找到 一个拥有某个网卡(interface)节点作为其子代节点的主机(host),然后 ,看看它是否在树中同一个层级拥有一个邻居网卡(interface)节点。如果 有的话,如果 有的话,则, 向上到达主机(host)节点,并且返回它的 name 属性。

进一步说明

如果妳觉得XPath 狠有趣,想要深入学习的话,那么,在本附录之外,有狠多其它的教程可供妳学习。请阅读下一小节中列出的规范和其它教程。学习其它的判定条件和方向轴。认识一下XPath 2.0,这样,当出现了能够使用该规范的Perl 模块时,妳立即就能用上。另外,多多把玩一下这门语言,直到妳觉得自己已经狠熟练了,就可以将它收到妳的工具箱中了。

如果 妳喜欢本文,请考虑购买 使用 Perl 进行自动 化的系统管理,第二版

http://www.kanunu8.com/book3/7781/170810.html

Your opinions

Your name:Email:Website url:Opinion content:
- no title specified

HxLauncher: Launch Android applications by voice commands