1. xpath基本语法
1.1.介绍
XPath 使用路径表达式来选取 XML 文档中的节点或节点集。XML,可能很多同学都不知道这是个什么东西,XML和HTML很相似,但是也有本质的区别。这里来简单介绍一下。
HTML 是超文本标记语言,HTML 使用标记标签来描述网页,它的文档中包含了 HTML 标签以及文本内容,HTML 文档也叫做 web 页面。
XML 是可扩展标记语言,用于创建网页和 web 应用程序。XML 是动态的,可用来传输数据。
区别
-
- 语言类型不同;HTML 是超文本标记语言,而 XML 是可扩展标记语言。
- 功能状态不同;HTML 用于显示数据,它是静态的;而 XML 用于传输数据,它是动态的。
- 区分大小写;HTML 中不区分大小写,而 XML 需要区分大小写。
- 标签数量;HTML 的标签数量有限,而 XML 标记是可扩展的。
- 结束标记;HTML 中不需要结束标记,而 XML 中需要结束标记。
- 预定义;HTML 中有预定义标记,而 XML 中是用户定义的标记。
这些概念大家了解一下即可,只需要知道Xpath不仅仅可以选取XML文档中的节点或节点集,也适用与HTML,目前的话已经很少见XML了,基本都是HTML。
1.2.语法
实例
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
1.2.1.选取节点
XPath 使用路径表达式在 XML 文档中选取节点。节点是通过沿着路径或者 step 来选取的。 下面列出了最有用的路径表达式:
表达式 | 描述 |
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:
路径表达式 | 结果 |
bookstore | 选取 bookstore 元素的所有子节点。 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
1.2.2.谓语
谓语用来查找某个特定的节点或者包含某个指定的值的节点。
谓语被嵌在方括号中。
在下面的表格中,我们列出了带有谓语的一些路径表达式,以及表达式的结果:
路径表达式 | 结果 |
/bookstore/book[1] | 选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] | 选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] | 选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()] | 选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] | 选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang='eng'] | 选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] | 选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]//title | 选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
需要注意xpath索引值是从1开始,而非从0开始。
1.2.3.选取未知节点
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
1.2.4.选取若干路径
通过在路径表达式中使用"|"运算符,您可以选取若干个路径。
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
2. jsonpath语法与xpath语法对比
2.1.语法对比
XPath | JsonPath | 说明 |
/ | $ | 文档根元素 |
. | @ | 当前元素 |
/ | .或[] | 匹配下级元素 |
.. | N/A | 匹配上级元素,JsonPath不支持此操作符 |
// | .. | 递归匹配所有子元素 |
* | * | 通配符,匹配下级元素 |
@ | N/A | 匹配属性,JsonPath不支持此操作符 |
[] | [] | 下标运算符,根据索引获取元素,XPath索引从1开始,JsonPath索引从0开始 |
` | ` | [,] |
N/A | [start:end:step] | 数据切片操作,XPath不支持 |
[] | ?() | 过滤表达式 |
N/A | () | 脚本表达式,使用底层脚本引擎,XPath不支持 |
() | N/A | 分组,JsonPath不支持 |
Xpath用于解析XML/HTML,jsonpath用于解析json数据,正则用于解析字符串。
2.2.实例操作对比
在之前学习jsonpath的时候有这么一个例子:
json格式实例
{
"store": {
"book": [{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
}, {
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
}, {
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
}, {
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
}
}
获取同样节点jsonpath规则与xpath规则有什么不同呢?下面给大家列出来了。不过这里一定需要注意xpath是无法解析json格式数据的,这里只是给大家举例获取节点模拟一下。
接下来我们看一下如何对这个文档进行解析:
XPath | JsonPath | Result |
/store/book/author | $.store.book[*].author | 所有book的author节点 |
//author | $..author | 所有author节点 |
/store/* | $.store.* | store下的所有节点,book数组和bicycle节点 |
/store//price | $.store..price | store下的所有price节点 |
//book[3] | $..book[2] | 匹配第3个book节点 |
//book[last()] | $..book[(@.length-1)],或 $..book[-1:] | 匹配倒数第1个book节点 |
//book[position()<3] | $..book[0,1],或 $..book[:2] | 匹配前两个book节点 |
//book[isbn] | $..book[?(@.isbn)] | 过滤含isbn字段的节点 |
//book[price<10] | $..book[?(@.price<10)] | 过滤price<10的节点 |
//* | $..* | 递归匹配所有子节点 |
3. python程序使用xpath
3.1. xpath 依赖库使用
3.1.1. 依赖库安装
想要在python当中使用 xpath 语法进行数据解析需要安装 xpath 依赖库。
普通安装:pip install lxml
镜像源安装: pip install lxml -i http://pypi.douban.com/simple/
安装好之后就可以使用该库写xpath规则解析数据了。
3.1.2. lxml 库应用
导入模块
from lxml import etree
虽然是 xpath 解析库,但是不论是从库名还是方法名来看都是和 xpath 没有关系的,所以也是需要大家打破传统的思想,认为使用什么技术那么所依赖的库名肯定就是该技术名称,其实是不对的,之后大家会遇到很多名称和库名不对应的依赖库的。
库导入成功后,使用第一小节中的实例代码给大家演示一下该模块的使用方法。
etree基本使用
from lxml import etree
line = """
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
"""
tree = etree.XML(line) # 因为line数据是XML结构,所以是.XML(), 如果数据是HTML结构就是.HTML()
print(tree)
可以看到打印结果是一个 Element 对象,只要数据类型是 Element 对象就可以使用.xpath()方法。比如现在我们要获取第一个 book 标签下的 price 节点, xpath 规则为/bookstore/book[1]/price, 那么在程序中就可以写成:
获取第一个book下的price节点
from lxml import etree
line = """
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
"""
tree = etree.XML(line) # 因为line数据是XML结构,所以是.XML(), 如果数据是HTML结构就是.HTML()
# print(tree)
first_price = tree.xpath('/bookstore/book[1]/price')
print(first_price)
就获取到了 price 节点,如果想要该节点下的文本值,就只需要在 xpath 规则后面跟上/text()
即可。
如果是要获取第一个book节点下面的title节点的lang
属性值,就可以写成:
返回的是一个数据结构是列表,要取具体文本值的话就只需要使用列表取值操作去取值即可。其他的解析需求各位自行按照第一小节去尝试获取,这里我就不再一一进行演示了,自己一定要都去试试,别我不演示你就不去试了,那我会了不代表你也会了,能力都是敲出来的。接下来就上案例。
3.2. 案例实战
本次案例选择的是自如租房,获取房屋数据(标题)。抓包的流程就不再赘述,通过关键词检索发现数据就在z0/
这个数据包当中,所以需要先获取这个z0/
数据包。
请求方式为get,无需请求参数,那就看看请求头里面有没有特殊的参数。
都是普通参数,那就带上这个User-Agent:
用户代理发起请求。
发起请求获取响应保存本地html
import requests
class MySpider(object):
def __init__(self):
self.url = "https://www.ziroom.com/z/z0/"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}def Urequest(self): # 封装的请求函数
return requests.get(url=self.url, headers=self.headers)def getMainHtml(self):
response = self.Urequest().content.decode()
with open('自如.html', 'w', encoding='utf8') as f:
f.write(response)
if __name__ == '__main__':
s = MySpider()
s.getMainHtml()
运行后就可以看到本地文件当中多出了一个自如.html
文件,打开里面是有数据的,那再通过关键词搜索看看数据在不在里面,也就是看看有没有获取到正确的响应数据。
现在可以确定数据就在这个响应当中,就可以开始着手解析数据了。这里直接就给大家讲遇到这种数据的解析思路了。可以看到页面当中是有很多房屋信息的,那就要先找到整个房屋数据标签是什么,这时候可以在抓包工具中的 Elements 里面去查看一下。
这个是所有数据的标签,下面的一个又一个div标签就是每一条数据。
所以就可以先写到这个第一个房屋信息标签的 xpath 规则,快捷方法是直接右键标签选择copy xpath
但这种方法存在一个问题,就是这里copy的xpath规则是按照 Elements 结构的规则,而非真实响应数据的规则,也就是说当响应数据与 Elements 结构不同时,直接copy的xpath规则可能会导致获取不到数据,所以此方法虽好,但不注意就会掉进一些坑里面去。作为一个骨灰级爬虫玩家,我选择手写 xpath 规则。
先来看一下,一般先找整个数据标签,也就是:
在咱们的本地文件中搜一下这个class值Z_list-box
发现该值是唯一的,那么就可以直接通过这个特征进行定位。
定位总数据标签
import requests
from lxml import etreeclass MySpider(object):
def __init__(self):
self.url = "https://www.ziroom.com/z/z0/"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}def Urequest(self): # 封装的请求函数
return requests.get(url=self.url, headers=self.headers)def getMainHtml(self):
response = self.Urequest().content.decode()
# with open('自如.html', 'w', encoding='utf8') as f:
# f.write(response)
tree = etree.HTML(response) # 现在解析的是HTML结构所以是.HTML()
div_list = tree.xpath('//div[@class="Z_list-box"]')
if __name__ == '__main__':
s = MySpider()
s.getMainHtml()
tree.xpath('//div[@class="Z_list-box"]')
这句的意思是跨节点定位一个div标签,什么条件的div标签呢,class值为Z_list-box
的div标签,所以就能够拿到该div节点。
现在总的数据标签拿到了,但是具体数据是在下面的子div标签里面的,所以还需要跨一个div节点获取到所有的div节点,也就是tree.xpath('//div[@class="Z_list-box"]/div')
,没有指定是什么div,所以就会把子代div都获取到,如果是/div[1]
,那么就是第一条数据节点,不指定就是所有的子代div节点。
现在就获取到了很多的div节点了,且这些节点都是被列表包裹的,可以使用len(div_list)
查看数量。现在单条房屋信息数据标签列表拿到了,就该去循环这个列表继续去精准xpath取值了。
先来获取房屋标题。这个时候为了更好的分析结构,我们可以单独拿一个数据标签出来放到一个新的html文件中进行观察。
通过结构展开后,收起不需要的标签,整个结构就非常清晰了。
可以看到房屋标题是 h5 标签下面的 a 标签的文本值,搜一下 h5 标签是不是唯一的,一般这种标题型标签都是比较特殊的。
是唯一的,那么如果你大胆一点就直接可以//h5/a/text()
获取,如果想要保守一点就/div[@class="info-box"]/h5/a/text()
。
获取房屋标题
import requests
from lxml import etreeclass MySpider(object):
def __init__(self):
self.url = "https://www.ziroom.com/z/z0/"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}def Urequest(self): # 封装的请求函数
return requests.get(url=self.url, headers=self.headers)def getMainHtml(self):
response = self.Urequest().content.decode()
# with open('自如.html', 'w', encoding='utf8') as f:
# f.write(response)
tree = etree.HTML(response) # 现在解析的是HTML结构所以是.HTML()
div_list = tree.xpath('//div[@class="Z_list-box"]/div')
for div in div_list:
title = div.xpath('.//h5/a/text()') # . 是代表当前节点
print(title)
if __name__ == '__main__':
s = MySpider()
s.getMainHtml()
这里说一下为什么循环的div也能继续.xpath()
,在上文中我们打印div_list
的时候,大家能够看到列表当中包裹的是一个有一个的div Element
对象,所以还能够继续写xpath规则,.
就是代表当前节点,当前节点在哪呢,如果是第一次循环,那么当前节点就在
这个位置,以此类推。
查看输出结果,发现第五个数据是空的,来看看页面中第五个是什么东西。
第五个是广告信息,所以没有信息是正常的,也可以去本地看看,本地的第五个div结构是和其他div结构不一样的,先不管他,后面保存数据的时候再来处理这个东西。标题拿到之后还需要拿房屋详细信息,也就是
这个规则就更简单了,只需要定位class属性值为desc
的div标签,然后再获取下面div节点的文本值即可。
两个div的文本值都是获取到了,当然如果你觉得这两个文本值不是同一个类型,你也可以单独进行获取,第二个文本值有很多空行,可以先将这两个元素拼接起来后再去除这些换行以及空格,拼接列表元素的话可以使用''.join()
方法,前面引号当中是要以什么字符拼接列表元素,不写就是以空拼接,方法内写的就是列表对象。
拼接上之后就可以用replace()
方法进行替换换行、制表符和空格了。
现在就已经拼接上了,老样子,第五个广告先不管。这个案例的价格的话是用图片的形式展示的,所以就先不获取价格了,再获取一下房屋图片的url吧。
这里出现了两个后缀为.jpg
的url,那到底是哪一个呢,去到页面中看一下。
页面当中的图片url是src
属性值,但是页面当中的src
属性值和本地src
属性值不同,通过对比,页面中的src值和本地的data-original
属性值相同,这也是本地响应数据和Element结构不同的一个小点,直接从页面copy xpath 的话你很容易就写成了/@src
,但实际上响应数据的/@src
值与页面端完全不一样。
所以这里xpath规则可以这么写.//img/@data-original
。
第五个广告的图片又不一样,所以可以确定,第五个元素是我们不需要的,那在循环前直接把第五个元素删掉就好了。然后返回值都是列表,那就对列表进行下标取值就行了。图片url缺失协议头,所以还需要再前面拼接https:
import requests
from lxml import etreeclass MySpider(object):
def __init__(self):
self.url = "https://www.ziroom.com/z/z0/"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}def Urequest(self): # 封装的请求函数
return requests.get(url=self.url, headers=self.headers)def getMainHtml(self):
response = self.Urequest().content.decode()
# with open('自如.html', 'w', encoding='utf8') as f:
# f.write(response)
tree = etree.HTML(response) # 现在解析的是HTML结构所以是.HTML()
div_list = tree.xpath('//div[@class="Z_list-box"]/div')
div_list.pop(4) # 删除广告div节点
for div in div_list:
title = div.xpath('.//h5/a/text()')[0] # . 是代表当前节点
infoMessage = div.xpath('.//div[@class="desc"]/div/text()')
info = "-".join(infoMessage).replace(' ', '').replace('\n', '').replace('\t', '')
pic_url = 'https:' + div.xpath('.//img/@data-original')[0]
print(title, info, pic_url)
if __name__ == '__main__':
s = MySpider()
s.getMainHtml()
数据成功拿到,保存为json文件就不需要再过多赘述了吧,之前都演示过了,我这里就直接给完整代码了,无非就是类的写法会有点区别。
完整代码
import json
import requests
from lxml import etreeclass MySpider(object):
def __init__(self):
self.url = "https://www.ziroom.com/z/z0/"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
self.dic = { # 定义类全局字典用于数据保存
'list': []
}def Urequest(self): # 封装的请求函数
return requests.get(url=self.url, headers=self.headers)def getMainHtml(self):
response = self.Urequest().content.decode()
# with open('自如.html', 'w', encoding='utf8') as f:
# f.write(response)
tree = etree.HTML(response) # 现在解析的是HTML结构所以是.HTML()
div_list = tree.xpath('//div[@class="Z_list-box"]/div')
div_list.pop(4) # 删除广告div节点
for div in div_list:
title = div.xpath('.//h5/a/text()')[0] # . 是代表当前节点
infoMessage = div.xpath('.//div[@class="desc"]/div/text()')
info = "-".join(infoMessage).replace(' ', '').replace('\n', '').replace('\t', '')
pic_url = 'https:' + div.xpath('.//img/@data-original')[0]
# print(title, info, pic_url)
dic = {
'title': title,
'info': info,
'picUrl': pic_url
}
self.dic["list"].append(dic)def saveJson(self):
with open('自如.json', 'w', encoding='utf8') as f:
json.dump(self.dic, f, ensure_ascii=False, indent=4)
if __name__ == '__main__':
s = MySpider()
s.getMainHtml()
# 前面数据获取完成之后再调用保存函数
s.saveJson()