XPath简介
XPath,全称为XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言。最初是用来搜寻XML文档的,但同样适用于HTML文档的搜索。更多详细信息可以参考:https://www.w3.org/TR/xpath 。
XPath常用规则
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点父节点 |
@ | 选取属性 |
示例如://title[@lang='eng']
,这就是个XPath规则,它代表选择所有名称为title,同时属性lang的值为eng的节点。
示例引入
首先要确定安装lxml库,安装可以参考Python库的安装。
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-2"><a href="link3.html">third item</a></li>
<li class="item-3"><a href="link4.html">fourth item</a></li>
<li class="item-4"><a href="link5.html">fifth item</a>
</ul>
</div>
'''
html = etree.HTML(text)
result = etree.tostring(html)
#这里的result是bytes类型,用decode转化为str类型
#print(type(result))
print(result.decode('utf-8'))
输出结果为:
<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-2"><a href="link3.html">third item</a></li>
<li class="item-3"><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
#test.html的内容就是上述例子中的HTML代码
html = etree.parse('test.html', etree.HTMLParser())
result = etree.tostring(html)
#这里的result是bytes类型,用decode转化为str类型
#print(type(result))
print(result.decode('utf-8'))
输出结果多了一个DOCTYPE的声明,对解析无任何影响。
所有节点
-
用
//
开头的XPath规则来选取所有符合要求的节点。用上述HTML为例:from lxml import etree #test.html的内容就是上述例子中的HTML代码 html = etree.parse('test.html', etree.HTMLParser()) #使用*代表匹配所有节点,也就是整个HTML文本中的所有节点都会被获取 result = html.xpath('//*') print(result)
结果:
[<Element html at 0x2ca3bcaf148>, <Element body at 0x2ca3bcaf0c8>, <Element div at 0x2ca3bcaf088>, <Element ul at 0x2ca3bcaf188>, <Element li at 0x2ca3bcaf1c8>, <Element a at 0x2ca3bcaf248>, <Element li at 0x2ca3bcaf288>, <Element a at 0x2ca3bcaf2c8>, <Element li at 0x2ca3bcaf308>, <Element a at 0x2ca3bcaf208>, <Element li at 0x2ca3bcaf348>, <Element a at 0x2ca3bcaf388>, <Element li at 0x2ca3bcaf3c8>, <Element a at 0x2ca3bcaf408>]
可见,返回形式是一个列表,每个元素是Element类型,其后跟了节点的名称,如html、body、div、ul、li、a等,所有节点都包含在列表中。
-
也可以匹配指定节点名称。如果想获取所有li节点,示例如下:
from lxml import etree #test.html的内容就是上述例子中的HTML代码 html = etree.parse('test.html', etree.HTMLParser()) result = html.xpath('//li') print(result) #结果为Element对象,列表形式,直接用中括号加索引 print(result[0])
选取所有li节点,可以使用//,然后直接加上节点名称即可,调用时直接使用xpath()方法即可,结果如下:
[<Element li at 0x23438b80088>, <Element li at 0x23438b80048>, <Element li at 0x23438b80148>, <Element li at 0x23438b80188>, <Element li at 0x23438b801c8>] <Element li at 0x23438b80088>
子节点
-
通过/或//可查找元素的子节点或子孙节点。如选择li节点的所有直接a子节点:
from lxml import etree html = etree.parse('test.html', etree.HTMLParser()) #通过追加/a即选择了所有li节点的所有直接a子节点 #//li用于选中所有li节点,/a用于选中li节点的所有直接子节点a,结合一起就是所有li节点的所有直接a子节点 result = html.xpath('//li/a') print(result)
-
用//获取所有子孙节点。要获取ul节点下的所有子孙节点:
from lxml import etree html = etree.parse('test.html', etree.HTMLParser()) result = html.xpath('//ul//a') print(result)
这个运行结果是一样的,这里使用//ul/a,就无法获取任何结果了。因为/用于获取直接子节点,而在ul节点下没有直接的子节点,只有li节点,所以无法获取任何匹配结果。
父节点
-
..
可以用来查找父节点。from lxml import etree html = etree.parse('test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link2.html"]/../@class') print(result)
运行结果如下:
['item-1']
-
还可以通过
parent::
来获取父节点from lxml import etree html = etree.parse('test.html', etree.HTMLParser()) result = html.xpath('//a[@href="link2.html"]/parent::*/@class') print(result)
属性匹配
@
符号进行属性过滤。比如选取class为item-0的li节点:
from lxml import etree
html = etree.parse('test.html', etree.HTMLParser())
#加入[@class="item-0"]限制节点的class属性为item-0
result = html.xpath('//li[@class="item-0"]')
print(result)
文本获取
-
text()方法选取节点的文本。如要获取上述例子中li节点内部的文本有两种方式,一种是先选取a节点再获取文本,另一种就是使用//。
-
方法1:先选取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']
这里是逐层选取的,先选取了li节点,又利用/选取了其直接子节点a,然后再选取其文本,得到的结果恰好是符合预期的。
-
方法2:使用//
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标签,XPath自动填补,但是是加在下一行上,故而有个换行符。
-
总而言之,如果想要获取子孙节点内部的所有文本,可以直接用//加text()的方式,可以保证获取到最全面的文本信息,但是可能会夹杂一些换行符等特殊字符。如果想要获取某些特定子孙节点下的所有文本,可以先选取到特定的子孙节点,然后再调用text()方法获取其内部文本,这样可以保证获取的结果是整洁的。
属性获取
@符号获取属性。如获取所有li节点下所有a节点的href属性:
from lxml import etree
html = etree.parse('test.html', etree.HTMLParser())
#注意与属性匹配的区别
result = html.xpath('//li/a/@href')
print(result)
运行结果:
['link1.html', 'link2.html', 'link3.html', 'link4.html', 'link5.html']
属性多值匹配
有时候,某些节点的某个属性可能有多个值,如:
from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
#html = etree.parse('test.html', etree.HTMLParser())
html = etree.HTML(text)
result = html.xpath('//li[@class="li"]/a/text()')
print(result)
这里的class属性有两个值li和li-first,此时用之前的属性匹配就无法操作了,匹配结果为空。这里要使用contains()函数,代码改下如下:
from lxml import etree
text = '''
<li class="li li-first"><a href="link.html">first item</a></li>
'''
#html = etree.parse('test.html', etree.HTMLParser())
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li")]/a/text()')
print(result)
运行结果如下:
['first item']
可见,通过contains()方法,第一个参数传入属性名称,第二个参数传入属性值,只要属性中包含传入的属性值,就可以完成匹配了。
多属性匹配
还有一种情况,就是根据多个属性确定一个节点,这时就需要同时匹配多个属性。此时可以用and运算符来连接,示例如下:
from lxml import etree
text = '''
<li class="li li-first" name="item"><a href="link.html">first item</a></li>
'''
#html = etree.parse('test.html', etree.HTMLParser())
html = etree.HTML(text)
result = html.xpath('//li[contains(@class,"li") and @name="item"]/a/text()')
print(result)
运算符及其介绍
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
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元素的节点集 |
+ | 加 | 6 + 4 | 10 |
- | 减 | 6 - 4 | 2 |
* | 乘 | 6*4 | 24 |
div | 除 | 8 div 4 | 2 |
= | 等于 | age=19 | 如果age是19则返回true。如果age是20,则返回false |
!= | 不等于 | age!=19 | 如果age是18则返回true。如果age是19,则返回false |
< | 小于 | age<19 | 如果age是18则返回true。如果age是19,则返回false |
<= | 小于或等于 | age<=19 | 如果age是19则返回true。如果age是20,则返回false |
> | 大于 | age>20 | 如果age是21则返回true。如果age是19,则返回false |
>= | 大于或等于 | age>=20 | 如果age是20则返回true。如果age是19,则返回false |
按序选择
在选择的时候某些属性可能同时匹配多个节点,但是只想要其中的某个节点,如第二个节点或者最后一个节点,该怎么办呢?这时可以利用中括号传入索引的方法获取特定次序的节点,实例如下:
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.parse('test.html', etree.HTMLParser())
html = etree.HTML(text)
#选取第一个li节点,[1]即可
result1 = html.xpath('//li[1]/a/text()')
print(result1)
#选取最后一个li节点,[last()]即可
result2 = html.xpath('//li[last()]/a/text()')
print(result2)
#选取位置小于3的li节点,即位置序号为1和2的节点,[position()<3]
result3 = html.xpath('//li[position()<3]/a/text()')
print(result3)
#选取倒数第三个li节点,last()-2。因为last()是最后一个,所以last()-2就是倒数第三个。
result4 = html.xpath('//li[last()-2]/a/text()')
print(result4)
运行结果如下:
['first item']
['fifth item']
['first item', 'second item']
['third item']
在xpath中提供了100多个函数,包括存取、数值、字符串、逻辑、节点、序列的等处理功能,具体作用可以参考:http://www.w3school.com.cn/xpath/xpath_functions.asp 。
节点轴选择
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.parse('test.html', etree.HTMLParser())
html = etree.HTML(text)
#调用ancestor轴,可以获取所有祖先节点,其后需要跟两个冒号,然后是节点选择器,使用*表示匹配所有节点
#返回的结果是第一个li节点的所有祖先节点,包括html、body、div和ul
result1 = html.xpath('//li[1]/ancestor::*')
print(result1)
#加入限制条件,在冒号后面加div,得到的结果就只有div这个祖先节点了。
result2 = html.xpath('//li[1]/ancestor::div')
print(result2)
#调用attribute轴,可以获取所有属性值,其后跟的选择器*代表获取节点的所有属性,返回值就是li节点的所有属性值
result3 = html.xpath('//li[1]/attribute::*')
print(result3)
#调用child轴,可以获取所有直接子节点,加入限定条件,选取href属性为link.html的a节点
result4 = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result4)
#调用descendant轴,可以获取所有子孙节点,加入span限定条件,返回结果只包含span节点
result5 = html.xpath('//li[1]/descendant::span')
print(result5)
#following轴,可以获取当前节点之后的所有节点
result6 = html.xpath('//li[1]/following::*[2]')
print(result6)
#following-sibling轴,可以获取当前节点之后的所有同级别节点,其后使用*匹配,获取后续所有同级节点
result7 = html.xpath('//li[1]/following-sibling::*')
print(result7)
运行结果如下:
[<Element html at 0x1410ace0148>, <Element body at 0x1410ace00c8>, <Element div at 0x1410ace0088>, <Element ul at 0x1410ace0188>]
[<Element div at 0x1410ace0088>]
['item-0']
[<Element a at 0x1410ace0248>]
[<Element span at 0x1410ace0288>]
[<Element a at 0x1410ace0208>]
[<Element li at 0x1410ace0308>, <Element li at 0x1410ace0348>, <Element li at 0x1410ace0388>, <Element li at 0x1410ace03c8>]