avatar


2.解析内容提取数据

在上一章,我们讨论了HTTP中Request Headers和Response Headers,然后,我们讨论的所有的内容都是如何发出恰好好处的请求,以此拿到恰好好处的响应。
现在假设我们的已经成功的发出了恰好好处的请求,拿到了恰好好处的响应,但是响应中的内容肯定不会都是我们想要的。
比如,在猪价格网中。
猪价格
我们只想要时间、地点、种类和价格,但是这个网页呢,还给了许多我们不想要的内容,比如:广告。
接下来,我们讨论如何解析内容提取数据。

响应内容的分类

通常我们拿到的响应内容有四种:

  1. json
  2. html
  3. xml
  4. jpgpngmp3

其中jpgpngmp3等,这些都不用解析内容提取数据,直接下载就行了。
示例代码:

1
2
3
4
5
6
import requests

response = requests.get("https://kakawanyifan.com/img/avatar.png")

with open("a.png", "wb") as f:
f.write(response.content)

我们主要讨论前面三种。

json

比如,我们这个博客,其中的评论数据,就是json格式的。
json
主要依赖的解析工具有:

  1. json
  2. jsonpath

Chrome的小技巧
如图所示,在Chrome浏览的开发者工具中

  1. ALL:所有
  2. XHR:ajax请求
  3. JS:JS文件
  4. CSS:CSS文件
  5. Img:图片
  6. Media:媒体
  7. Font:字体

我们可以通过点击这些tab,进行快速的筛选。也可以同时按住Ctrl键,进行多选。

html

比如,我们这个博客的首页,就是html格式的。
html
主要的解析工具有

  1. re:正则
  2. lxml:xpath语法
  3. pyquery:css选择器
  4. beautiful:同时支持正则、xpath语法、css选择器,所以也因为不够专注,不够专业,在性能方面,不及前3种。

xml

还有一种是xml格式。
这种格式现在更多是用作配置文件,作为前后端交互的一种方式,已经很少见了。
在我们这个博客中,用于SEO优化的站点地图sitemap.xml,就是xml格式的。
xml

xml和html非常的相似。
xml是可扩展标记语言,为了传输和存储数据,侧重点是传输存储。在xml中,标签可以自行定义。
html是超文本标记语言,为了更好的显示数据,侧重点是为了显示。在html中,标签不可以自行定义,有特定的标签,具有特定的含义。

但两者都是以标签的形式进行存储,所以两者的具体解析工具有相通之处。
主要的解析工具有

  1. re:正则
  2. lxml:xpath语法

所以,在接下来,我们不会单独讨论xml的解析。

JSON数据的解析提取

我们以豆瓣电影为例,讨论讨论JSON数据的解析提取。

分析

我们点开豆瓣的网站,选中到高分电影。观察请求,会发现这么一个请求

https://movie.douban.com/j/search_subjects?type=movie&tag=豆瓣高分&sort=recommend&page_limit=20&page_start=0

这个请求的返回就是电影数据,同时我们观察这个请求的Request Headers,特别的,我们发现有Referer,在上一章,我们讨论过,Referer用来表明Client客户端是从该Referer页面发起的请求,且Referer的值是https://movie.douban.com/explore,那么,这个字段极有可能被用作反爬虫的一个参考。

而且,我们发现似乎并没有类似签名的内容。所以,我们也不需要考虑签名的事情。
相反的,在我们博客的评论页面,我们发现为了评论数据的安全,居然还设置了一个id,一个sign
签名
如果签名不对的话,返回的是

1
{"code":401,"error":"Unauthorized."}

所以,对于我们博客的评论数据,要爬取的难度就较大了,首先要先破解签名算法。但在豆瓣中,不需要。

此外,在豆瓣的请求中,我们发现有五个参数

1
2
3
4
5
type: movie
tag: 豆瓣高分
sort: recommend
page_limit: 20
page_start: 0

而且,如果我们在豆瓣的网页上点击加载更多更多的话
加载更多
会发现参数变成了

1
2
3
4
5
type: movie
tag: 豆瓣高分
sort: recommend
page_limit: 20
page_start: 20

这样,我们就发现了豆瓣电影翻页的方法。
甚至,我们可以把page_limit设置大一点的话,这样我们可以少翻几页。但为了防止被反爬虫,我们要尽量装浏览器装得像一点,就设置成20。
特别的,我们发现,在整个翻页过程中,cookie也没有变化,我们把cookie也带上。

而且,我们发现,其他参数也没变化,所以索性都带上。

解析

接下来,我们就来解析内容提取数据

基于JSON解析

JSON的四个方法

如果要解析JSON,第一步就是把Response的返回,转换成json,这里介绍两个方法:

  1. json.load()
  2. json.loads()

json.load()
官方介绍是

Deserialize ‘‘fp’’ (a ‘’.read()‘’-supporting file-like object containing a JSON document) to a Python object.
即:把类文件文件对象反序列化成Python对象

json.loads()
官方介绍是

Deserialize ‘‘s’’ (a ‘‘str’’, ‘‘bytes’’ or ‘‘bytearray’’ instance containing a JSON document) to a Python object.
即:把一个字符串对象反序列化成Python对象

与上面两个方法相对应的序列化方法

  1. json.dump()
  2. json.dumps()

毫无疑问,我们这里应该用json.loads()

response.json()

特别的,我们还有一个方法:

1
response.json()
  • 但是有些JSON可能用压缩算法进行了压缩,所以还是需要json.loads()方法。

解析JSON

在Python中,json的类型是dict或list。
JSONObject的类似是dict,JSONArray的类型是list。
关于如何读取dict和list中的元素,我们不讨论。

我们以只爬取第一页的数据为例。翻页的话,只需要修改参数。此外,在翻页过程中,我们可以控制每次爬取间隔的时间,防止被反爬虫
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import requests
import json
# 为了打印json好看一点
import pprint

headers = {'Host': 'movie.douban.com',
'Connection': 'keep-alive',
'Accept': '*/*',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36',
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Referer': 'https://movie.douban.com/explore',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
'Cookie': 'll="108296"; bid=noH33Ykdfmk; ap_v=0,6.0; __utmc=30149280; __utmz=30149280.1614170174.1.1.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; __utmc=223695111; __utmz=223695111.1614170174.1.1.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; __yadk_uid=Ml4HFDvUuxK0kn89ZrqV5FC8U4wBdQHt; _vwo_uuid_v2=D05D38A9E441CD39BAF136B2B379F413D|668a6aec9ef0ee0ca1d42703dc95a4e4; ct=y; __gads=ID=425c7384e2d8fac1-22f23fa221c600be:T=1614170242:RT=1614170242:S=ALNI_MbIBQ0LIllOmgDPaTLPQpMO6qYhMA; _pk_ref.100001.4cf6=%5B%22%22%2C%22%22%2C1614173468%2C%22https%3A%2F%2Fwww.baidu.com%2Flink%3Furl%3D9IuszM9UXMoB3uqWQ60Vyh-HVG0IgmIvN2b22xvYLj_m8fHq7-Wq1aZ9d6xEk_oY%26wd%3D%26eqid%3Dd08fd63b0003d5c8000000046036483c%22%5D; _pk_ses.100001.4cf6=*; __utma=30149280.865532167.1614170174.1614170174.1614173468.2; __utmb=30149280.0.10.1614173468; __utma=223695111.2146551022.1614170174.1614170174.1614173468.2; __utmb=223695111.0.10.1614173468; _pk_id.100001.4cf6=686e8e370c3971d2.1614170173.2.1614175148.1614170239.'}
params = {'type': 'movie', 'tag': '豆瓣高分', 'sort': 'recommend', 'page_limit': '20', 'page_start': '0'}
response = requests.get('https://movie.douban.com/j/search_subjects', headers=headers, params=params)
print(response.headers)
j = json.loads(response.content.decode())
# 为了打印json好看一点
pprint.pprint(j)

print(type(j))
print(type(response.json()))
print(type(j['subjects']))
print(j['subjects'][0]['title'])

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
{'Date': 'Wed, 24 Feb 2021 14:24:27 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Transfer-Encoding': 'chunked', 'Connection': 'keep-alive', 'Keep-Alive': 'timeout=30', 'Vary': 'Accept-Encoding, Accept-Encoding', 'X-Xss-Protection': '1; mode=block', 'X-Douban-Mobileapp': '0', 'Expires': 'Sun, 1 Jan 2006 01:00:00 GMT', 'Pragma': 'no-cache', 'Cache-Control': 'must-revalidate, no-cache, private', 'X-DAE-App': 'movie', 'X-DAE-Instance': 'default', 'Server': 'dae', 'X-Content-Type-Options': 'nosniff', 'Content-Encoding': 'br'}
{'subjects': [{'cover': 'https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2561305376.jpg',
'cover_x': 2810,
'cover_y': 3937,
'episodes_info': '',
'id': '26752088',
'is_new': False,
'playable': True,
'rate': '9.0',
'title': '我不是药神',
'url': 'https://movie.douban.com/subject/26752088/'},
{'cover': 'https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2563780504.jpg',
'cover_x': 5594,
'cover_y': 8268,
'episodes_info': '',
'id': '26794435',
'is_new': False,
'playable': True,
'rate': '8.4',
'title': '哪吒之魔童降世',
'url': 'https://movie.douban.com/subject/26794435/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2632682629.jpg',
'cover_x': 1813,
'cover_y': 2560,
'episodes_info': '',
'id': '35327724',
'is_new': True,
'playable': False,
'rate': '8.1',
'title': '鬼灭之刃 柱众会议・蝶屋敷篇',
'url': 'https://movie.douban.com/subject/35327724/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2614625312.jpg',
'cover_x': 1688,
'cover_y': 2500,
'episodes_info': '',
'id': '23044161',
'is_new': True,
'playable': False,
'rate': '8.2',
'title': '飞哥与小佛大电影:坎迪斯对抗宇宙',
'url': 'https://movie.douban.com/subject/23044161/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p480747492.jpg',
'cover_x': 2000,
'cover_y': 2963,
'episodes_info': '',
'id': '1292052',
'is_new': False,
'playable': True,
'rate': '9.7',
'title': '肖申克的救赎',
'url': 'https://movie.douban.com/subject/1292052/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2614500649.jpg',
'cover_x': 1080,
'cover_y': 1663,
'episodes_info': '',
'id': '25662329',
'is_new': False,
'playable': True,
'rate': '9.2',
'title': '疯狂动物城',
'url': 'https://movie.douban.com/subject/25662329/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2557573348.jpg',
'cover_x': 1080,
'cover_y': 1560,
'episodes_info': '',
'id': '1291561',
'is_new': False,
'playable': True,
'rate': '9.4',
'title': '千与千寻',
'url': 'https://movie.douban.com/subject/1291561/'},
{'cover': 'https://img9.doubanio.com/view/photo/s_ratio_poster/public/p457760035.jpg',
'cover_x': 2015,
'cover_y': 3000,
'episodes_info': '',
'id': '1292722',
'is_new': False,
'playable': True,
'rate': '9.4',
'title': '泰坦尼克号',
'url': 'https://movie.douban.com/subject/1292722/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2401676338.jpg',
'cover_x': 1414,
'cover_y': 2048,
'episodes_info': '',
'id': '26387939',
'is_new': False,
'playable': True,
'rate': '9.0',
'title': '摔跤吧!爸爸',
'url': 'https://movie.douban.com/subject/26387939/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2572166063.jpg',
'cover_x': 5906,
'cover_y': 8268,
'episodes_info': '',
'id': '30166972',
'is_new': False,
'playable': True,
'rate': '8.3',
'title': '少年的你',
'url': 'https://movie.douban.com/subject/30166972/'},
{'cover': 'https://img3.doubanio.com/view/photo/s_ratio_poster/public/p511118051.jpg',
'cover_x': 658,
'cover_y': 980,
'episodes_info': '',
'id': '1295644',
'is_new': False,
'playable': False,
'rate': '9.4',
'title': '这个杀手不太冷',
'url': 'https://movie.douban.com/subject/1295644/'},
{'cover': 'https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2614500706.jpg',
'cover_x': 1080,
'cover_y': 1512,
'episodes_info': '',
'id': '20495023',
'is_new': False,
'playable': True,
'rate': '9.1',
'title': '寻梦环游记',
'url': 'https://movie.douban.com/subject/20495023/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2372307693.jpg',
'cover_x': 1872,
'cover_y': 2546,
'episodes_info': '',
'id': '1292720',
'is_new': False,
'playable': True,
'rate': '9.5',
'title': '阿甘正传',
'url': 'https://movie.douban.com/subject/1292720/'},
{'cover': 'https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2561716440.jpg',
'cover_x': 600,
'cover_y': 889,
'episodes_info': '',
'id': '1291546',
'is_new': False,
'playable': True,
'rate': '9.6',
'title': '霸王别姬',
'url': 'https://movie.douban.com/subject/1291546/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2549177902.jpg',
'cover_x': 2000,
'cover_y': 3167,
'episodes_info': '',
'id': '27060077',
'is_new': False,
'playable': True,
'rate': '8.9',
'title': '绿皮书',
'url': 'https://movie.douban.com/subject/27060077/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p501177648.jpg',
'cover_x': 986,
'cover_y': 1466,
'episodes_info': '',
'id': '3319755',
'is_new': False,
'playable': True,
'rate': '9.1',
'title': '怦然心动',
'url': 'https://movie.douban.com/subject/3319755/'},
{'cover': 'https://img2.doubanio.com/view/photo/s_ratio_poster/public/p2616355133.jpg',
'cover_x': 4500,
'cover_y': 6563,
'episodes_info': '',
'id': '3541415',
'is_new': False,
'playable': True,
'rate': '9.3',
'title': '盗梦空间',
'url': 'https://movie.douban.com/subject/3541415/'},
{'cover': 'https://img3.doubanio.com/view/photo/s_ratio_poster/public/p579729551.jpg',
'cover_x': 3030,
'cover_y': 4364,
'episodes_info': '',
'id': '3793023',
'is_new': False,
'playable': True,
'rate': '9.2',
'title': '三傻大闹宝莱坞',
'url': 'https://movie.douban.com/subject/3793023/'},
{'cover': 'https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2395733377.jpg',
'cover_x': 1428,
'cover_y': 2000,
'episodes_info': '',
'id': '26683290',
'is_new': False,
'playable': True,
'rate': '8.4',
'title': '你的名字。',
'url': 'https://movie.douban.com/subject/26683290/'},
{'cover': 'https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2626308994.jpg',
'cover_x': 6611,
'cover_y': 9435,
'episodes_info': '',
'id': '24733428',
'is_new': False,
'playable': False,
'rate': '8.9',
'title': '心灵奇旅',
'url': 'https://movie.douban.com/subject/24733428/'}]}
<class 'dict'>
<class 'dict'>
<class 'list'>
我不是药神

基于jsonpath的解析

在上面豆瓣的例子中,我们拿到了一个非常漂亮,非常容易解析的json。
但是,如果我们遇到一位技艺拙劣的程序员呢?拿到了一个非常不好解析的json。(虽然有时候是产品经历要求程序员这么做的)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
d = {
'a': {
'b': {
'c': {
'd': {
'e': [{
'f': {
'g': {
'h': {
'i': {
'j': 'kaka'
}
}
}
}
}]
}
}
}
}
}
print(d['a']['b']['c']['d']['e'][0]['f']['g']['h']['i']['j'][0])

运行结果:

1
kaka

如果这个json再复杂一点点,眼睛都要花掉。
所以,接下来,我们讨论jsonpath
jsonpath

jsonpath的官方介绍是An XPath for Json
那么什么是XPath呢?我们在讨论HTML和XML的解析方法的时候会讨论。

我们先直接看jsonpath中最常见的三个方法

  1. $:跟节点
  2. .:子节点
  3. ..:任意位置,符合条件的节点

关于更多的方法,大家可以参考如下的地址

https://goessner.net/articles/JsonPath/

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from jsonpath import jsonpath

d = {
'a': {
'b': {
'c': {
'd': {
'e': [{
'f': {
'g': {
'h': {
'i': {
'j': 'kaka'
}
}
}
}
}]
}
}
}
}
}

# jsonpath的结果是列表
print(jsonpath(d, '$.a.b.c.d.e.0.f.g.h.i.j'))

运行结果:

1
['kaka']

或许这个方法还是不够简洁,毕竟我们还是一层一层的数了,只是少写了点代码。
看这个
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from jsonpath import jsonpath

d = {
'a': {
'b': {
'c': {
'd': {
'e': [{
'f': {
'g': {
'h': {
'i': {
'j': 'kaka'
}
}
}
}
}]
}
}
}
}
}

# jsonpath的结果是列表
print(jsonpath(d, '$..j'))

运行结果:

1
['kaka']

jsonpath中,返回的都是list类型。

HTML数据的解析提取

我们以博客为例,讨论讨论HTML数据的解析提取。
我们请求我们的博客,发现文章的相关数据并不是以json形式进行传递的,而是原本就存在于html中。

分析

我们观察我们的网页,发现每一篇文章都在一个<div class="recent-post-item">中。

再观察,会发现<div class="recent-post-info">中的a标签,就有文章的标题和地址信息。
a

<div class="article-meta-wrap"><span class="article-meta">有文章的分类信息。

<time class="post-meta__date">中有文章的发布以及更新时间。

XPath

所以,我们需要解析这个HTML。
怎么解析?
我们从根节点出发,一层一层get,最后获取到我们想要的内容。太麻烦了!
我们在上一章讨论jsonpath的时候,特别提到了其官方介绍An XPath for JSON。然后,我们也见识到了jsonpath的神奇魔力,快速定位到我们想要的元素。
现在,我们就来看看什么是XPath。

XPath Helper

工欲善其事,必先利其器。
首先,我们安装一个Chrome的插件,XPath Helper
接下来,我们会利用这个插件讨论XPath,一边讨论,一边实践。

节点

首先,我们讨论什么是XPath中的节点。
每个html、xml的标签我们都称之为节点,其中最顶层的节点称为根节点。
以我们博客的首页为例,其根节点是html,然后还有两个子节点,分别是headbody
html

节点选择语法

“人如其名”,XPath使用路径表达式(Path)来选取文档中的节点或者节点集。

表达式 描述
nodename 选中该元素。
/ 从根节点选取、或者是元素和元素间的过渡。
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
. 选取当前节点。
.. 选取当前节点的父节点。
@ 选取属性。
text() 选取文本。

我们来看几个具体的例子。

/:从根节点选取、或者是元素和元素间的过渡。

例如,我们获取我们主页的浏览器标签的title。
我们从跟节点出发/,先选中html,再选中head,最后选中title
示例代码:

1
/html/head/title
运行结果:

/html/head/title

  • 使用XPath Helper选择标签时候,选中的标签会添加属性class="xh-highlight"

//:从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。
当然,我们也可以//开头。
实力代码:

1
//title
运行结果:

//title

..:选取当前节点的父节点。
我们在上例中添加..,回到head节点,再添加..,回到html节点。
然后我们又//title,重新回到title。
示例代码:

1
//title/../..//title
运行结果:

//title/../..//title

text():选取文本。
特别注意这时候的意思是选取文本内容,之前title标签的class="xh-highlight"没有了。
示例代码:

1
//title/../..//title/text()
运行结果:

//title/../..//title/text()

@:选取属性。
我们以获取翻页地址为例,讨论选取属性。
示例代码:

1
//nav/div/a/@href
运行结果:

//nav/div/a/@href

节点修饰语法

讨论了那么多,我们来获取文章标题吧。
示例代码:

1
//main/div/div

运行结果:

//main/div

<div class="post_cover left_radius"><div class="recent-post-info">都是div
这时候久需要节点节点修饰语法了。

路径表达式 结果
//title[@lang="eng"] 选择lang属性值为eng的所有title元素
/bookstore/book[1] 选取属于bookstore子元素的第一个book元素。
/bookstore/book[last()] 选取属于bookstore子元素的最后一个book元素。
/bookstore/book[last()-1] 选取属于bookstore子元素的倒数第二个book元素。
/bookstore/book[position()>1] 选择bookstore下面的book元素,从第二个开始选择。
//book/title[text()='Harry Potter'] 选择所有book下的title元素,仅仅选择文本为Harry Potter的title元素
/bookstore/book[price>35.00]/title 选取bookstore元素中的book元素的所有title元素,且其中的price元素的值须大于35.00。

注意:

  • 在XPath中,第一个元素的位置是1

我们来看几个具体的例子。

通过索引修饰节点
示例代码:

1
//main/div[1]/div/div[2]/a/text()
运行结果:

//main/div[1]/div/div[2]/a/text()

通过属性修饰节点
还可以通过通过属性修饰节点。
示例代码:

1
//a[@class='article-title']/text()
运行结果:

//a[@class='article-title']/text()

通过多条件修饰节点
比如,我们把标题包含"爬虫及其Python实现"的文章选出来了。

and多条件修饰。

1
//a[@class='article-title' and contains(@title,'爬虫及其Python实现')]
* `contains(@title,'爬虫及其Python实现')`:包含

contains(@title,'爬虫及其Python实现')

特别的,我们也可以在contains()中加text()

1
//a[@class='article-title' and contains(text(),'爬虫及其Python实现')]

通配符

可以通过通配符来选取未知的html、xml的元素

通配符 描述
* 匹配任何元素节点。
node() 匹配任何类型的节点。

例如,我们把a换成*
示例代码:

1
//*[@class='article-title' and contains(text(),'爬虫及其Python实现')]

更多的XPath语法参考:https://www.w3school.com.cn/xpath/index.asp

lxml

在讨论了XPath之后,接下来就是应用了。
我们需要依赖lxml这个模块,这是一个第三方模块,需要额外按照。

方法

  1. 导入lxml 的 etree 库

    from lxml import etree

  2. 利用etree.HTML,将html字符串(bytes类型或str类型)转化为Element对象,Element对象具有xpath的方法,返回结果的列表

    1
    2
    html = etree.HTML(text) 
    ret_list = html.xpath("xpath语法规则字符串")
  3. xpath方法返回列表的三种情况

    • 返回空列表:根据xpath语法规则字符串,没有定位到任何元素
    • 返回由字符串构成的列表:xpath字符串规则匹配的一定是文本内容或某属性的值
    • 返回由Element对象构成的列表:xpath规则字符串匹配的是标签,列表中的Element对象可以继续进行xpath

例子

我们以获取我们博客的所有文章名称、文章地址、类别、发布日期和更新日期为例。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import requests
from lxml import etree

host = 'https://kakawanyifan.com'
url = 'https://kakawanyifan.com'
while True:
response = requests.get(url)
html = etree.HTML(response.content.decode())
# 文章标题
title = html.xpath("//a[@class='article-title']/@title")
# 文章地址
href = html.xpath("//a[@class='article-title']/@href")
# 类别
categories = html.xpath("//span[@class='article-meta__categories']/text()")
# 发布日期
created = html.xpath("//span[@class='post-meta__date-created']/text()")
# 更新日期
updated = html.xpath("//span[@class='post-meta__date-updated']/text()")

for i in range(len(title)):
print(title[i], href[i], categories[i * 2], categories[i * 2 + 1], created[i], updated[i])

next_page = html.xpath("//a[@rel = 'next']")
if len(next_page) == 1:
url = host + html.xpath("//a[@rel = 'next']/@href")[0]
else:
break

运行结果:

1
2
3
4
5
6
7
8
9
基于Hexo的博客搭建:11.修改Hexo /10111 计算机 基于Hexo的博客搭建 2021-02-26 2021-02-26
爬虫及其Python实现:1.发送请求获取响应 /10701 计算机 爬虫及其Python实现 2021-02-21 2021-02-21
算法入门经典(Java与Python描述):11.动态规划 /10611 计算机 算法入门经典(Java与Python描述) 2021-02-16 2021-02-16

【部分运行结果略】

基于Hexo的博客搭建:3.新建博客 /10103 计算机 基于Hexo的博客搭建 2020-04-21 2020-04-25
基于Hexo的博客搭建:2.准备工具 /10102 计算机 基于Hexo的博客搭建 2020-04-21 2020-04-25
基于Hexo的博客搭建:1.准备环境 /10101 计算机 基于Hexo的博客搭建 2020-04-21 2020-04-25
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/11302
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板