动态网页(澎湃网)图片爬取

前言

上次完成了静态网页的爬取,这次来爬取动态加载的网页。

静态网页一般通过点击“下一页”来加载下一页的内容,一般来说 URL 都会随页数而产生相应的变化。

而所谓的动态网站,就是那些下拉自动加载或者点击“加载更多”来加载更多的网页内容,而且网页的 URL 不发生变化;

这种网页一般采用 AJAX 请求后端服务器,实现动态加载的效果。

AJAX 具体怎么工作的?还要慢慢了解,这里我只知道动态加载的效果是通过 AJAX 请求服务器完成的;

既然有请求,就肯定有请求链接,分析出请求链接之后,就可以通过 requests 模拟请求,从而实现数据的爬取。

爬虫目标

  • 爬取澎湃网美数课专栏的所有文章中的图片

地址:(https://www.thepaper.cn/list_25635)

image

爬取步骤

AJAX 请求 url 分析

首先 F12 进入开发者页面,选择 Network,再点击 XHR ;这个 XHR 就是 AJAX 的请求类型;

然后一直下拉网页,让它加载几次之后,就能看到出现了几个特殊的请求链接;

随便点击一个请求链接,就能看到这个请求的详细信息;里面的 Request URL 就是 AJAX 请求后台服务器的完整链接。

image

找到请求链接之后,来看一下有没有什么规律:

1
2
3
https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=2&isList=true&lastTime=1549194757315
https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=3&isList=true&lastTime=1545817966882
https://www.thepaper.cn/load_index.jsp?nodeids=25635&topCids=&pageidx=4&isList=true&lastTime=1542155759648

通过观察可以发现,请求链接只有 pageidxlastTime 两个参数在变化,前者显然是一页一页的增加的,后面那个完全没有规律好嘛?

索性,,,删掉试试;果然,那个 lastTime 参数并没有什么用处,删掉之后一样能得到返回的结果。

这样请求链接就变得相当有规律了,就能很方便的用 for 循环构造了;甚至那个 isList=true 都可以不要了;某一返回结果如下图:

image

返回结果分析

可以看到每篇文章的标题里都会有一个链接:

image

打开那个链接,就进入到了那篇文章的详情页面;

而且,对文章中的图片进行 检查,图片的源地址暴露无疑:

image

到这里就差不多了,总体思路就是:先模拟 AJAX 请求,在返回的结果页面中提取出详情页面的链接,然后再对详情页面中的图片地址进行提取,最后下载。

爬取结果

最后是下载到了一个本地的文件夹里:

image

完整代码

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
import requests
import bs4
import os
import re
import multiprocessing

Domain = 'https://www.thepaper.cn/'

# 爬取的动态页面
URL = 'https://www.thepaper.cn/list_25635'

# 请求头部
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
}

# 获取当前 url 的 html 文本
def get_html(url) :
response = requests.get(url, headers = headers)
if (response.status_code == 200) :
return response.text
else : return None

# 解析当前的 html 文本
def ParseThisPage(html) :
soup = bs4.BeautifulSoup(html, 'lxml')

lts = soup.find_all('h2')
for s in lts :
# yield 会返回一个可迭代的 生成器 (generator),
# 只能一次性迭代
yield {
'title': s.a.text,
# 详情页面的链接 (只有后缀)
'href': s.a['href']
}

# 获取当前 html 文本中的图片链接
def get_pic_urls(html) :
soup = bs4.BeautifulSoup(html, 'lxml')

if(soup.h1) :
# 取出 <h1>, 标题
title = soup.h1.text
# width 给出的是一个 list, 可以找到所有 width = "100%" 或 width = "600" 的 <img> 标签
lts = soup.find_all(name = 'img', width = ['100%', '600'])

for i in range(len(lts)) :
pic_url = lts[i].attrs['src']
# num 用来计数
yield {
'title': title,
'pic_url': pic_url,
'num': i
}

# 保存图片
def save_pics(pic) :
'''
:pic: 一个字典, 包括 {'title': 标题, 'pic_url': 图片链接, 'num': 编号}
'''

title = pic['title']
url = pic['pic_url']
num = pic['num']
# 用来去除一些文件名中不允许的字符
# 文章标题中有好多半角字符的 |
title = re.sub(r'[ \/:*?"<>||]', '', title).strip()
# print (title)

# 为当前这篇文章新建一个文件夹
if not os.path.exists(title) :
os.mkdir(title)

response = requests.get(url, headers = headers)
try:
if (response.status_code == 200) :
# 保存图片
file = '{0}/{1}.jpg'.format(title, num)
if not os.path.exists(file) :
with open(file, 'wb') as f :
f.write(response.content)
print('文章"{0}"的第{1}张图片下载完成'.format(title,num))
except Exception as e:
print ('Picture download failed')
return None


def Job(i) :
# 构造 AJAX 的请求 url
cur_url = Domain + 'load_index.jsp?nodeids=25635&topCids=&pageidx=' + str(i)
html = get_html(cur_url)
suffixs = ParseThisPage(html)

for item in suffixs :
# 解析详情页面
html = get_html(Domain + item['href'])
# 获取图片的 urls
pic_urls = get_pic_urls(html)
# 保存图片
for pic in pic_urls :
save_pics(pic)

'''
# 单进程, 耗时 1165.7s
if __name__ == '__main__' :
# 新建一个文件夹
if not os.path.exists('pengpai_pictures') :
os.mkdir('pengpai_pictures')
# 切换到那个文件夹下
os.chdir('./pengpai_pictures')

for i in range(1, 26) :
Job(i)
'''

# 多进程, 耗时 343.8s
if __name__ == '__main__' :
if not os.path.exists('pengpai_pictures') :
os.mkdir('pengpai_pictures')
os.chdir('./pengpai_pictures')

pl = multiprocessing.Pool()
pl.map(Job, [i for i in range(1, 26)])
pl.close()
pl.join()

外话

这里尝试了一下多进程爬取,效果非常明显,普通下载 1000+ s,多进程下载 300+ s;虽然我不太懂多进程具体是怎么回事,就冲着这个效果,也要学一学吧。

------ The happy endingThank you for reading ------
坚持原创技术分享,您的支持将鼓励我继续创作!