Python爬虫技术系列-02HTML解析-xpath与lxml
- 2 XPath介绍与lxml库
- 2.1 XPath概述
- 2.2 lxml库介绍
- 2.2.1 lxml库安装
- 2.2.2 lxml库基本使用
- 2.2.3 lxml案例
- a.读取数据并补全
- b.读取数据并选取节点:
2 XPath介绍与lxml库
参考连接:
XPath教程
https://www.w3school.com.cn/xpath/index.asp
lxml文档
https://lxml.de/index.html#support-the-project
爬虫专栏
https://blog.csdn.net/m0_38139250/category_12001010.html
2.1 XPath概述
XPath的中文名称为XML路径语言(XML Path Language),其最初的设计是用来搜索 XML 文档,但也适用于HTML文档搜索。1996年11月,XPath 成为W3C标准, XQuery 和 XPointer 都构建于 XPath 表达之上。
XPath有着强大的搜索选择功能,提供了简洁的路径选择表达式, 提供了100+的内建函数,可以完成XML和HTML的绝大部分的定位搜索需求。
XML和HTML均可通过树形结构的DOM(文档对象模型,Document Object Model)表示,DOM中包含元素节点,文本节点,属性节点三种节点。
其中元素节点是DOM的基础,元素就是DOM中的标签,
如<html>是根元素,代表整个文档,其他的元素还包括<head>,<body>,<div>,<ul>,<span>等,元素节点之间可以相互包含。
文本节点:包含在元素节点中,
比如<span>文本节点</span>。
属性节点:元素节点可以包含一些属性,属性的作用是对元素做出更具体的描述,
如<span class="属性节点值">文本节点</span>。
XPath的核心思想就是写地址,通过地址查找到XML和HTML中的元素,文本,属性等信息。
获取元素n:
//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../标签n
获取文本:
//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../text()
获取属性n的值:
//标签[@属性1="属性值1"]/标签[@属性2="属性值2"]/.../@属性n
[@属性1=“属性值1”]是谓语,用于过滤相同的标签,如果不需要通过属性过滤标签,可以不加谓语过滤。
下面介绍XPath的节点类型和常用语法。
1)节点(Node): XPath包括元素、属性、文本、命名空间、处理指令、注释以及文档(根)等七种类型的节点。XML 文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。节点之间的关系包括父(Parent),子(Children),同胞(Sibling),先辈(Ancestor),后代(Descendant)。
2)语法:
XPath中,通过路径(Path)和步(Step)在XML文档中获取节点。
a.常用的路径表达式
常见的路径表达式如下表所示:
表 XPath表达式与示例
b.谓语(Predicates)
为查找特点节点或包含某个指定值的节点,可以使用谓语(Predicates),谓语用方括号[]表示,如:
//div[@class=‘useful’]
表示选取所有div 元素,且这些元素拥有值为 useful的 class属性。
//div[@class=‘useful’]//li[last()]
表示选取具有class值为useful的div标签下的任意li元素的最后一个li元素。
c.选取未知节点
XPath可以通过通配符搜索未知节点,如*表示匹配任何元素,@*表示匹配任何带有属性的节点,node()表示匹配任何类型的节点。如:
//title[@*]
表示选取所有带有属性的title元素。
d.选取若干路径
XPath可以通过“|”运算符表示选取若干路径。如
//title | //price
表示选取文档中的所有 title 和 price 元素
3)轴与步:
a.XPath轴(axis)
轴表示当前节点的节点集XPath轴的名称见表13-2所示:
表13-2 XPath轴名称与结果
b.步(Step)
步可以根据当前节点集中的节点来进行计算搜索。
步的语法:
轴名称::节点测试[谓语]
其中,轴(axis)表示所选节点与当前节点之间的关系,节点测试(node-test)表示是某给定轴内部的节点,谓语(predicate)用于搜索特定的节点集。
步的使用如表13-3所示:
步的使用案例如下:
//div[@class=“useless”]/descendant::a’)
获取任意class属性值为useless的div标签下得所有子孙a标签节点。
2.2 lxml库介绍
Web数据展示都通过HTML格式,如果采用正则表达式匹配lxml是Python中的第三方库,主要用于处理搜索XML和HTML格式数据。
2.2.1 lxml库安装
安装lxml:
pip install lxml==4.8.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
如果安装不成,可以在
https://www.lfd.uci.edu/~gohlke/pythonlibs/
下载对应的whl安装包,然后安装即可。
如果部分读者还是安装不成,可以把whl包解压,然后把解压后的两个文件夹放在python安装文件夹下的Lib\site-packages目录下即可。
2.2.2 lxml库基本使用
lxml的使用首先需要导入lxml的etree模块:
from lxml import etree
etree模块可以对HTML文件进行自动修正,lxml中的相关使用方法如下:
读取数据:
etree.HTML(text, parser=None, base_url=None,)
第一个参数text为一个字符串,字符串应该可以转换为HTML或XML文档,如果字符串中的标签存在不闭合等问题,本方法会自动修正,并把文本转换成为HTML格式文档。返回结果类型为’lxml.etree._Element’。
etree.fromstring(text, parser=None, base_url=None)
与etree.HTML()类似,但转换过程中,要求text字符串为标准的XML或HTML格式,否则会抛出异常。返回结果类型为’lxml.etree._Element’。
etree.parse(source, parser=None, base_url=None)
可如果没有解析器作为第二个参数提供,则使用默认解析器。返回一个加载了源元素的ElementTree对象,返回结果类型为’lxml.etree._ElementTree’。
etree.tostring(element_or_tree, encoding=None,)
输出修正后的HTML代码,返回结果为bytes类型。
搜索数据:
假定有变量html为etree模块读取数据后返回’lxml.etree._Element’或’lxml.etree._ElementTree’类型,可以调用:
html.xpath(self, _path, namespaces=None, extensions=None, smart_strings=True, **_variables)
_path为xpath中的路径表达式和步,xpath函数可以通过_path参数值实现对文档的搜索。
2.2.3 lxml案例
下面根据具体案例来介绍lxml的基本使用。
a.读取数据并补全
from lxml import etree
# 定义一个不规则的html文本
text = '''
<html><body><div><ul>
<li class="item-0">01 item</a></li>
'''
html = etree.HTML(text) # etree把不规则文本进行修正
complete_html = etree.tostring(html) # toString可输出修正后的HTML代码,返回结果为bytes
print("原数据------->", text)
print("修正后的数据--->\n",complete_html.decode('utf-8')) # 输出修正后的html
输出结果如下:
原数据------->
<html><body><div><ul>
<li class="item-0">01 item</a></li>
修正后的数据--->
<html><body><div><ul>
<li class="item-0">01 item</li>
</ul></div></body></html>
从输出结果可以看出,etree.toString()可以对缺少闭合标签的HTML文档进行自动修正。
etree模块可以调用HTML读取字符串,也可以调用parse()方法读取一个HTML格式的文件。把上面代码中的text变量保存在文本文件中,文件命名为lxml.html。
from lxml import etree
# 读取html文件
html = etree.parse("./lxml.html",etree.HTMLParser()) # etree把不规则文本进行修正
complete_html = etree.tostring(html) # toString可输出修正后的HTML代码,返回结果为bytes
print(complete_html.decode('utf-8'))
输出结果如下:
<!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">01 item</a></li></ul></div></body></html>
从输出结果看可以看出,etree完成了HTML自动修正,同时还加上了!DOCTYPE标签。
b.读取数据并选取节点:
创建Demo11-03.html文件,内容如下:
<!DOCTYPE html>
<html>
<body>
<div class="useful">
<ul>
<li class="cla-0" id="id-0"><a href="/link1">01</a></li>
<li class="cla-1"><a href="/link2">02</a></li>
<li><strong><a href="/link3">03</a></strong></li>
<li class="cla-1"><a href="/link4">04</a></li>
<li class="cla-0"><a href="/link5">05</a></li>
</ul>
</div>
<div class="useless">
<ul>
<li class="cla-0"><a href="/link1">useless-01</a></li>
<li class="cla-1"><a href="/link2">useless-02</a></li>
</ul>
</div>
</body>
</html>
导入库,并通过etree读取html文档:
from lxml import etree
# 加载HTML文档
html = etree.parse("./Demo11-03.html",etree.HTMLParser())
00.获取根路径的div元素:
print('--result00----/div-----')
result00 = html.xpath('/div') # 匹配/div节点
print(result00)
输出如下:
--result00----/div-----
[]
因为根路径下标签为,所以无法匹配度根路径下的div标签。
01.获取任意路径的div元素:
print('--result01----/div-----')
result00 = html.xpath('/div') # 匹配所有div节点
print(result01)
输出如下:
--result01----//div-----
[<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]
匹配到两个div元素。//表示任意路径。
02.获取任意路径div元素的所以子节点:
print('--result02----//div/*-----')
result02 = html.xpath('//div/*') # 匹配所有div节点的子节点
print(result02)
输出如下:
--result02----//div/*-----
[<Element ul at 0x182e1169f80>, <Element ul at 0x182e1169fc0>]
*表示匹配任意节点。
03.匹配所有class属性的值:
print('--result03----//@class-----')
result03 = html.xpath('//@class') # 匹配所有class属性的值
print(result03)
输出如下:
--result03----//@class-----
['useful', 'cla-0', 'cla-1', 'cla-1', 'cla-0', 'useless', 'cla-0', 'cla-1']
@class表示获取属性class的值。
04.获取任意路径下li标签的a标签子节点:
print('--result04----//li/a-----')
result04 = html.xpath('//li/a') # 匹配所有li标签下的子节点a标签
print(result04)
输出如下:
--result04----//li/a-----
[<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]
原始数据中一共7个a标签,返回值为6个a标签,是因为如下原始数据
<li><strong><a href="/link3">03</a></strong></li>
a标签不是li标签的子节点。
05.获取任意路径下li标签的任意a标签子孙节点:
print('--result05----//li//a-----')
result05 = html.xpath('//li//a') # 匹配所有li标签下的所有a标签
print(result05)
输出如下:
--result05----//li//a-----
[<Element a at 0x182e116a400>, <Element a at 0x182e116a480>, <Element a at 0x182e116a600>, <Element a at 0x182e116a4c0>, <Element a at 0x182e116a500>, <Element a at 0x182e116a540>, <Element a at 0x182e116a5c0>]
原始数据中一共7个a标签,返回值为7个a标签,全部获取到。
06. 匹配具有herf属性为/link2的元素的父元素的class属性的值:
print('--result06----//a[@href="/link2"]/../@class-----')
result06 = html.xpath('//a[@href="/link2"]/../@class')print(result06)
输出如下:
--result06----//a[@href="/link2"]/../@class-----
['cla-1', 'cla-1']
…表示当前节点的父元素。
07.查找所有class="cla-0"的li节点:
print('--result07----//li[@class="cla-0"]-----')
result07 = html.xpath('//li[@class="cla-0"]') # 查找所有class="cla-0"的li节点:
print(result07)
输出如下:
--result07----//li[@class="cla-0"]-----
[<Element li at 0x182e116a140>, <Element li at 0x182e116a2c0>, <Element li at 0x182e116a3c0>]
//表示匹配任意路径,[]代表谓语,@class="cla-0"代表过滤出class属性值为cla-0的元素。
08.获取a节点下的文本:
print('--result08----//li[@class="cla-0"]/a/text()-----')
result08_1 = html.xpath('//li[@class="cla-0"]/a/text()') # 先选取a节点,再获取a节点下的文本
print(result08_1)
输出如下:
--result08----//li[@class="cla-0"]/a/text()-----
['01', '05', 'useless-01']
text()表示获取匹配节点的文本内容。
09.获取li节点下a节点的href属性:
print('--result09----//li/a/@href-----')
result09 = html.xpath('//li/a/@href') # 获取li节点下a节点的href属性
print(result09)
输出如下:
--result09----//li/a/@href-----
['/link1', '/link2', '/link4', '/link5', '/link1', '/link2']
//li/a/@href表示匹配任意路径下的li元素的a标签子节点的href属性值。
10.获取li节点下所有a节点的href属性:
print('--result10----//li//a/@href-----')
result10 = html.xpath('//li//a/@href') # 获取li节点下所有a节点的href属性
print(result10)
输出如下:
--result10----//li//a/@href-----
['/link1', '/link2', '/link3', '/link4', '/link5', '/link1', '/link2']
相比result9,本次结果匹配到了/link3。
11.获取class属性值包含-0的li元素下的a标签的文本:
print('--result11----//li[contains(@class,"-0")]/a/text()-----')
result11 = html.xpath('//li[contains(@class,"-0")]/a/text()') # 获取class属性值包含-0的li元素下的a标签的文本
print(result11)
输出如下:
--result11----//li[contains(@class,"-0")]/a/text()-----
['01', '05', 'useless-01']
contains(@class,“-0”)表示过滤条件为class属性包含-0。于此类似的还有starts-with,starts-with表示以什么开头。
12.用多个属性获取:
print('--result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----')
result12 = html.xpath('//li[contains(@class,"-0") and @id="id-0"]/a/text()') # 多个属性用and运算符来连接
print(result12)
输出如下:
--result12----//li[contains(@class,"-0") and @id="id-0"]/a/text()-----
['01']
contains(@class,“-0”) and @id="id-0"表示待匹配的元素需要具有满足以上两种条件。and 操作符也可以替换为or 操作符。由于同时包含两种属性条件的a标签只有一个,所以返回的文本只有01。
13.按照顺序获取节点:
print('--result13----//li[last()]/a/text()-----')
result13 = html.xpath('//li[last()]/a/text()') # 取最后一个li节点下a节点的文本
print(result13)
输出如下:
--result13----//li[last()]/a/text()-----
['05', 'useless-02']
返回结果表示,通过last()返回了两个li列表中的最后一个节点。
14.通过ancestor轴获取所有的祖先节点:
print('--result14----//li[1]/ancestor::*-----')
result14 = html.xpath('//li[1]/ancestor::*') # ancestor轴可以获取所有的祖先节点
print(result14)
输出如下:
--result14----//li[1]/ancestor::*-----
[<Element html at 0x182e0de9c80>, <Element body at 0x182e116abc0>, <Element div at 0x182e1169e80>, <Element ul at 0x182e1169f80>, <Element div at 0x182e1169e00>, <Element ul at 0x182e1169fc0>]
//li[1]表示获取任意路径的li中的第一个元素,/ancestor::*表示获取当前节点的任意祖先节点。
15.通过ancestor轴获取祖先div节点:
print('--result15----//li[1]/ancestor::div-----')
# 只获取div这个祖先节点
result15 = html.xpath('//li[1]/ancestor::div')
print(result15)
for result15_1 in result15:
print(result15_1.xpath('.//li[contains(@class,"-0")]/a/text()'))
输出如下:
--result15----//li[1]/ancestor::div-----
[<Element div at 0x182e1169e80>, <Element div at 0x182e1169e00>]
['01', '05']
['useless-01']
result15的返回结果为div节点,然后对result15进行遍历,在遍历中,通过xpath路径进一步获取a标签的文本。这里需要注意的是循环内的xpath路径以“.”开头,表示相对于当前div元素下,第一次输出为[‘01’, ‘05’],第二次输出为[‘useless-01’]。如果循环内的xpath路径去掉“.”,则循环内的两次输出是一致,应该都为[‘01’, ‘05’, ‘useless-01’]。
16.获取所有属性值:
print('--result16----//li[1]/attribute::*-----')
result16 = html.xpath('.//li[1]/attribute::*')
print(result16)
输出如下:
--result16----//li[1]/attribute::*-----
['cla-0', 'id-0', 'cla-0']
输出结果为所有li中的第1个节点的属性值。
17.获取所有子孙节点a标签:
print('--result17----//div/descendant::a-----')
result17 = html.xpath('//div[@class="useless"]/descendant::a')
print(result17)
输出如下:
--result17----//div/descendant::a-----
[<Element a at 0x1f34cf2a540>, <Element a at 0x1f34cf2a5c0>]
descendant表示匹配子孙节点。
以上就是lxml的基本操作,更多操作可以自行组合或参考官网,需要说明的是,在浏览器端通过开发者工具–查看器–选择元素–右键复制–选择XPath路径,可以获取选择元素的XPath路径,通过这种方法可以加快XPath路径构建。
另外需要注意的是,xpath()函数的返回值为列表,可以通过先抓取外层的数据,然后通过遍历或是索引的方式获取节点数据,然后通过相对路径的方式进一步读取内层元素节点。案例如下:
18.先获取外层元素,再通过相对路径的方式获取内部元素:
print('--result18----//li[1]/ancestor::div-----')
result18 = html.xpath('//li[1]/ancestor::div')
print(result18)
print(result18[0].xpath('./ul/li/a/text()'))
在上面代码中 ,result18[0]表示获取列表中的第一个Element 类型元素,然后对Element 类型元素进行xpath操作。./ul/li/a/text()中的“.”表示相对当前节点。
输出为:
--result18----//li[1]/ancestor::div-----
[<Element div at 0x28e3b83a000>, <Element div at 0x28e3b83a0c0>]
['01', '02', '04', '05']