Python XPath的使用
Python XPath的使用
0x00 前言
几个月前学习了一下XPath,好久没用发现已经忘得一干二净了,今天打算参考《Python3 网络爬虫开发实战》复习一下XPath的基本内容,做点笔记放在博客上方便以后查阅
0x01 XPath介绍
XPath,全称XML Path Language,即XML路径语言,它是一门在XML文档中查找信息的语言。它最初是用来搜寻XML文档的,但是它同样适用于HTML文档的搜索。所以1在做爬虫时,我们完全可以使用XPath来做相应的信息抽取。
0x02准备工作
在使用XPath之前我们得先安装lxml库
pip install lxml
0x03 实例引入
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
'''
html = etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))
我们首先导入lxml库得etree模块,然后声明了一段HTML文本(来自猫眼电影),调用HTML类进行初始化,这样就成功构造了一个XPath解析对象。etree模块还可以自动修正HTML文本。这里我们调用tostring()方法即可输出修正后的HTML代码,但是结果是bytes类型,我们利用了decode()方法将其转化成str类型,结果如下:
<html><body><div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
</p><p class="releasetime">上映时间:2019-12-13 </p></div>
</body></html>
可以看到,经过处理之后节点标签被不齐,还自动添加了body、html节点。
另外,XPath也可以直接读取文本进行解析,示例如下:
from lxml import etree
html = etree.parse('F:\\code\\myProject\\博客\\test.html', etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
其中读取目录里的文件内容为:
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
这里解析中文出现了html编码,我们现在只是了解下XPath解析基础知识,不用纠结于中文被编码,我们稍后会解决这个问题。运行结果如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
</p><p class="releasetime">上映时间:2019-12-13 </p></div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div></body></html>
0x04 获取节点
- 获取所有节点
我们一般会用//开头的XPath规则来选取所有符合要求的节点。可以这样实现:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//*') # 获取所有节点
print(result)
这里使用*代表匹配所有节点,也就是整个HTML文本中的所有节点都会被获取。可以看到,返回形式是一个列表,每个元素是Element类型,其后跟了节点的名称,如html、body、div、p、a等,所有节点都包含在列表中了。
- 获取指定节点
如果想获取所有p节点,示例如下:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p') # 获取所有p节点
print(result)
print(result[0])
这里要选取所有p节点,可以使用//,然后直接加上节点名称即可,调用时直接使用xpath()方法即可。运行结果:
[<Element p at 0x46ac428>, <Element p at 0x46ac4e8>, <Element p at 0x46ac528>, <Element p at 0x46ac548>, <Element p at 0x46ac568>, <Element p at 0x46ac5a8>]
<Element p at 0x46ac428>
这里可以看到提取结果是一个列表形式,其中每个元素都是一个Element对象。如果要取出其中的一个对象,可以直接用中括号加索引,如[0]
- 获取子节点
我们可以通过/或//即可查找出元素的子节点或子孙节点。假如现在想选择div节点的所有p子节点,可以这样实现:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//div/p') # 获取div的子节点p
print(result)
print(result[0])
运行结果如下:
[<Element p at 0x49ad508>, <Element p at 0x49ad5c8>, <Element p at 0x49ad608>, <Element p at 0x49ad628>, <Element p at 0x49ad648>, <Element p at 0x49ad688>]
<Element p at 0x49ad508>
* 获取子孙节点 上面我们使用/来获取了子节点,想要获取子孙节点我们可以用//。例如要获取div节点下的所有子孙节点,可以这样实现: ```python from lxml import etree
text = '''
''' html = etree.HTML(text) result = html.xpath('//div//a') # 获取div下的子孙节点,就是a print(result) print(result[0]) ``` 运行结果如下: ``` [- 获取父节点
我们已经知道了获取子节点和子孙节点,获取父节点可以用..来实现。比如我们想获取href属性为/films/1218273的a节点的父节点的class属性,也就是p的class属性,可以这样来实现:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//a[@href="/films/1218273"]/../@class')
print(result)
运行结果如下:
['name']
0x05 属性匹配
上面我们获取父节点的属性时已经用到了属性的知识,在xpath中我们可以用@符号来进行属性过滤。比如这里要选取class为star的p节点,可以这样实现:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="star"]') # 属性匹配
print(result)
这里我们通过加入[@class="star"],限制了节点的class属性为star,而在案例的HTML文本中符合条件的p节点有两个,所以结果应该返回两个匹配到的元素。结果如下:
[<Element p at 0x4c0c3a8>, <Element p at 0x4c0c468>]
0x06文本获取
我们可以利用XPath中的test()方法来获取文本内容,接下来尝试获取属性为class的p节点的文本,也就是我们的电影名称
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]/text()')
print(result)
运行结果如下:
[]
我们可以看到我们并没有获取到任何文本,书上给的解释是:XPath中text()前面是/,而此处/的含义是选取直接子节点,p的直接子节点都是a节点,文本都是在a节点内部的。我们错误的使用了/使得我们没有匹配到想要获取的内容
如果想要获取p节点的内部文本,就有两种获取方式,一种是先获取a节点在获取文本,另一种就是使用//。接下来,我们来看一下二者的区别:
首先,选取到a节点在获取文本,代码如下:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]/a/text()')
print(result)
运行结果如下:
['误杀', '叶问4:完结篇']
可以看到这里的返回值是两个,内容都是属性为name的p节点的文本。这里我们是逐层选取的,先选取了p节点,有利用/选取了其直接子节点a,然后再选取文本,得到的结果恰好是我们预期的两个结果
再来看下用另外一种方式(即使用//)选取的结果,代码如下:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="name"]//text()') # 使用//获取p节点下的文本
print(result)
运行结果如下:
['误杀', '叶问4:完结篇']
我们可以看到我们还是得到了预期的结果,但是如果我们想获取属性为star的p节点下的文本,代码如下:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p[@class="star"]//text()')
print(result)
运行结果:
['\n 主演:肖央,谭卓,陈冲\n', '\n 主演:甄子丹,吴樾,吴建豪\n ']
我们发现虽然获取到了我们想要的内容,但是里面还有一些换行符。所以说,如果想要获取子孙节点内部的所有文本,可以直接使用//加text()方式,这样可以保证获取到最全面的文本信息,但是可能会夹杂一些换行符等特殊字符。如果想要获取某些特定子孙节点下的所有文本,可以先获取到特定的子孙节点,然后再调用text()方法获取其内部文本,这样可以保证我们的结果是整洁的
0x07 属性获取
我们知道用text()可以获取节点内部文本,那么节点属性该怎样获取呢?其实还是用@符号就可以。例如,我们想获取所有p节点下所有a节点的href属性,代码如下:
from lxml import etree
text = '''
<div class="movie-item-info">
<p class="name"><a href="/films/1218273" title="误杀" data-act="boarditem-click" data-val="{movieId:1218273}">误杀</a></p>
<p class="star">
主演:肖央,谭卓,陈冲
<p class="releasetime">上映时间:2019-12-13 </div>
<div class="movie-item-info">
<p class="name"><a href="/films/1190122" title="叶问4:完结篇" data-act="boarditem-click" data-val="{movieId:1190122}">叶问4:完结篇</a></p>
<p class="star">
主演:甄子丹,吴樾,吴建豪
</p>
<p class="releasetime">上映时间:2019-12-20</p> </div>
'''
html = etree.HTML(text)
result = html.xpath('//p/a/@href')
print(result)
这里我们通过@href即可获取节点的href属性。注意,此处和属性匹配的方法不同,属性匹配是中括号加属性名和值来限定某个属性,如[@href="/films/1218273"],而此处的@href指的是获取节点的某个属性,二者需要做好区分。
运行结果如下:
['/films/1218273', '/films/1190122']
我们成功获取了所有p节点下的a节点的href属性,他们以列表的形式返回
0x08 属性多值匹配
有时候,某些节点的某个属性可能有多个值,例如:
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()函数了,代码可以改写如下:
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()方法,第一个参数传入属性名称,第二个参数传入属性值,只要此属性包含所传入的属性值,就可以完成匹配了。
此时运行结果如下:
['first item']
0x09 多属性匹配
另外,我们可能遇到一种情况,那就是根据多个属性确定一个节点,这时就需要同时匹配多个属性。此时可以使用运算符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。要确定这个节点,需要同时根据class和name属性来选择,一个条件是class属性里面包含li字符串,另一个条件是name属性为item字符串,二者需要同时满足,需要用and操作符相连,相连之后置于中括号内进行条件筛选。运行结果如下:
['first item']
0x10 按序选择
有时候我们在选择的时候某些属性可能同时匹配了多个节点,但是又想要其中的某个节点,这是我们可以利用中括号传入索引的方法来获取特定次序的节点,示例如下:
from lxml import etree
text = '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first item</a></li>
<li class="item-0"><a href="link2.html">second item</a></li>
<li class="item-1"><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>
'''
html = etree.HTML(text)
result_1 = html.xpath('//li[1]/a/text()')
print(result_1)
result_2 = html.xpath('//li[last()]/a/text()')
print(result_2)
result_3 = html.xpath('//li[position()<3]/a/text()')
print(result_3)
result_4 = html.xpath('//li[last()-2]/a/text()')
print(result_4)
第一次选择时我们选取第一个li节点,中括号传入数字1即可。第二次选择时我们选取最后一个li节点,中括号中传入last()即可。第三次选择时,我们选取了位置小于3的li节点,也就是位置序号为1和2的li节点。第四次选择时,我们选取了倒数第三个li节点
运行结果如下:
['first item']
['fifth item']
['first item', 'second item']
结语
XPath选择器的功能十分强大,使用起来简单方便,大大的提升了我们爬虫的效率。