Python爬虫——多线程+协程threading+gevent

上一篇博客中我介绍了如何将爬虫改造为多进程爬虫,但是这种方法对爬虫效率的提升不是非常明显,而且占用电脑CPU较高,不是非常适用于爬虫。这篇博客中,我将介绍在爬虫中广泛运用的多线程+协程的解决方案,亲测可提高效率至少十倍以上。

本文既然提到了线程和协程,我觉得有必要在此对进程、线程、协程做一个简单的对比,了解这三个程之间的区别。
以下摘自这篇文章:进程线程协程的区别


一、概念介绍

1、进程

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

2、线程

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

3、协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
简单来说,协程相比线程来说有一个优势,就是在协程间切换时不需要很大的资源开销。在使用时可以开多个进程,然后每个进程开多个线程,每个线程开多个协程来综合使用。


二、代码

多线程可以使用的包一般有两个:Threadthreadingthreading更强大和常用一点,可以利用threading.Thread来自定义多线程类。geventpython下的协程包。

from bs4 import BeautifulSoup
import datetime
import sys
import threading
reload(sys)
sys.setdefaultencoding('utf-8')
import gevent.monkey
gevent.monkey.patch_all()   
import socket
socket.setdefaulttimeout(10)
path = sys.path[0] + '/data/'

本篇实例场景与上一篇相同,依旧为爬取外文数据库,可参考Python爬虫——多进程multiprocessing

def main():
	"""将任务切割,开启多线程"""
    listf = open(path + 'urllist.txt', 'r')
    urllist = listf.readlines()
    length = len(urllist)
    print length
    queList = []
    threadNum = 6  # 线程数量
    # 将urllist按照线程数目进行切割
    for i in range(threadNum):
        que = []  # Queue.Queue()
        left = i * (length//threadNum)
        if (i+1) * (length//threadNum)<length:            
            right = (i+1) * (length//threadNum)
        else:
            right = length
        for url in urllist[left:right]:
            que.append(url.strip())
        queList.append(que)
    threadList = []
    for i in range(threadNum):
        threadList.append(threadDownload(queList[i]))
    for thread in threadList:
        thread.start()  # 启动线程
    for thread in threadList:
        thread.join()  # 这句是必须的,否则线程还没开始运行就结束了

其中threadDownload是自定义的线程类,传入参数为url列表。在这个线程类中开启多个协程。

class threadDownload(threading.Thread):
	"""使用threading.Thread初始化自定义类"""
    def __init__(self, que):
        threading.Thread.__init__(self)
        self.que = que
    def run(self):               
        length = len(self.que)
        coroutineNum = 20  # 协程数量
        for i in range(coroutineNum):
            jobs = []
            left = i * (length//coroutineNum)
            if (i+1)*(length//coroutineNum)<length:            
                right = (i+1) * (length//coroutineNum)
            else:
                right = length
            for url in self.que[left:right]:
                jobs.append(gevent.spawn(getThesis, url))
            gevent.joinall(jobs)

在上述代码中我开启了6个线程,并且在每个线程中开启了20个协程,因为需要抓取的数据量较大,故对数据进行了切割。其实也可以使用quene队列的方式来实现,多个协程共用同一个队列数据,但是管理起来稍微麻烦一点。

我在运行的过程中发现有这样几个问题,运行速度很快,是多进程的十几倍,但是抓很多数据时会抓取失败,报各种错误,最常见的是too many open files和new connection failed之类的错误,应该是每个协程都获得了一个文件的句柄,所以你可能只打开了几个文件,但是系统会认为你开启了很多,网上找了解决方案(有一个是修改ulimit,即系统设定的最大开启文件数量,在ubuntu下输入ulimit -n得到的1024是系统默认的,可以通过ulimit - n 5000修改为5000,但是也没能解决问题),但是都没有很好的方法能避免这类问题,希望懂的高手能够告知一下。

多线程+协程的方法效率高,但是很不稳定,会出现很多错误,所以在编写代码的过程中,需要做一些错误的处理,使程序更加robust,在抓取一些链接出问题的时候能够不挂掉继续抓取其他页面。建议将出错的url保存到一个文件内,最后再对这些url进行抓取。

我的urllist文件中其实有20万条数据,但是全部一次性运行会挂掉,所以我是每次读取4万条记录,然后6个线程,每个线程分别20个协程进行抓取,大概1小时搞定。


以上,欢迎交流,如有错误也欢迎指出。

  • 6
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值