通过lxml库,可以利用XPath对HTML进行解析操作。
1.lxml概述:
lxml
库是基于libxml2
的XML
解析库的Python库,该模块使用C语言编写,使用它可以轻松处理XML和HTML文件,这个库的主要优点是易于使用,在解析大型文档时速度非常快,归档的也非常好,并且提供了简单的转换方法来将数据转换为Python数据类型,从而使文件操作更容易。lxml
库利用Xpath
语法可以快速地定位特定的元素或节点
使用pip install lxml
命令安装。
lxml库中大部分功能都位于lxml.etree模块中,导入lxml.etree模块的常见方式如下:
from lxml import etree
2.XPath概述:
XPath(XML Path Language)即XML路径语言,最初用于搜索XML文档,但是同样适用于HTML文档。XPath提供了非常简洁明了的路径选择表达式。另外,还提供了100多个内建函数,用于字符串、数值、时间的匹配以及节点、序列的处理等。
XPath常用规则有以下几种:
表达式 | 说明 |
---|---|
nodename | 选择此节点的所有子节点 |
/ | 从当前节点选取直接子节点 |
// | 从当前节点选取子孙节点 |
. | 选取当前节点 |
.. | 选取当前节点的父节点 |
@ | 选取属性 |
* | 通配符,选择所有元素节点与元素名 |
@* | 选取所有属性 |
[@attrib] | 选取具有给定属性的所有元素 |
[@attrib='value'] | 选取给定属性具有给定值的所有元素 |
[tag] | 选取所有具有指定元素的直接子节点 |
[tag='text'] | 选取所有具有指定元素并且文本内容是text节点 |
例如,匹配规则//title[@lang='eng']
表示选择所以名称为title,同时属性lang的值为eng的所有子孙节点。
3.lxml使用:
以如下html为例(缺少body、html的闭合标签):
html_text = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<div class="story">
<ul class="list_elements ul_first" id="list_eng" name="elements">
<li class="list_1 list_active">
<a href='link1.html'><span>AAA</span></a>
</li>
<li class="list_2 list_two"><a href='link2.html'>BBB</a></li>
<li class="list_3"><a href='link3.html'>CCC</a></li>
<li class="list_4"><a href='link4.html'>DDD</a></li>
</ul>
</div>
3.1 读取并解析HTML
在解析HTML代码的时候,如果HTML代码不规范或者不完整,lxml解析器会自动修复或补全代码,从而提高效率。
from lxml import etree
html = etree.HTML(html_text)
# 调用tostring()方法即可输出修正后的html,但是属于bytes类型,调用decode('utf-8')方法将其转化为str类型
result = etree.tostring(html).decode('utf-8')
print(result)
运行结果如下:
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<div class="story">
<ul class="list_elements ul_first" id="list_eng" name="elements">
<li class="list_1 list_active">
<a href="link1.html"><span>AAA</span></a>
</li>
<li class="list_2 list_two"><a href="link2.html">BBB</a></li>
<li class="list_3"><a href="link3.html">CCC</a></li>
<li class="list_4"><a href="link4.html">DDD</a></li>
</ul>
</div>
</body>
</html>
另外,也可以直接读取文本文件进行解析。
from lxml import etree
# 读取文本文件,当标签有错误时,需要使用 etree.HTMLParser()
html = etree.parse('html_text.txt' , etree.HTMLParser())
# 将html内容序列化
result = etree.tostring(html).decode('utf-8')
print(result)
3.2 选取所有符合要求的节点
使用以//
开头的XPath规则,选择所有符合要求的节点。
使用//
加节点名称,获取指定的所有节点。
from lxml import etree
html = etree.HTML(html_text)
# 选取HTML中的所有节点
result_1 = html.xpath('//*')
print(result_1)
# 选取所有li节点
result_2 = html.xpath('//li')
print(result_2)
运行结果如下:
[<Element html at 0x17a7d46f848>, <Element head at 0x17a7d46f808>, <Element title at 0x17a7d46f788>, <Element body at 0x17a7d46f888>, <Element div at 0x17a7d46f8c8>, <Element ul at 0x17a7d46f948>, <Element li at 0x17a7d46f988>, <Element a at 0x17a7d46f9c8>, <Element span at 0x17a7d46fa08>, <Element li at 0x17a7d46f908>, <Element a at 0x17a7d46fa48>, <Element li at 0x17a7d46fa88>, <Element a at 0x17a7d46fac8>, <Element li at 0x17a7d46fb08>, <Element a at 0x17a7d46fb48>]
[<Element li at 0x17a7d46f988>, <Element li at 0x17a7d46f908>, <Element li at 0x17a7d46fa88>, <Element li at 0x17a7d46fb08>]
3.2 选取所有符合要求的子节点或子孙节点
使用/
或//
规则,选择所有符合要求的子节点或子孙节点。
from lxml import etree
html = etree.HTML(html_text)
# 所有li节点的所有直接子节点a
result_1 = html.xpath('//li/a')
print(result_1)
print('----------')
# 所有ul节点的所有直接子节点a
result_2 = html.xpath('//ul/a')
print(result_2)
print('----------')
# 所有ul节点的所有子孙节点a
result_3 = html.xpath('//ul//a')
print(result_3)
运行结果如下:
[<Element a at 0x1854f2df8c8>, <Element a at 0x1854f2df848>, <Element a at 0x1854f2df948>, <Element a at 0x1854f2df988>]
----------
[]
----------
[<Element a at 0x1854f2df8c8>, <Element a at 0x1854f2df848>, <Element a at 0x1854f2df948>, <Element a at 0x1854f2df988>]
3.3 选取所有符合要求的父节点
使用..
或parent::
规则,选择所有符合要求的父节点。
from lxml import etree
html = etree.HTML(html_text)
# 选取href属性为"link1.html"的a节点的父节点
result_1 = html.xpath('//a[@href="link1.html"]/..')
print(result_1)
print('----------')
# 选取href属性为"link1.html"的a节点的父节点
result_2 = html.xpath('//a[@href="link1.html"]/parent::*')
print(result_2)
print('----------')
# 选取href属性为"link1.html"的a节点的父节点的clas属性
result_3 = html.xpath('//a[@href="link1.html"]/../@class')
print(result_3)
运行结果如下:
[<Element li at 0x1f37fc5f848>]
----------
[<Element li at 0x1f37fc5f848>]
----------
['list_1 list_active']
3.4 属性匹配
使用@
实现节点属性匹配。
from lxml import etree
html = etree.HTML(html_text)
# 获取class属性为"list_4"的li节点
result = html.xpath('//li[@class="list_4"]')
print(result)
运行结果如下:
[<Element li at 0x1c763631788>]
3.5 属性多值匹配
当某个节点的某个属性有多个值时,上面属性匹配方法就无法使用。
此时,需要使用contains()
方法。
from lxml import etree
html = etree.HTML(html_text)
# 属性匹配方法无法使用
result_1 = html.xpath('//li[@class="list_2"]')
print(result_1)
# 使用contains()方法
result_2 = html.xpath('//li[contains(@class , "list_2")]')
print(result_2)
运行结果如下:
[]
[<Element li at 0x268fc66f848>]
3.6 多属性匹配
有时需要根据多个属性来选取节点,即需要同时匹配多个属性,此时,就需要使用到运算符。
from lxml import etree
html = etree.HTML(html_text)
# 获取class属性包含"ul_first",并且name属性为"elements"的ul节点
result_1 = html.xpath('//ul[contains(@class , "ul_first") and @name="elements"]')
print(result_1)
print('----------')
# 获取class属性包含"list_1",或者包含"list_2"的li节点
result_2 = html.xpath('//li[contains(@class , "list_1") or contains(@class , "list_2")]')
print(result_2)
运行结果如下:
[<Element ul at 0x1e3797bf888>]
----------
[<Element li at 0x1e3797bf808>, <Element li at 0x1e3797bf908>]
可用在 XPath 表达式中的运算符有以下几种(来源W3school):
运算符 | 描述 | 实例 | 返回值 |
---|---|---|---|
| | 计算两个节点集 | //book | //cd | 返回所有拥有 book 和 cd 元素的节点集 |
+ | 加法 | 6 + 4 | 10 |
- | 减法 | 6 – 4 | 2 |
* | 乘法 | 6 * 4 | 24 |
div | 除法 | 8 div 4 | 2 |
= | 等于 | price=9.80 | 如果 price 是 9.80,则返回 true。如果 price 是 9.90,则返回 false。 |
!= | 不等于 | price!=9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。 |
< | 小于 | price<9.80 | 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。 |
<= | 小于或等于 | price<=9.80 | 如果 price 是 9.00,则返回 true。如果 price 是 9.90,则返回 false。 |
> | 大于 | price>9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.80,则返回 false。 |
>= | 大于或等于 | price>=9.80 | 如果 price 是 9.90,则返回 true。如果 price 是 9.70,则返回 false。 |
or | 或 | price=9.80 or price=9.70 | 如果 price 是 9.80,则返回 true。如果 price 是 9.50,则返回 false。 |
and | 与 | price>9.00 and price<9.90 | 如果 price 是 9.80,则返回 true。如果 price 是 8.50,则返回 false。 |
mod | 计算除法的余数 | 5 mod 2 | 1 |
3.7 获取文本
使用XPath中的text()
方法,获取节点中的文本。
from lxml import etree
html = etree.HTML(html_text)
# text()方法前为/,表示获取li节点的直接子节点a,而文本在a节点内,因此无法成功获取
result_1 = html.xpath('//li[@class="list_3"]/text()')
print(result_1)
print('----------')
# 先选取li节点的直接子节点a,然后再获取文本
result_2 = html.xpath('//li[@class="list_3"]/a/text()')
print(result_2)
print('----------')
# 使用//获取li节点内部的文本
result_3 = html.xpath('//li[@class="list_3"]//text()')
print(result_3)
运行结果如下:
[]
----------
['CCC']
----------
['CCC']
3.8 获取属性值
使用@
获取节点属性。
from lxml import etree
html = etree.HTML(html_text)
# 获取所有li节点的class属性值
result_1 = html.xpath('//li/@class')
print(result_1)
print('----------')
# 获取所有li节点的直接子节点a的href属性值
result_2 = html.xpath('//li/a/@href')
print(result_2)
运行结果如下:
['list_1 list_active', 'list_2 list_two', 'list_3', 'list_4']
----------
['link1.html', 'link2.html', 'link3.html', 'link4.html']
3.9 按序选择
在使用某些属性选取节点时,可能同时匹配到多个节点,此时,可以通过索引的方式来获取特定次序的节点。
from lxml import etree
html = etree.HTML(html_text)
# 选择第一个li节点,需要注意的是,序号是从1开始
result_1 = html.xpath('//li[1]/a/span/text()')
print(result_1)
print('----------')
# 选择最后一个li节点
result_2 = html.xpath('//li[last()]/a/text()')
print(result_2)
print('----------')
# 选择位置小于4的li节点,即第1、2、3三个节点
result_3 = html.xpath('//li[position()<4]/a//text()')
print(result_3)
print('----------')
# 选择倒数第二个节点
result_4 = html.xpath('//li[last()-1]/a/text()')
print(result_4)
运行结果如下:
['AAA']
----------
['DDD']
----------
['AAA', 'BBB', 'CCC']
----------
['CCC']
3.10 节点轴选择
- 祖先节点
调用ancestor
轴,获取祖先节点。
from lxml import etree
html = etree.HTML(html_text)
# 获取第一个li节点的所有祖先节点
result_1 = html.xpath('//li[1]/ancestor::*')
print(result_1)
print('----------')
# 获取第一个li节点的所有祖先节点中的ul节点
result_2 = html.xpath('//li[1]/ancestor::ul')
print(result_2)
运行结果如下:
[<Element html at 0x1d8fa34f8c8>, <Element body at 0x1d8fa34f888>, <Element div at 0x1d8fa34f808>, <Element ul at 0x1d8fa34f908>]
----------
[<Element ul at 0x1d8fa34f908>]
- 子孙节点
调用child
轴,获取所有直接子节点。
from lxml import etree
html = etree.HTML(html_text)
# 获取第一个li节点的直接子节点
result_1 = html.xpath('//li[1]/child::*')
print(result_1)
print('----------')
# 获取第一个li节点的子节点中href属性值为"link1.html"的a节点
result_2 = html.xpath('//li[1]/child::a[@href="link1.html"]')
print(result_2)
运行结果如下:
[<Element a at 0x2856c53f8c8>]
----------
[<Element a at 0x2856c53f8c8>]
调用descendant
轴,获取所有子孙节点。
from lxml import etree
html = etree.HTML(html_text)
# 子孙节点
result_5 = html.xpath('//li[1]/descendant::*')
print(result_5)
运行结果如下:
[<Element a at 0x1c06b4117c8>, <Element span at 0x1c06b411748>]
- 当前节点之后的所有节点
from lxml import etree
html = etree.HTML(html_text)
# 第一个li节点之后的所有节点
result = html.xpath('//li[1]/following::*')
print(result)
运行结果如下:
[<Element li at 0x1d7fa531788>, <Element a at 0x1d7fa531708>, <Element li at 0x1d7fa531808>, <Element a at 0x1d7fa531848>, <Element li at 0x1d7fa531888>, <Element a at 0x1d7fa531908>]
- 当前节点之后的所有同级节点
from lxml import etree
html = etree.HTML(html_text)
# 第一个li节点之后的所有同级节点
result = html.xpath('//li[1]/following-sibling::*')
print(result)
运行结果如下:
[<Element li at 0x219f9f71748>, <Element li at 0x219f9f71848>, <Element li at 0x219f9f71888>]
- 获取节点的所有属性值
from lxml import etree
html = etree.HTML(html_text)
# 获取ul节点的所有属性值
result = html.xpath('//ul/attribute::*')
print(result)
运行结果如下:
['list_elements ul_first', 'list_eng', 'elements']