前排提醒!
本文并非是“零起点”的。阅读下列文档之前,请保证你具有以下的知识:
- 了解requests和grequests模块,并能够使用其建立HTTP请求;
- 了解回调函数的含义。
背景
进行爬虫操作时,程序员除了想要获取请求的结果之外,有时还想在请求过程中获取相关的信息(比如说在某个时刻已请求数据的长度),以完成某些任务(比如说下载进度条)。我们知道Python内置模块urllib下的方法urllib.request.urlretrieve
可以添加回调函数参数callback,但是在另外一个常用模块requests中,我们难以找到直接的回调函数参数,在国内互联网中也难以找到相关信息,但这不代表这个模块完不成这个任务。在此我查阅了一些资料和问答频道,并写成此文,以供参考。
研究
以下我们以添加GET请求的进度条的方式来展示改进方法。
使用的目标URL:http://wppkg.baidupcs.com/issue/netdisk/yunguanjia/BaiduNetdisk_6.8.9.1.exe(百度网盘Windows端安装包,大小约30MB,下载需时约10s)。
requests没有直接的callback参数(这一点待定),但是其具有一个参数“流”(stream)允许我们以流的形式进行请求。以流形式请求时,我们可以“一块”(chunk)“一块”地获取请求的内容,请求对象Response在此时不会再全部内容完全被获取时才能返回;并且Response支持生成器iter_content
的方式来返回请求的内容。由以上二点,我们可以构造一个新的请求函数,来做出回调函数的效果:
注:本方法源自Stack Overflow某条回答,源地址遗失。
def get_callback(url, data=None, chunk_size=1024, callback=None):
r = requests.get(url, data=data, stream=True) # Response请求,由于添加了stream,请求速度不会慢
length = r.headers.get('content-length') # 请求的总长度,便于回调
if length == None: # 有的请求不带上面的头参数,故要特判
return r.content # 直接返回
if not callback: # 如果回调函数没有给定
callback = lambda now, tot: None # 一个无用的函数,占位用,其中callback需要接受两个参数,分别对应已获取长度和总长度,后同
dl = 0 # 目前已获取的长度
length = int(length) # headers里取得的值都是字符串,要化成整数
res = b'' # 目前已获取的请求内容
for chunk in r.iter_content(chunk_size=chunk_size): # 每次取出一块,注意chunk_size在requests中默认为1,这里调大为1024,节约for循环的成本
dl += len(chunk)
res = b''.join([res, chunk]) # 这里没有直接加,速度更快
callback(dl, length) # 执行回调
return res # 最终给出请求的内容
测试结果如下:
In [63]: res = get_callback(url, None, 1048576, callback)
1048576 35984960
2097152 35984960
3145728 35984960
4194304 35984960
5242880 35984960
6291456 35984960
7340032 35984960
8388608 35984960
9437184 35984960
10485760 35984960
11534336 35984960
12582912 35984960
13631488 35984960
14680064 35984960
15728640 35984960
16777216 35984960
17825792 35984960
18874368 35984960
19922944 35984960
20971520 35984960
22020096 35984960
23068672 35984960
24117248 35984960
25165824 35984960
26214400 35984960
27262976 35984960
28311552 35984960
29360128 35984960
30408704 35984960
31457280 35984960
32505856 35984960
33554432 35984960
34603008 35984960
35651584 35984960
35984960 35984960
In [64]: len(res)
Out[64]: 35984960
如果我们需要时刻对某个变量赋值,而不是print,我们可以在callback中添加global语句,使回调函数可以修改外部全局变量。
但如果是第三方进度条模块(比如说progressbar),则可能需要对源代码改进较多,原因是进度条的实现需要将iter_content
视作某个函数的自变量,并且进度条模块通常对生成器不友好(原因是生成器迭代次数无法确定,也就无法判断最大循环次数。但是我们先前已经得到了chunk_size
和length
两个变量,而整个过程不包括其他的循环,故我们可以计算得出总共的循环次数:
max_value = int(math.ceil(length / chunk_size))
以此我们得出回调的改进版:
def get_callback(url, data=None, chunk_size=1024, callback=None):
r = requests.get(url, data=data, stream=True)
length = r.headers.get('content-length')
if length == None:
return r.content
if not callback:
callback = lambda now, tot: None
dl = 0
length = int(length)
res = b''
max_value = int(math.ceil(length / chunk_size))
bar = progressbar.ProgressBar(max_value=max_value) # 设定进度条的最大循环次数
for chunk in bar(r.iter_content(chunk_size=chunk_size)):
dl += len(chunk)
res = b''.join([res, chunk])
callback(dl, length)
return res
由于进度条并未占用callback参数,我们依然可以另添一个回调函数完成其他任务。
全文完。