python爬虫——多线程+协程(threading+gevent)

上一篇博客中我介绍了如何将爬虫改造为多进程爬虫,但是这种方法对爬虫效率的提升不是非常明显,而且占用电脑cpu较高,不是非常适用于爬虫。这篇博客中,我将介绍在爬虫中广泛运用的多线程+协程的解决方案,亲测可提高效率至少十倍以上。
本文既然提到了线程和协程,我觉得有必要在此对进程、线程、协程做一个简单的对比,了解这三个程之间的区别。
以下摘自这篇文章:http://www.cnblogs.com/guokaixin/p/6041237.html

1、进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
2、线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
3、协程
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
简单来说,协程相比线程来说有一个优势,就是在协程间切换时不需要很大的资源开销。在使用时可以开多个进程,然后每个进程开多个线程,每个线程开多个协程来综合使用。

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/'
1
2
3
4
5
6
7
8
9
10
11
多线程可以使用的包一般有两个:Thread和threading,threading更强大和常用一点,可以利用threading.Thread来自定义多线程类。gevent为python下的协程包。
本篇实例场景与上一篇相同,依旧为爬取外文数据库,可参考 http://blog.csdn.net/qq_23926575/article/details/76375042

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() #这句是必须的,否则线程还没开始运行就结束了
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
其中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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在上述代码中我开启了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小时搞定。

以上。有问题欢迎评论交流,如有错误也欢迎指出。
---------------------
作者:MoonBreeze_Ma
来源:CSDN
原文:https://blog.csdn.net/qq_23926575/article/details/76375337
版权声明:本文为博主原创文章,转载请附上博文链接!

上一篇:进击的Python【第十章】:Python的socket高级应用(多进程,协程与异步)


下一篇:Python 多进程 多线程 协程 I/O多路复用