Xpath
Reference: Xpath的使用(详细教学)
1. Xpath的概览 XPath的选择功能十分强大,他提供了非常简洁明了的路径选择表达式。另外,它还提供了100多个内建函数, 用于字符串,数值,时间的匹配以及节点,序列的处理。几乎所有我们想要定位的节点。都可以用XPath选择。
2. XPath常用规则
表达式
描述
nodename
选取此节点的所有子节点
/
从当前节点选取直接子节点
//
从当前节点选取子孙节点
.
选取当前节点
. .
选取当前节点的父节点
@
选取属性
这里列出了XPath的一个常用匹配规则,如下: //title[@lang='eng'] 他代表选择所有名称为title,同时属性lang的值为eng的节点。 后面会通过python的lxml库,利用XPath对HTML进行解析
3. 准备工作 使用lxml库之前,首先要确保其已经安装好。 可以用使用pip3来安装 pip3 install lxml
4. 实例引用 下面通过实例感受一下使用Xpath对网页进行解析的过程,相关代码如下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = etree.tostring(html) print(result.decode('utf-8'))
这里首先导入lxml库的etree模块,然后声明了一段HTML文本,接着调用HTML类进行初始化,这样就构造了一个XPath解析对象,此处需要注意一点,HTML文本中的最后一个li节点是没有闭合的,而etree模块可以自动修正HTML文本。 之后调用tostring方法即可输出修正后的HTML代码,但是结果是bytes类。于是利用decode方法将其转换成str类型,结果如下:
<html><body><div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li></ul> </div> </body></html>
可以看到,经过处理之后li节点标签得以补全,并且自动添加了body、html节点。 另外,也可以不声明,直接读取文本进行解析 ,示例如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = etree.tostring(html) print(result.decode('utf-8'))
期中test.html的内容就是上面列子中的HTML代码,内容如下:
<div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div>
这次输出的结果略有不同,多了一个DOCTYPE声明,不过对解析无任何影响,结果如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"> <html><body><div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li></ul> </div></body></html>
5. 所有节点 我们一般会用以//开头的XPath规则,来选取所有符合要求的节点。这里还是以第一个实例中的HTML文本为例,选取其中所有节点,实现代码如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//*') print(result)
运行结果如下:
[<Element html at 0x1a5aa580b48>, <Element body at 0x1a5aa580c48>, <Element div at 0x1a5aa580c88>, <Element ul at 0x1a5aa580cc8>, <Element li at 0x1a5aa580d08>, <Element a at 0x1a5aa580d88>, <Element li at 0x1a5aa580dc8>, <Element a at 0x1a5aa580e08>, <Element li at 0x1a5aa580e48>, <Element a at 0x1a5aa580d48>, <Element li at 0x1a5aa580e88>, <Element a at 0x1a5aa580ec8>, <Element li at 0x1a5aa580f08>, <Element a at 0x1a5aa580f48>]
这里使用*代表匹配所有节点,也就是获取整个HTML文本中的所有节点。从运行结果可以看到返回形式是一个列表,其中每个元素是Element类型,类型后面跟这节点的名称,如html、body、div等所有节点都包含在列表中。 当然,此处也可以匹配指定节点名称。列如想获取li节点,示例如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li') print(result) print(result[0])
这里选取所有li节点,可以使用//,然后直接加上节点名称,调用时使用xpath方法即可。 运行结果如下:
[<Element li at 0x1c66d060c48>, <Element li at 0x1c66d060c88>, <Element li at 0x1c66d060cc8>, <Element li at 0x1c66d060d08>, <Element li at 0x1c66d060d48>] <Element li at 0x1c66d060c48>
可以看到,提取结果也是一个列表,其中每个元素都是Element类型。要是想取出一个对象可以直接用中括号加索引获取,如[0]。
6. 子节点 通过/或//即可查找元素的子节点或子孙节点。假如现在想选择li节点的所有直接子节点a,则可以这样实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li/a') print(result)
这里通过追加/a的方式,选择了所有li节点的所有直接子节点a。其中//li用于选中所有li节点,/a用于选中li节点的所有直接子节点啊。 运行结果如下:
[<Element a at 0x2592a090c48>, <Element a at 0x2592a090c88>, <Element a at 0x2592a090cc8>, <Element a at 0x2592a090d08>, <Element a at 0x2592a090d48>]
上面的/用于选取节点的直接子节点,如果要获取节点的所有子孙节点,可以使用//。例如要获取ul节点下面的所有子孙节点a,可以这样实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul//a') print(result)
运行结果是相同的。 但是如果这里用//ul/a,就无法获取结果了,因为/用于获取直接的子节点,而ul节点下没有直接的a子节点,只有li节点,所以无法获取任何匹配结果如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//ul/a') print(result)
运行结果如下:
因此这里要注意/和//的区别,前者用于获取直接子节点,后者用于获取子孙节点
7. 父节点 如何查找父节点呢? 示例如下: 首先选中href属性为link4.html的a节点,然后获取其父节点,在获取父节点的class属性,相关代码如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link4.html"]/../@class') print(result)
运行结果如下:
可以通过观察这正是我们获取a标签属性为href为link4.html的父节点li节点的class属性 我们也可以用parent::获取父节点 ,代码如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link4.html"]/parent::*/@class') print(result)
结果相同都是[‘item-1’]
8. 属性匹配 在选取节点的时候,还可以使用@符号实现属性过滤。例如,要选取class属性为item-0的li接地但,可以这样实现:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]') print(result)
这里通过加入[@class=‘item-0’],限制了节点的class属性为item-0.HTML文本中符合这个条件的li节点有两个,所以结果因该返回两个元素。 结果如下:
[<Element li at 0x23e52010c48>, <Element li at 0x23e52010c88>]
可见,匹配结果因该返回两个元素,后面在验证
9. 文本获取 用Xpath中的text方法可以获取节点中的文本,接下来尝试获取前面li节点中的文本,相关代码如下:
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/text()') print(result)
运行结果为:
奇怪的是,我们没有获取任何文本,只获取了一个换行符,这是为什么呢?因为xpath中text方法的前面是/,而/的含义就是直接选取子节点,很明显li的直接子节点都是a节点,文本都是在a节点内部的,所以这里匹配到的结果就是被修正的li节点内部的换行符,因为自动修正的li节点的尾标签换行了。 及选中的是这两个节点:
<li class="item-0"><a href="link1.html">first item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </li>
其中一个节点因为自动修正,li节点的尾标签在添加的时候换行了,所以提取文本得到的唯一结果就是li节点的尾标签和a节点的尾标签之前的换行符。
因此,如果想获取li节点内部的文本,就有两种方法,一种是先选取a节点在获取文本,另一种是使用//。接下来我们看两种的区别。
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]/a/text()') print(result)
运行结果如下:
['first item', 'fifth item']
可以看到,这里返回两个值,内容都是class属性为item-0的li节点的文本,这也印证了前面属性匹配的结果是正确的。 这种方式下,我们是逐层选取的,先选取li节点,然后利用/选取其直接子节点a,在选取a的文本,得到的两个结果恰好符合我们的预期。
再看一下啊使用//能够获取什么结果
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li[@class="item-0"]//text()') print(result)
运行结果如如下:
['first item', 'fifth item', '\r\n ']
不出所料,这里的返回结果是三个。可想而知这里选取的是所有子孙节点的文本,其中前两个是li的子节点a内部的文本,另外一个是最后一个li节点内部的文本,及换行符
如果需要选取子孙节点内部的所有文本,直接使用//text,这样可以获得全部信息,但是也会有一些换行符等特殊字符,如果获取特定子孙节点的所有文本,则先逐层选择在用text方法
10. 属性获取 获取li节点下面的所有a节点的href
from lxml import etree html = etree.parse('./test.html', etree.HTMLParser()) result = html.xpath('//li/a/@href') print(result)
这里通过@href获取节点的href属性,注意此处和属性匹配的方法不同,属性区别是用中括号加属性名和值来限定某个属性,如[@href=“link1.html”],此处的@href是指获取节点的某个属性,二者需要做好区分。 运行结果如下:
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
可以看到,我们成功获取了所有li节点下a节点的href属性,并以列表形式返回了他们。
11. 属性多值匹配 有时候,某些节点的属性可能有多个值,例如:
from lxml import etree text = ''' <li class="li li-first"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[@class="li"]/a/text()') print(result)
这里HTML文本中li节点的class属性就有两个值:li和 li-first。此时如果还用之前的属性匹配获取节点,就无法获取到了 运行结果如下:
这种情况需要用到contains方法(和jquery中的contains方法一致)
from lxml import etree text = ''' <li class="li li-first"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[contains(@class,"li")]/a/text()') print(result)
上面使用了contains方法,给其第一个参数传入属性名称,第二个参数传入属性值,只要传入的属性包含传入的属性值,就可以完成匹配了。 此时运行结果如下:
contains方法在某个节点的某个属性有多个值时用到
12. 多属性匹配 我们还可能遇到一种情况,就是更具多个属性确定一个节点,这是需要同事匹配多个属性。运算符and用于连接多个属性,实例如下:
from lxml import etree text = ''' <li class="li li-first" name="item"><a href="link.html">first item</a></li> ''' html = etree.HTML(text) result = html.xpath('//li[contains(@class, "li") and @name="item"]/a/text()') print(result)
这里的li节点又增加了一个name属性,因此要确定li节点,需要同时考察class和name属性,一个条件是class属性里面包含了li字符串 ,另一个条件是name属性为item字符串,这二者同时得到满足,才是li节点。class和name属性需要用到and运算符相连。 结果如下:
以下列举了其他运算符号
运算符
描述
实例
返回值
or
或
age=19 or age=20
如果age是19则返回True。如果age是21则返回false 两个条件满足一个即可
and
与
age>19 and age<20
如果age是20则返回True。如果age是18则返回false 两个条件同时满足即可
mod
计算除法的余数
5 mod 2
1
计算两个节点集
//book
//cd
返回所有拥有book和cd元素的节点集
**加减乘除 + - * div(表示除法)
大于小于等比较运算符 不举例说明**
13. 按序选择 在选择节点时,某些属性可能同时匹配了多个节点,但我们只想要其中一个,如第二个或者最后一个 可以使用往中括号中传入索引的方法获取特定次序的节点如下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html">first item</a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = html.xpath('//li[1]/a/text()') print(result) result = html.xpath('//li[last()]/a/text()') print(result) result = html.xpath('//li[position()<3]/a/text()') print(result) result = html.xpath('//li[last()-2]/a/text()') print(result)
第一个print 往中括号中传入索引[1],即可获取第一个数据,注意序号以1开头,而不是0 第二个last()方法表示最后一个 第三个position()❤️ 选取了位置小于3的li节点也就是第一个和第二个节点 第四次last()-2则表示倒数第三个 结果如下:
['first item'] ['fifth item'] ['first item', 'second item'] ['third item']
在这个实例中我们使用了last、position等方法。xpath提供了100多个方法,包括存取、数值、字符串、逻辑、节点、序列等处理功能
节点集函数 last() 返回当前上下文中的最后一个节点的位置号数。 position() 返回当前节点的位置的数字,位于第多少个。 count(node-set) 返回节点集node-set中的节点数。 id(mark) 根据在DTD中声明为ID类型的标识符选择元素,返回一个节点集。 name() 返回节点名称。 local-name() 返回不带名称空间的节点名称。 namespace-uri() 返回名称空间。 字符串函数 string(object) 把节点集、数字、布尔值等转化成字串并返回。 format-number(num) 把数字转化成字串并返回。 concat(string1,string2...) 合并多个字串并返回。 starts-with(string1,string2) 如果字串string1开头带string2的所有字符则返回true,否则返回false。 contains(string1,string2) 如果字串string1包含string2的所有字符则返回true,否则返回false。 substring(string,number1,number2) 取string中从位置number1开始,number2长的子串,number2可省略。 substring-before(string1,string2) 取string1在string2第一次出现位置之前的子串。 substring-after(string,string) 取string1在string2第一次出现位置之后的子串。 string-length(string) 返回string的长度数字。 normalize-space(string) 清除string头尾的空白字符并且把连续的空白字符替换为一个再返回。 translate(string1,string2,string3) 假如string1中的字符在string2中有出现,那么替换为string3对应string2的同一位置的字符,假如string3这个位置取不到字符则删除string1的该字符。 布尔函数 boolean(object) 非0和NaN的数字/非空节点/长度大于0的字串返回true,非基本类型的转换有时无法估计。 not(boolean) 对布尔值取反。 true() 返回true。 false() 返回false。 lang(string) 如果上下文节点的lang属性和string相同则返回true。 数字函数 number(object) 使对象转化成数字,布尔值true为1,false为0;节点集首先转换成字符串,字符串转换成数字或者NaN。 sum(node-set) 对节点集node-set中的所有节点应用number()函数后返回和。 floor(number) 返回不大于数字number的最大整数。 ceiling(number) 返回不小于数字number的最小整数。 round(number) 返回和数字number的四舍五入结果。
14. 节点轴选择 XPath提供了很多节点轴的选择方法,包括获取子元素,兄弟元素,父元素,祖先元素等,实例如下:
from lxml import etree text = ''' <div> <ul> <li class="item-0"><a href="link1.html"><span>first item</span></a></li> <li class="item-1"><a href="link2.html">second item</a></li> <li class="item-inactive"><a href="link3.html">third item</a></li> <li class="item-1"><a href="link4.html">fourth item</a></li> <li class="item-0"><a href="link5.html">fifth item</a> </ul> </div> ''' html = etree.HTML(text) result = html.xpath('//li[1]/ancestor::*') print(result) result = html.xpath('//li[1]/ancestor::div') print(result) result = html.xpath('//li[1]/attribute::*') print(result) result = html.xpath('//li[1]/child::a[@href="link1.html"]') print(result) result = html.xpath('//li[1]/descendant::span') print(result) result = html.xpath('//li[1]/following::*[2]') print(result) result = html.xpath('//li[1]/following-sibling::*') print(result)
运行结果如下:
[<Element html at 0x2458d4d6bc8>, <Element body at 0x2458d4d6b48>, <Element div at 0x2458d4d6b08>, <Element ul at 0x2458d4d6c08>] [<Element div at 0x2458d4d6b08>] ['item-0'] [<Element a at 0x2458d4d6c08>] [<Element span at 0x2458d4d6b08>] [<Element a at 0x2458d4d6c08>] [<Element li at 0x2458d4d6b48>, <Element li at 0x2458d4d6c48>, <Element li at 0x2458d4d6c88>, <Element li at 0x2458d4d6cc8>]
首先介绍关于xpath轴的方法
ancestor选取当前节点的所有先辈(父、祖父等)。 ancestor-or-self选取当前节点的所有先辈(父、祖父等)以及当前节点本身。 attribute选取当前节点的所有属性。 child选取当前节点的所有子元素。 descendant选取当前节点的所有后代元素(子、孙等)。 descendant-or-self选取当前节点的所有后代元素(子、孙等)以及当前节点本身。 following选取文档中当前节点的结束标签之后的所有节点。 following-sibling选取当前节点之后的所有兄弟节点 namespace选取当前节点的所有命名空间节点。 parent选取当前节点的父节点。 preceding选取文档中当前节点的开始标签之前的所有节点。 preceding-sibling选取当前节点之前的所有同级节点。 self选取当前节点。