一、XPath
全称:XML Path Language,即XML路径语言。
1、XPath概念
XPath提供了非常简洁明了的路径选择表达式。还提供了100个内键函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。
2、XPath常用规则
XPath常用规则
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选择子孙节点 |
. | 选取当前节点 |
… | 选取当前节点的父节点 |
@ | 选取属性 |
举例:
//title[@lang='eng']
选择所有名称为title,同时属性lang的值为eng的节点。
3、准备工作
下载lxml库
pip install lxml
4、实例引入
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类初始化,构建一个XPath解析对象
html = etree.HTML(text)
# 输出修正后的html代码
result = etree.tostring(html)
print(result.decode('utf-8'))
首先导入lxml库的etree模块,然后声明一个HTML文本,调用etree的HTML类进行初始化,这里就成功构建了一个XPath解析对象。
注意:声明的文本中最后一个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())
# 输出修正后的html代码
result = etree.tostring(html)
print(result.decode('utf-8'))
test.html文件中的代码就是上一个案例声明的代码。
这次输出的结果略有不同,多了一个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 0x234ab053648>, <Element body at 0x234ab053748>, <Element div at 0x234ab053788>, <Element ul at 0x234ab0537c8>, <Element li at 0x234ab053808>, <Element a at 0x234ab053888>, <Element li at 0x234ab0538c8>, <Element a at 0x234ab053908>, <Element li at 0x234ab053948>, <Element a at 0x234ab053848>, <Element li at 0x234ab053988>, <Element a at 0x234ab0539c8>, <Element li at 0x234ab053a08>, <Element a at 0x234ab053a48>]
这里使用*来匹配所有节点,也就是整个HTML文本中的所有节点都会被获取。可以看到,返回形式是一个列表,每个元素是Element类型,其后跟了节点的名称,如html、body、div、ul、li、a等。
匹配也可以指定节点名称。如果想要获取所有li节点:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li')
print(result)
这里选取所有li节点,可以使用//加上节点名即可。
[<Element li at 0x2e3ff7d0748>, <Element li at 0x2e3ff7d0788>, <Element li at 0x2e3ff7d07c8>, <Element li at 0x2e3ff7d0808>, <Element li at 0x2e3ff7d0848>]
提取结果是一个列表形式。其中每个元素都是一个Element对象。如果想要取出其中一个对象,可以直接用中括号加索引,例[0]
6、子节点
可以通过/或//查找子节点或子孙节点。
/ 是查找子节点
// 是查找子孙节点
选择 li 节点中的所有直接 a 子节点:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li/a')
print(result)
要获取 ul 节点下的所有子孙 a 节点:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//ul//a')
print(result)
如果这里用了//ul/a,就无法获取任何结果了。因为/用于获取直接子节点,而ul节点下没有a节点。
7、父节点
(1)、通过 …
通过 … 来查找父节点。
首先选中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)
'''
结果:
['item-1']
'''
(2)、通过 parent::
通过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)
'''
# 结果:
[<Element li at 0x165cc6b0748>, <Element li at 0x165cc6b0788>]
'''
我们通过加入[@class=“item-0”],限制了节点的class属性为item-0。
9、文本获取 (text())
用XPath中的text()方法获取节点中的文本
获取前面li节点中的文本:
from lxml import etree
html = etree.parse('./test.html', etree.HTMLParser())
result = html.xpath('//li[@class="item-0"]/text()')
print(result)
'''
结果:
['\r\n']
'''
没有获取到任何文本,只是获取到了回车符和换行符,这是为什么?
因为XPath中text()前面是/,而此处/的含义是选取直接子节点,很明显li的直接子节点都是a节点,文本都是在a节点内部的,所以这里匹配的结果就是被修正的li节点内部的回车符和换行符,因为自动修正的li节点的尾标签换行了。
因此,如想获取li节点内部文本,就有两种方式:一是先选取a节点在获取文本,二是使用//
第一种:先选取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']
'''
第二种:使用//
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']
'''
根据结果发现,返回的是3个结果。这里是选取子孙节点的文本,其中前两个是li的子节点中a节点内部的文本,最后一个是li节点内部的文本。
10、属性获取 (@)
用@就可以获取属性的值
获取所有li节点下a节点的href值: