问题背景

  将这次任务概括就是将300+张图片一一上传到一个网址,经过其后台服务器处理,生成一张高动态(HDR)图片并返回其下载链接。在这种重复性劳动的应用需求下,我们很容易想到利用爬虫代替我们人工,毕竟我们的时间是很宝贵的(hh,有这时间去打一把王者不好吗?)。
  言归正传,这是一个很基础的爬虫任务,代码量也少。但在实现过程中却遇到不少问题,特地在此记录一下。

开发环境

  1. win10 1903家庭版
  2. Python3.5、Jupyter notebook,Fiddler

python包

  1. requests
  2. beautifulSoup
  3. 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()函数,算是聊胜于无吧!

总结

  以上就是本次实现该小爬虫过程中遇到的主要问题。但程序还有可以改进的地方,比如引进多线程机制并行下载等等,但考虑到任务量和服务器负载情况,并没有尝试实现。

文章来源于互联网:记一次编写爬虫过程中遇到的问题

发表评论