Python爬虫 之数据解析之xpath
数据解析之xpath
一、xpath解析
1、xpath是什么
xpath是最常用且最高效的一种解析方式。通用性很高,不止在Python语言中可以使用,在其它语言中也可以使用。
xpath 全称 XML Path Language,即 XML 路径语言,它是一门 XML 文档中查找信息的语言。它最初是用来搜寻 XML 文档的,但是它同样适用于 HTML 文档的搜索。所以在做爬虫时,我们完全可以使用 xpath 来做相应的信息抽取。
2、xpath解析原理:
① 实例化一个 etree 的对象,且需要将被解析的页面源码数据加载到该对象中。
② 调用 etree 对象中的 xpath 方法,结合着xpath表达式实现标签的定位和内容的捕获。
二、环境的安装
使用xpath解析,只需要安装一个解析器:lxml。而这个解析器在bs4中也需要安装。lxml是一个解析器,也是下面的xpath要用到的库。
环境安装:
pip install lxml
三、实例化etree对象
首先需要导入模块
from lxml import etree
etree对象同样的有两个操作
1、将本地的 html 文档中的源码数据加载到 etree 对象中:
etree.parse(filePath)
# filePath为文件的路径
2、可以从互联网上获取的源码数据加载到该对象中:
etree.HTML('page_text')
# page_text为从页面获取的源码数据
调用 xpath 方法
使用 xpath 方法需要传入参数,这个参数是一个字符串,而这个字符串就是 xpath表达式。
xpath('xpath表达式')
也就是说在数据解析过程中,最重要的步骤就是xpath表达式的书写。意味着可以根据不同形式的xpath表达式定义不同的标签,并且可以将定位标签的相关文本数据进行获取。
四、xpath表达式
符号 | 含义 |
---|---|
/ | 表示的是从根节点开始定位。表示的是一个层级。 |
// | 表示的是多个层级。可以表示从任意位置开始定位。 |
属性定位:
tag[@attrName="attrValue"]
# 例如:
//div[@class="book-mulu"]
索引定位:
# 索引是从[1]开始的
tag[@attrName="attrValue"]/a[num] # a为标签名,num为第几行
# 例如
//div[@class="book-mulu"]/ul/li[3]
# 也可以表达为://div[@class="book-mulu"]//li[3]
取文本:
/text() # 获取标签中直系的文本内容
//text() # 获取标签中非直系文本内容(所有文本内容)
取属性:
/@attrName
# 例如:img/@src
五、项目实例
实例一
需求:
爬取58同城二手房源信息(以北京市为例)。解析出所有房源的名称,并进行持久化存储。
网址:https://bj.58.com/ershoufang/
思路:
主要就是观察页面的结构,看每个房源的名字所在的标签是哪个。然后写出xpath表达式即可。
因为所有房源名称都在li标签中,所以我们先将li标签的所有内容都爬取下来。
# xpath表达式
tree.xpath('//ul[@class="house-list-wrap"]/li')
我们观察到li标签下第二个div标签的h2标签下有名称信息,所有写下xpath表达式。因为这个是局部解析,div上一层标签是li所有div前面要加 “ . ”。
# xpath表达式
title = li.xpath('./div[2]/h2/a/text()')[0]
代码实现:
# 需求:爬取58通常二手房源信息
import requests
from lxml import etree
if __name__ == '__main__':
url = 'https://bj.58.com/ershoufang/'
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 爬取页面源码数据
page_text = requests.get(url=url,headers=header).text
# 页面解析
tree =etree.HTML(page_text)
li_list = tree.xpath('//ul[@class="house-list-wrap"]/li')
fp = open('58.txt','w',endcoding='utf-8')
for li in li_list:
# 局部解析
# 一定要加 . 这个 . 表示的就是局部定位到的标签
title = li.xpath('./div[2]/h2/a/text()')[0]
print(title)
# 存入文件
fp.write(title + '/n')
实例二
需求:
爬取《红楼梦》所有章节的标题。
网址:https://www.shicimingju.com/book/hongloumeng.html
思路:
主要是对xpath表达式的书写。通过观察标签写出xpath表达式。
# xpath表达式
tree = etree.xpath('//div[@class="card bookmark-list"]/div[4]/ul/li/a/text()')
代码实现:
import requests
from lxml import etree
if __name__ == '__main__':
url = 'https://www.shicimingju.com/book/hongloumeng.html'
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 爬取页面源码数据
page_text = requests.get(url=url,headers=header).text
# 实例化etree
tree = etree.HTML(page_text)
# xpath表达式
list1 = tree.xpath('//div[@class="card bookmark-list"]/div[4]/ul/li/a/text()')
for li in list1:
title = li
print(title)
实例三
需求:
解析下载图片数据。
网址:http://pic.netbian.com/4kfengjing/
思路:
主要是对 xpath表达式的书写,和怎样处理中文乱码。
xpath表达式可以从< div class = “slist”>标签开始,也可以从更上面的标签开始,比如< idv id = “main” > 可以从这里开始。
当然两个写法的含义是一样的。
# 1
src_list = tree.xpath('///div[@class="slist"]/ul/li/a/img/@src')
# 2
src_list = tree.xpath('//div[@id="main"]/div[3]/ul/li/a/img/@src')
# 3也可以选择简写
src_list = tree.xpath('///div[@class="slist"]//li/a/img/@src')
代码实现:
import requests
from lxml import etree
import os
if __name__ == '__main__':
url = 'http://pic.netbian.com/4kfengjing/'
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 爬取页面源码数据 获取相应对象
reponse = requests.get(url=url,headers=header)
# 手动设置响应数据编码格式
#reponse.encoding = 'utf-8'
page_text = reponse.text
# 数据解析,解析src的属性值,解析alt的属性值
# 实例化etree
tree = etree.HTML(page_text)
# xpath表达式
#src_list = tree.xpath('//div[@id="main"]/div[3]/ul/li/a/img/@src')
#alt_list = tree.xpath('//div[@id="main"]/div[3]/ul/li/a/img/@alt')
# 也可以这样写
src_list = tree.xpath('//div[@class="slist"]/ul/li')
# 创建一个文件夹
if not os.path.exists('./tupian'):
os.mkdir('./tupian')
for li in src_list:
img_src = 'http://pic.netbian.com' + li.xpath('./a/img/@src')[0]
img_alt = li.xpath('./a/img/@alt')[0] + '.jpg'
# 统用处理解决中文乱码的解决方法
img_alt = img_alt.encode('iso-8859-1').decode('gbk')
print(img_alt + " : " + img_src)
# 图片地址转化成二进制
img_data = requests.get(url=img_src,headers=header).content
img_path = 'tupian/' + img_alt
# 存储
with open(img_path,'wb') as fp:
fp.write(img_data)
案例四
需求:
解析出所给网址中全国城市的名称。
网址:https://www.aqistudy.cn/historydata/
思路:
首先实例化xpath对象,然后根据热门城市和全部城市的标签层级关系写出xpath表达式。解析表达式所对应的a标签,然后xpath函数返回一个列表,列表中存的就是a标签对应的城市。然后我们遍历列表即可。
# 热门城市
hot_city_list = tree.xpath('//div[@class="bottom"]/ul/li')
# 全部城市
all_city_list = tree.xpath('//div[@class="bottom"]/ul/div[2]/li')
我们无法只通过一共 xpath表达式,将两个层级标签都表示数量,但是我们可以将两个层级标签写在一起,只需要用按位或 “ | ” 进行分割。这个意味着将第一个 xpath表达式或者第二个 xpath表达式,作用到 xpath函数当中。这样可以解析第一个表达式所对应的a标签定位到,也可以将第二个表达式所对应的a标签定位到。
# 用按位或进行分割 “ | ”
a_city_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
xpath返回一个列表,这个列表里面存的是热门城市加全部城市a标签所对应的一个列表。
然后遍历列表即可。
代码实现:
第一种写法:分别解析热门城市和所有城市,然后把这些城市的名字存入列表中。
import requests
from lxml import etree
import os
if __name__ == '__main__':
url = 'https://www.aqistudy.cn/historydata/'
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 爬取页面源码数据 获取相应对象
page_text = requests.get(url=url,headers=header).text
# 数据解析
# 实例化对象
tree = etree.HTML(page_text)
all_city = [] #所有的城市
# 热门城市
hot_city_list = tree.xpath('//div[@class="bottom"]/ul/li')
for li in hot_city_list:
hot_city_name = li.xpath('./a/text()')[0]
all_city.append(hot_city_name)
#print(hot_city_name)
# 全部城市
all_city_list = tree.xpath('//div[@class="bottom"]/ul/div[2]/li')
for li in all_city_list:
all_city_name = li.xpath('./a/text()')[0]
all_city.append(all_city_name)
#print(all_city_name)
print(all_city," 一共有:",len(all_city),"个城市")
第二种写法:用按位或将两个层级关系连接。
import requests
from lxml import etree
if __name__ == '__main__':
url = 'https://www.aqistudy.cn/historydata/'
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 爬取页面源码数据 获取相应对象
page_text = requests.get(url=url,headers=header).text
# 数据解析
# 实例化对象
tree = etree.HTML(page_text)
all_city = [] #所有的城市
# 解析到热门城市和所有城市对应的a标签
# 热门城市对应a标签层级关系://div[@class="bottom"]/ul/li/a
# 所有城市对应a标签层级关系://div[@class="bottom"]/ul/div[2]/li/a
# 用按位或进行分割 “ | ”
a_city_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
for a in a_city_list:
city_name = a.xpath('./text()')[0]
all_city.append(city_name)
print(all_city," 一共有:",len(all_city),"个城市")
案例五
需求:
获取大学生求职简历模板的封面和名称,并且打印下来。
网址:http://sc.chinaz.com/jianli/daxuesheng.html
思路:
首先实例化xpath对象,然后根据图片和名称的标签层级关系写出xpath表达式。解析表达式所对应的a标签,然后xpath函数返回一个列表,列表中存的就是a标签对应的图片和名字。然后我们遍历列表即可。
当然我们发现每个模板的名字(中文信息)打印出来是乱码,这就需要我们更改编码格式。由于统用处理解决中文乱码的解决方法会报错,无法正常使用,所以我们使用手动设置响应数据编码格式。
reponse = requests.get(url=url,headers=header)
# 手动设置响应数据编码格式
reponse.encoding = 'utf-8'
page_text = reponse.text
我们如何打印所有页的数据呢?
我们观察发现,模板只有13页
而网址也很相似:
第一页:http://sc.chinaz.com/jianli/daxuesheng.html
第二页:http://sc.chinaz.com/jianli/daxuesheng_2.html
第三页:http://sc.chinaz.com/jianli/daxuesheng_3.html
第四页:http://sc.chinaz.com/jianli/daxuesheng_4.html
...
第十三页:http://sc.chinaz.com/jianli/daxuesheng_13.html
我们观察发现除了第一页外其它12页的url只有后面的数字不同,我们可以用字符串拼接的方法将url拼接出来,而那个不同的数字用for循环即可,然后将循环变量强制转换成str型的,最后拼接字符串。而第一页的特殊处理,独立输出,其他页(2到13页)的循环输出。
代码实现:
import requests
from lxml import etree
if __name__ == '__main__':
header = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4209.2 Safari/537.36'
}
# 第一页
# 第一页的url和其它页的有所不同
url1 ='http://sc.chinaz.com/jianli/daxuesheng.html'
# 爬取页面源码数据 获取相应对象
reponse = requests.get(url=url1, headers=header)
# 手动设置响应数据编码格式
reponse.encoding = 'utf-8'
page_text = reponse.text
# 数据解析
# 实例化对象
tree = etree.HTML(page_text)
src_list = tree.xpath('//div[@id="main"]/div/div')
for a in src_list:
img_src = a.xpath('./a/img/@src')[0]
img_alt = a.xpath('./a/img/@alt')[0]
# 统用处理解决中文乱码的解决方法
# 会报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0xbf in position 32: incomplete multibyte sequence
# img_alt = img_alt.encode('iso-8859-1').decode('gbk')
print(img_alt, ' : ', img_src)
# 其他页(2到13页)
url = 'http://sc.chinaz.com/jianli/daxuesheng'
for num in range(2,13+1):
num = str(num)
new_url = url + '_' + num + '.html'
# 爬取页面源码数据 获取相应对象
reponse = requests.get(url=new_url, headers=header)
# 手动设置响应数据编码格式
reponse.encoding = 'utf-8'
page_text = reponse.text
# 数据解析
# 实例化对象
tree = etree.HTML(page_text)
src_list = tree.xpath('//div[@id="main"]/div/div')
for a in src_list:
img_src = a.xpath('./a/img/@src')[0]
img_alt = a.xpath('./a/img/@alt')[0]
# 统用处理解决中文乱码的解决方法
# 会报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0xbf in position 32: incomplete multibyte sequence
#img_alt = img_alt.encode('iso-8859-1').decode('gbk')
print(img_alt,' : ',img_src)
六、补充知识
爬取的中文数据出现乱码
解决方法一:
手动设置响应数据编码格式。
reponse = requests.get(url=url,headers=header)
# 手动设置响应数据编码格式
reponse.encoding = 'utf-8'
page_text = reponse.text
解决方法二:
找到发生乱码对应的数据,对该数据进行:encode(‘iso-8859-1’).decode(‘gbk’) 操作。
# 统用处理解决中文乱码的解决方法
img_alt = img_alt.encode('iso-8859-1').decode('gbk')
案例三和案例五就是对这两种处理中文乱码的方法的应用。
案例三:是方法二。统用处理解决中文乱码的解决方法。
案例五:是方法一。手动设置响应数据编码格式。
按位或分割标签
当两个页面数据的层级标签类似时,我们可以使用按位或“ | ”,进行标签分割。这样可以将多个 xpath表达式解析。之间是或的关系。
这个意味着将第一个 xpath表达式或者第二个 xpath表达式,作用到 xpath函数当中。这样可以解析第一个表达式所对应的a标签定位到,也可以将第二个表达式所对应的a标签定位到。
样例:
# 用按位或进行分割 “ | ”
a_city_list = tree.xpath('//div[@class="bottom"]/ul/li/a | //div[@class="bottom"]/ul/div[2]/li/a')
注:详细内容请看案例四