Xpath的全程是 XML Path Language,即xml路径语言,用来在XML文档中查找信息,它最初就是用来搜寻xml文档的,但同时适用于html文档的搜索。
所以在做爬虫时,我们完全可以使用Xpath实现对应的信息抽取,本节我们就介绍一下Xpath的基本用法。
1.Xpath的概览
首先,Xpath的选择功能十分强大,它提供了非常简洁明了的路径选择表达式,另外,它还提供了100多个内建函数,用于字符串,数值,时间的匹配以及节点,序列的处理等,几乎所有我们想要的定位节点,都可以用Xpath选择。
Xpath于1999年11月16日成为W3C标准,它被设计出来,供XSLT,XPointer以及其他XML解析软件使用
2.Xpath常用规则
具体语法规则可以参考下面网站
//title[@lang='eng']
它代表了选择所有名称为title,同时属性lang的值为eng的节点。
后面通过python的lxml库,利用Xpath对HTML进行解析
3.准备工作
使用lxml库之前,首先确保其已经安装
安装代码:pip install lxml
详情参考 https://setup.scrape.center/lxml
安装完成
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting lxml
Downloading https://pypi.tuna.tsinghua.edu.cn/packages/31/58/e3b3dd6bb2ab7404f1f4992e2d0e6926ed40cef8ce1b3bbefd95877499e1/lxml-4.9.3-cp311-cp311-win_amd64.whl (3.8 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.8/3.8 MB 4.3 MB/s eta 0:00:00
Installing collected packages: lxml
Successfully installed lxml-4.9.3
如果大家使用谷歌浏览器的话,可以安装一个xpath helper的插件

这个插件可以方便我们直接在网页上输入xpath代码,并返回我们想要的结果,是一个不错的实用插件,具体安装方法可以在网上搜索一下。我的插件之前装完不知道放哪了。
4.实例引入
下面我们通过实例感受一下xpath对网页进行解析的过程,相关代码如下:
from lxml import etree
text='''
<div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
<div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
<div class="el-card__body">
<img data-v-63864230=""
src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c"
class="image">
<p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item"
aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
<p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item"
aria-describedby="el-tooltip-9582" tabindex="0">饰:Civil War News Poppet</p></div>
</div>
</div>
'''
html=etree.HTML(text)
result = etree.tostring(html)
print(result.decode('utf-8'))
这里我们随便找了一段网页代码,首先导入lxml库的etree模块,然后声明了一段HTML文本,接着调用HTML类进行初始化,这样就成功构造了一个Xpath解析对象,此处需要注意一点,HTML的文本如果没有对其或者闭合,etree模块会帮我们自动修正HTML文本的。
之后调用tostring方法即可输出修正后的HTML代码。但是结果时bytes类型,于是利用decode方法将其转化str类型。输出结果如下:
<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
<div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
<div class="el-card__body">
<img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>
<p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
<p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">饰:Civil War News Poppet</p></div>
</div>
</div>
</body></html>
可以看到的是,它不仅可以不全代码,还自动添加了body,html节点。
另外也可以不声明。直接读取文本进行解析,示例如下:
from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = etree.tostring(html)
print(result.decode('utf-8'))
其中文件中的内容就是上面例子中的HTML代码。这次输出的结果和上次不同,多了一个DOCTYPE声明,不过对解析没有任何影响,结果如下
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
<div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
<div class="el-card__body">
<img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>
<p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
<p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">饰:Civil War News Poppet</p></div>
</div>
</div>
</body></html>
5.所有节点
我们一般会用//开头的xpath规则,来选取所有符合要求的节点,这里还是以之前实例中HTML文本为例,选取其中的所有节点,实现代码如下:
from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//*')
print(result)
运行结果如下
[<Element html at 0x2757cb68140>, <Element body at 0x2757d0ce240>, <Element div at 0x2757d0ce340>, <Element div at 0x2757d0ce5c0>, <Element div at 0x2757d0ce200>, <Element img at 0x2757d0ce4c0>, <Element p at 0x2757d0ce300>, <Element p at 0x2757d0ce640>]
这里使用*代表匹配所有节点,也就是获取了整个HTML文本中的所有节点,如果只想获取某一类型的节点例如文本中含有p节点,我们就可以改写代码:
from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//p')
print(result)
输出结果
[<Element p at 0x2505a3ee240>, <Element p at 0x2505a3ee340>]
可以看到提取结果也是一个列表,其中每个元素都是Element类型,要是想取出其中一个对象,可以直接用中括号加元素索引获取,如[0]
6.子节点
通过/或者//即可查找元素的子节点或子孙节点,假如现在想获取div节点下的所有p节点,则可以这样实现:
from lxml import etree
html=etree.parse('xpath的基本使用\\test.html',etree.HTMLParser())
result = html.xpath('//div/p')
print(result)
通过追加/p的方式,选择了所有div下的p节点,其中//div用于选择所有li节点,/a用于选择li节点的所有子节点a
运行结果如下:
[<Element p at 0x20e6d59a380>, <Element p at 0x20e6d59a480>]
有人会说这两次的返回值是一样的,我直接//p节点也可以获取这两个节点,其实是不一样的
我们在原文档的代码再加上两个p节点
<html><body><div data-v-63864230="" class="actor el-col el-col-4" style="padding-left: 7.5px; padding-right: 7.5px;">
<div data-v-63864230="" class="el-card m-b is-hover-shadow"><!---->
<div class="el-card__body">
<img data-v-63864230="" src="https://p0.meituan.net/movie/2dfc1d8df6b41d2055e637c1a4b04e0e13679.jpg@128w_170h_1e_1c" class="image"/>
<p data-v-63864230="" class="el-tooltip name text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-6009" tabindex="0">Dulcie Smart</p>
<p data-v-63864230="" class="el-tooltip role text-center m-b-none m-t-xs item" aria-describedby="el-tooltip-9582" tabindex="0">饰:Civil War News Poppet</p></div>
</div>
</div>
<p>hello</p>
<p>world</p>
</body></html>
再次运行发现,依然只有两个p节点被获取了,这是因为div下的p节点只有两个,如果//p获取的话会获取所有的p节点,所以对比较精细的数据进行提取的时候,要注意提取格式,不要出现获取不必要的数据,加重运行负担。
上面的/用于选取节点的直接子节点,如果要获取节点的所有子孙节点,可以使用//,例如要获取body节点下的所有img节点,就可以写成//body//img
运行结果都是一样的,但是如果写成//body/img就无法获取任何结果,因为/用于获取直接子节点,而body下没有直接的img子节点,只有div和p节点,所有无法获取任何匹配结果。输出结果为[]
因此这里要注意/和//的区别,前者用于获取直接子节点,后者用于获取子孙节点。
父节点
通过连续的/或//可以查找子节点或子孙节点,那么假如知道了子节点,怎样查找父节点呢,这可以用..实现
首先选择a节点中href属性为detail/1,然后获取其父节点,在获取父节点的class属性,相关代码如下
html代码
<div data-v-7f856186="" class="p-h el-col el-col-24 el-col-xs-9 el-col-sm-13 el-col-md-16">
<a data-v-7f856186="" href="/detail/1" class="name">
<h2 data-v-7f856186="" class="m-b-sm">霸王别姬 - Farewell My Concubine</h2>
</a>
<div data-v-7f856186="" class="categories">
<button data-v-7f856186="" type="button"
class="el-button category el-button--primary el-button--mini">
<span>剧情</span>
</button>
<button data-v-7f856186="" type="button"
class="el-button category el-button--primary el-button--mini">
<span>爱情</span>
</button>
</div>
python代码
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@href="/detail/1"]/../@class')
print(result)
运行结果
['p-h el-col el-col-24 el-col-xs-9 el-col-sm-13 el-col-md-16']
正好就是父节点的class属性。
也可以通过parent::获取父节点,代码如下:
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@href="/detail/1"]/parent::*/@class')
print(result)
运行结果是一样的
属性匹配
在选取节点的时候,还可以使用@符号实现属性过滤,例如,选取class属性为name的a节点,可以这样实现
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]')
print(result)
运行结果:[<Element a at 0x1369dc9e740>]
这里符合xpath的表达式的只有一个结果,那么就会返回一个,如果多个表达式符合,那么就都会进行返回,如果返回的结果不是想要的,那么xpath还需要添加别的方法进行验证。
文本获取
用XPath中text方法可以获取节点中的文本,接下来尝试获取前面的节点中的文本,相关代码如下:
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]/text()')
print(result)
运行结果:['\r\n ', '\r\n ']
奇怪的是,没有获取任何文本,只获取了换行符,这是为什么呢?因为xpath中的text方法前面是/,而/的含义是选取直接子节点,很明显a的子节点是h2,文本都在h2节点内部,所以这里匹配的结果就是被修正a节点内部的换行符,因为自动修正的a节点尾部标签换行了。
因此,如果想要获取a节点的文本,就有两种方式,一种是先选取a节点在获取文本,另一种是使用//
我们写代码举例
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]//text()')
print(result)
因为结果含有中文,输出结果会有乱码['\r\n ', 'é\x9c¸ç\x8e\x8bå\x88«å§¬ - Farewell My Concubine', '\r\n ']
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a[@class="name"]/h2/text()')
print(result)
由此,想要获取子孙节点内部的所有文本可以直接使用//加text方法的方式,这样能够保证获取最全面的文本信息,但是可能会夹杂一些换行符,如果只想获取某些特定子孙节点下的所有文本,则可以先选取特定的子孙节点,在调用text方法获取其内部的文本,这样就可以保证获取的结果是整洁的。
属性获取
我们已经用text方法获取节点内部文本,那么节点属性该如何获取,其实依然可以用@符号代替,例如通过代码获取所有a节点下的href属性:
from lxml import etree
html=etree.parse('xpath的基本使用\\test2.html',etree.HTMLParser())
result = html.xpath('//a/@href')
print(result)
运行结果:['/detail/1']
这里只有一个a节点并且有href值,如果存在多个,则会返回多个,并以列表形式返回
属性多值匹配
有些时候,某个节点的某个属性可能有多个值,或者属性值存在多个节点,这个时候要匹配节点就要进行多值匹配
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"]')
print(result)
这里的li节点有两个属性,如果用上述的方法获取节点,是无法获取到内容的
运行结果:
[]
这种情况就要用到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)
运行结果:['first item']
上面使用了contains方法,给其第一个参数传入属性名称,第二个参数传入属性值,只要传入的属性包含传入的属性值,就可以完成匹配
contains方法经常在某个节点的某个属性有多个值时用到
多属性匹配
我们还可能遇到一种情况,就是根据多个属性确定一个节点,这是需要同时匹配多个节点,运算符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,因此要确定li节点,需要同时考察class和name属性,一个条件是class属性里面包含li字符串,另一个条件是name属性的item字符串,这两者同时满足才能获取,class和name属性需要用and运算符相连,相连之后置于中括号内进行条件筛选,运行结果如下
['first item']
按序选择
在选择节点时,某个属性同时匹配了多个节点,但是我们只要其中一个,第一个,第二个或者最后一个,这是应该怎么办?
可以往中括号中传入索引的方法获取特定次序的节点,示例如下:
from lxml import etree
text= '''
<div>
<ul>
<li class="item-0"><a href="link1.html">first</a></li>
<li class="item-1"><a href="link3.html">second</a></li>
<li class="item-inactive"><a href="link4.html">third</a></li>
<li class="item-3"><a href="link5.html">fourth</a></li>
<li class="item-4"><a href="link2.html">fifth</a></li>
</ul>
</div>
'''
html=etree.HTML(text)
result=html.xpath('//li[1]/a/text()')
result2=html.xpath('//li[last()]/a/text()')
result3=html.xpath('//li[position()<3]/a/text()')
result4=html.xpath('//li[last()-2]/a/text()')
print(result)
print(result2)
print(result3)
print(result4)
上述代码中,第一次选择选取第一个li节点,在中括号中传入数字一即可实现,注意的是和代码不同传入的是数字1,而非0
第二次选择时选择最后一个li节点,在中括号中调用last方法即可实现。
第三次选择是,选择位置小于3的li节点,也就是位置序号为1,2的节点,得到的结果就是前两个节点。
第四次选择时,选取了倒数第三个li节点,在中括号中调用last方法在减去2即可实现,因为last方法时最后一个,在此基础上减2就得到了倒数三个
运行结果如下
['first']
['fifth']
['first', 'second']
['third']
https://www.runoob.com/xpath/xpath-syntax.html
4万+

被折叠的 条评论
为什么被折叠?



