问题背景
将这次任务概括就是将300+张图片一一上传到一个网址,经过其后台服务器处理,生成一张高动态(HDR)图片并返回其下载链接。在这种重复性劳动的应用需求下,我们很容易想到利用爬虫代替我们人工,毕竟我们的时间是很宝贵的(hh,有这时间去打一把王者不好吗?< . >)。
言归正传,这是一个很基础的爬虫任务,代码量也少。但在实现过程中却遇到不少问题,特地在此记录一下。
开发环境
- win10 1903家庭版
- Python3.5、Jupyter notebook,Fiddler
python包
- requests
- beautifulSoup
- tqdm(主要用来实时更新下载进度)
实现过程中遇到的问题
1. 下载中断卡死
因为那个网址是个小站而且在国外,下载速度十分慢(平均6~7KB),而且经常中断,卡死。另一方面由于生成的HDR文件有2.7MB左右,可想而知下载一个文件需要多久。因此这就产生了一个问题,当下载到2.3M左右后突然中断时,人是会很奔溃的。于是想到能不能实现断点续传的功能,本着不要重复造轮子的想法,我Google了一下,果然有人已经实现了。参考百里、罗云这篇博文,将下载函数改进了一下。
但是再次尝试下载的时候,有时侯又会出现程序卡死无响应的问题。最后用了下面几个方法解决:
- 给
requests.get加上timeout参数,超时报错,再用try-except捕获继续下载下一文件。
s = requests.Session()
s.mount('http://',requests.adapters.HTTPAdapter(max_retries=3))
req = s.get(url, headers=head_range,proxies=pro, stream=True, timeout=20)
- 但这也存在一个问题,当分块下载(
iter_content函数)的时候卡死根本不会触发timeout,因为timeout只是控制从爬虫发出请求到目标服务器回复这段时间不超时。因此统计iter_content函数的运行时间,超过自己设定的时间则抛出一个异常。
Start = time.time()
for chunk in req.iter_content(chunk_size=1024):
if chunk:
Now = time.time()
if(Now - Start > 1000):
raise Timeerror("超时1000s")
f.write(chunk)
- 最后用一个
try except包裹。总的下载函数如下:
def download_from_url(url,dst, pro):
hdr_img = requests.get(url,headers=header, proxies=pro,stream=True)
file_size = int(hdr_img.headers["Content-length"])
if os.path.exists(dst):
first_byte = os.path.getsize(dst)
else:
first_byte = 0
print("待下载文件大小: ",file_size)
if file_size==42: #文件出错时会返回42byte的数据
return (False,file_size)
if first_byte >= file_size:
return (True,file_size)
#head_range = {"Range": "bytes={"+ str(first_byte) + "}-{" + str(file_size) + "}"}
head_range = header
try:
s = requests.Session()
s.mount('http://',requests.adapters.HTTPAdapter(max_retries=3))
req = s.get(url, headers=head_range,proxies=pro, stream=True, timeout=20)
with tqdm.tqdm(total = file_size, initial=0,
unit='B',unit_scale=True, desc=dst) as pbar:
with(open(dst,"wb")) as f:
Start = time.time()
for chunk in req.iter_content(chunk_size=1024):
if chunk:
Now = time.time()
if(Now - Start > 1000):
raise Timeerror("超时1000s")
f.write(chunk)
pbar.update(1024)
except Exception as e:
print(e)
pbar.close()
return (False,first_byte)
pbar.close()
first_byte = os.path.getsize(dst)
#print(file_size," ",first_byte)
if(first_byte >= file_size):
return (True,file_size)
else:
return (False,first_byte)
2. 文件直接写入硬盘的问题
然后还遇到的一个问题就是,关于文件写入的问题。这里我新建了一个success_img.txt文件用来保存下载成功的图片,防止重复下载:
success_img = open(root_path + "success_img.txt","a+")
但这里有一个问题,在写入内容的过程中并不会直接写入到硬盘中,而是先写到缓存里,当文件调用close()方法时,再将缓存中的内容写入到硬盘中。正常来说,这样操作没有任何毛病,而且减少IO时间耗费。但在爬虫等应用场景中就会遇到一个问题,当你程序卡死或者报错跳出时,你会发现你的爬虫几个小时的努力全白费了,内容并没有写入到文件中,这是因为报错或中断后没正常关闭文件即调用close()方法。知道了问题的原因,我们就可以对症下药了,这里有几个方法来避免这些问题:
- 用
with来安全的打开文件。如上面的方法可以改为:
with open(root_path + "success_img.txt", "a+") as success_img:
pass
- 在每次文件调用
write()方法后,加上一句flush()方法,直接将缓存中的数据写入硬盘。
success_img.write(content)
success_img.flush()
- python的
open函数里有一个buffering的参数,该参数定义如下:
buffering : 如果 buffering 的值被设为 0,就不会有寄存。如果 buffering 的值取 1,访问文件时会寄存行。如果将 buffering 的值设为大于 1 的整数,表明了这就是的寄存区的缓冲大小。如果取负值,寄存区的缓冲大小则为系统默认。(菜鸟教程
open()函数)
因此,我们可以将buffering设置为0。
success_img = open(root_path + "success_img.txt", "a+", 0)
- 最后,还有一个方法也能处理这个问题,那就是万能的异常捕捉了。不像上面的异常捕捉,这里我们是使用
try-finally机制。其实和with打开文件的方法很像,不管遇到什么问题都能安全地关闭文件,亲测Ctrl+C都也能被正确捕获并关闭文件。
success_img = open(root_path + "success_img.txt", "a+")
try:
pass
finally:
success_img.close()
3.简单的反爬
在实现程序过程中,考虑到可能会封ip,参考Python爬虫代理这篇博文,引入了代理机制,这里不得不吹一波requests模块了,直接提供了相应的参数十分方便。
res = requests.request("POST", root_url,headers=header, proxies=pro ,files=file)
另一个考虑到服务器负载的问题,我在每次访问之间都调用了一个time.sleep()函数,算是聊胜于无吧!
总结
以上就是本次实现该小爬虫过程中遇到的主要问题。但程序还有可以改进的地方,比如引进多线程机制并行下载等等,但考虑到任务量和服务器负载情况,并没有尝试实现。