一、xpath初步认识
1、xpath介绍
XPath(XML Path Language)是一种用于在 XML 文档中定位节点的语言。它是一种在 XML 文档中导航和查询信息的语言,类似于在关系数据库中使用 SQL 查询数据。XPath 提供了一种灵活的方式来定位和处理 XML 文档中的元素和属性。
2、lxml的安装
lxml是Python的一个第三方解析库,支持HTML和XML解析,而且效率非常高,弥补了Python自带的xml标准库在XML解析方面的不足。
由于是第三方库,所以在使用 lxml 之前需要先安装:pip install lxml
from lxml import etree
# 将源码转化为能被XPath匹配的格式
selector=etree.HTML(源码)
# 返回为一列表
selector.xpath(表达式)
3、xpath解析原理
XPath 使用路径表达式来选取 XML 文档中的节点或节点集。这些路径表达式类似于文件系统中的路径,可以沿着元素和属性之间的层次结构前进,并选择所需的节点。XPath 也支持使用谓词来过滤和选择节点,以便更精确地定位目标节点。
二、xpath的语法
XPath 语法 : XPath 参考手册 ] - 在线原生手册 - php中文网
1、选取节点
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
在下面的表格中,我们已列出了一些路径表达式以及表达式的结果:
路径表达式 | 结果 |
---|---|
bookstore | 选取 bookstore 元素的所有子节点。 |
/bookstore | 选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book | 选取属于 bookstore 的子元素的所有 book 元素。 |
//book | 选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book | 选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang | 选取名为 lang 的所有属性。 |
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。 |
3、选取未知节点
XPath 通配符可用来选取未知的 XML 元素。
通配符 | 描述 |
---|---|
* | 匹配任何元素节点。 |
@* | 匹配任何属性节点。 |
node() | 匹配任何类型的节点。 |
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
/bookstore/* | 选取 bookstore 元素的所有子元素。 |
//* | 选取文档中的所有元素。 |
//title[@*] | 选取所有带有属性的 title 元素。 |
4、选取若干路径
通过在路径表达式中使用"|"运算符,您可以选取若干个路径。
在下面的表格中,我们列出了一些路径表达式,以及这些表达式的结果:
路径表达式 | 结果 |
---|---|
//book/title | //book/price | 选取 book 元素的所有 title 和 price 元素。 |
//title | //price | 选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price | 选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
(1)逻辑运算
//div[@id="head" and @class="s_down"] # 查找所有id属性等于head并且class属性等于s_down的div标签
//title | //price # 选取文档中的所有 title 和 price 元素,“|”两边必须是完整的xpath路径
(2)属性查询
//div[@id] # 找所有包含id属性的div节点
//div[@id="maincontent"] # 查找所有id属性等于maincontent的div标签
//@class
//li[@name="xx"]//text() # 获取li标签name为xx的里面的文本内容
(3)获取第几个标签 索引从1开始
tree.xpath('//li[1]/a/text()') # 获取第一个
tree.xpath('//li[last()]/a/text()') # 获取最后一个
tree.xpath('//li[last()-1]/a/text()') # 获取倒数第二个
(4)模糊查询
//div[contains(@id, "he")] # 查询所有id属性中包含he的div标签
//div[starts-with(@id, "he")] # 查询所有id属性中包以he开头的div标签
//div/h1/text() # 查找所有div标签下的直接子节点h1的内容
//div/a/@href # 获取a里面的href属性值
//* #获取所有
//*[@class="xx"] #获取所有class为xx的标签
# 获取节点内容转换成字符串
c = tree.xpath('//li/a')[0]
result=etree.tostring(c, encoding='utf-8')
print(result.decode('UTF-8'))
5、示例
from lxml import etree
doc = '''
<html>
<head>
<base href='http://example.com/' /> <!-- 设置基准链接 -->
<title>Example website</title> <!-- 设置网页标题 -->
</head>
<body>
<div id='images'>
<a href='image1.html' id='lqz'>Name: My image 1 <br /><img src='image1_thumb.jpg' /></a>
<a href='image2.html'>Name: My image 2 <br /><img src='image2_thumb.jpg' /></a>
<a href='image3.html'>Name: My image 3 <br /><img src='image3_thumb.jpg' /></a>
<a href='image4.html'>Name: My image 4 <br /><img src='image4_thumb.jpg' /></a>
<a href='image5.html' class='li li-item' name='items'>Name: My image 5 <br /><img src='image5_thumb.jpg' /></a>
<a href='image6.html' name='items'><span><h5>test</h5></span>Name: My image 6 <br /><img src='image6_thumb.jpg' /></a>
</div>
</body>
</html>
'''
# 将HTML字符串转为可解析的对象
html = etree.HTML(doc)
# 1. 获取所有节点
all_nodes = html.xpath('//*')
print(all_nodes)
# 2. 指定节点(结果为列表)
head_node = html.xpath('//head')
print(head_node)
# 3. 子节点和子孙节点
child_nodes = html.xpath('//div/a') # 获取div下的所有a标签
descendant_nodes = html.xpath('//body//a') # 获取body下的所有子孙a标签
print(child_nodes)
print(descendant_nodes)
# 4. 父节点
parent_node = html.xpath('//body//a[1]/..') # 获取第一个a标签的父节点
print(parent_node)
# 5. 属性匹配
matched_nodes = html.xpath('//body//a[@href="image1.html"]') # 获取href属性为"image1.html"的a标签
print(matched_nodes)
# 6. 文本获取
text = html.xpath('//body//a[@href="image1.html"]/text()') # 获取第一个a标签的文本内容
print(text)
# 7. 属性获取
href_attributes = html.xpath('//body//a/@href') # 获取所有a标签的href属性值
print(href_attributes)
# 8. 属性多值匹配
li_class_nodes = html.xpath('//body//a[contains(@class, "li")]') # 获取class属性包含"li"的a标签
print(li_class_nodes)
# 9. 多属性匹配
matched_nodes = html.xpath('//body//a[contains(@class, "li") and @name="items"]') # 获取class属性包含"li"和name属性为"items"的a标签
print(matched_nodes)
# 10. 按序选择
second_a_text = html.xpath('//a[2]/text()') # 获取第二个a标签的文本内容
print(second_a_text)
# 11. 节点轴选择
ancestors = html.xpath('//a/ancestor::*') # 获取a标签的所有祖先节点
div_ancestor_node = html.xpath('//a/ancestor::div') # 获取a标签的祖先节点中的div
attribute_values = html.xpath('//a[1]/attribute::*') # 获取第一个a标签的所有属性值
child_nodes = html.xpath('//a[1]/child::*') # 获取第一个a标签的所有子节点
descendant_nodes = html.xpath('//a[6]/descendant::*') # 获取第六个a标签的所有子孙节点
following_nodes = html.xpath('//a[1]/following::*') # 获取第一个a标签之后的所有节点
following_sibling_nodes = html.xpath('//a[1]/following-sibling::*') # 获取第一个a标签之后的同级节点
print(ancestors)
print(div_ancestor_node)
print(attribute_values)
print(child_nodes)
print(descendant_nodes)
print(following_nodes)
print(following_sibling_nodes)
XPath 是一种强大的工具,广泛用于 XML 文档的处理和解析。它在各种领域中都有广泛的应用,包括 Web 开发、数据抓取、数据提取和数据转换等方面。在 Web 开发中,XPath 经常与 XML、HTML 和 XSLT(Extensible Stylesheet Language Transformations)一起使用,用于从网页中提取数据或进行数据转换。
三、项目实例
1、实例一
需求:
爬取58同城二手房源信息(以北京市为例)。解析出所有房源的名称,并进行持久化存储。
网址:https://bj.58.com/ershoufang/
思路:
主要就是观察页面的结构,看每个房源的名字所在的标签是哪个。然后写出xpath表达式即可。
import requests
from lxml import etree
from fake_useragent import UserAgent
url = 'https://bj.58.com/ershoufang/'
headers = {
"User-Agent": UserAgent().random
}
# 爬取页面源码数据
page_text = requests.get(url=url, headers=headers).text
# 页面解析
tree = etree.HTML(page_text)
div_list = tree.xpath('//*[@id="esfMain"]/section/section[3]/section[1]/section[2]/div')
with open('58.txt', 'w',encoding='utf8') as f:
for div in div_list:
# 局部解析
# 一定要加 . 这个 . 表示的就是局部定位到的标签
title = div.xpath('./a/div[2]/div[1]/div[1]/h3')[0].text
print(title)
# 存入文件
f.write(title + '\n')
2、案例二
需求:
爬取《红楼梦》所有章节的标题。
网址:https://www.shicimingju.com/book/hongloumeng.html
思路:
主要是对xpath表达式的书写。通过观察标签写出xpath表达式。
import requests
from lxml import etree
from fake_useragent import UserAgent
url = 'https://www.shicimingju.com/book/hongloumeng.html'
headers = {
"User-Agent":UserAgent().random
}
page_text = requests.get(url=url,headers=headers).text
# 实例化etree对象
tree = etree.HTML(page_text)
li_list = tree.xpath('//*[@id="main_left"]/div/div[4]/ul/li')
with open('红楼梦.txt', 'w',encoding='utf8') as f:
data_dic = []
for li in li_list:
title = li.xpath('./a/text()')[0] # 获取标签里的文本值
href = li.xpath('./a/@href')[0] # 获取标签里的href值
detail_url = 'https://www.shicimingju.com' + href
detail_page_text = requests.get(url=detail_url,headers=headers)
detail_page_text.encoding = 'utf8'
detail_page_text = detail_page_text.text
new_tree = etree.HTML(detail_page_text)
# 文章内容
p_list = new_tree.xpath('//*[@id="main_left"]/div[1]/div/p')
for p in p_list:
words = p.text
print(words)
data_dic.append({"title":title,"words":words})
f.write(str(data_dic))
3、实例三
需求:
解析下载图片数据。
网址:https://pic.netbian.com/4kdongman/
思路:
主要是对 xpath表达式的书写,和怎样处理中文乱码。
xpath表达式可以从< div class = “slist”>标签开始,也可以从更上面的标签开始,比如< idv id = “main” > 可以从这里开始。
当然两个写法的含义是一样的。
import requests
from lxml import etree
from fake_useragent import UserAgent
import os
if __name__ == '__main__':
url = 'https://pic.netbian.com/4kdongman/'
header = {
'User-Agent': UserAgent().random
}
# 爬取页面源码数据 获取相应对象
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)
4、案例四
需求:
解析出所给网址中全国城市的名称。
网址:https://www.aqistudy.cn/historydata/
思路:
首先实例化xpath对象,然后根据热门城市和全部城市的标签层级关系写出xpath表达式。解析表达式所对应的a标签,然后xpath函数返回一个列表,列表中存的就是a标签对应的城市。然后我们遍历列表即可。
第一种写法:分别解析热门城市和所有城市,然后把这些城市的名字存入列表中。
import requests
from lxml import etree
import os
from fake_useragent import UserAgent
url = 'https://www.aqistudy.cn/historydata/'
headers = {
'User-Agent':UserAgent().random
}
# 爬取页面源代码数据,获取响应对象
page_text = requests.get(url=url,headers=headers).text
# 数据解析
# 实例化对象
tree = etree.HTML(page_text)
all_city = [] # 所有的城市
# 热门城市
# hot_city_list = tree.xpath('//div[@class="bottom"]/ul/li')
hot_city_list = tree.xpath('/html/body/div[3]/div/div[1]/div[1]/div[2]/ul/li')
for li in hot_city_list:
hot_city_name = li.xpath('./a/text()')[0]
all_city.append(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,"一共有:",len(all_city),"个城市")
第二种写法:用按位或将两个层级关系连接。
我们无法只通过一共 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
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), "个城市")
5、案例五
需求:
获取大学生求职简历模板的封面和名称,并且打印下来。
网址:http://sc.chinaz.com/jianli/daxuesheng.html
思路:
首先实例化xpath对象,然后根据图片和名称的标签层级关系写出xpath表达式。解析表达式所对应的a标签,然后xpath函数返回一个列表,列表中存的就是a标签对应的图片和名字。然后我们遍历列表即可。
import requests
from fake_useragent import UserAgent
from lxml import etree
if __name__ == '__main__':
header = {
'User-Agent': UserAgent().random
}
# 第一页
# 第一页的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
- 我们如何打印所有页的数据呢?
- 我们观察发现,模板只有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页)的循环输出。
四、乱码解决办法
解决方法一:
手动设置响应数据编码格式。
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')
案例三和案例五就是对这两种处理中文乱码的方法的应用。
案例三:是方法二。统用处理解决中文乱码的解决方法。
案例五:是方法一。手动设置响应数据编码格式。