Python爬虫从入门到放弃

python爬虫从入门到放弃之十二:多协程

2019-07-23  本文已影响5人  52d19f475fe5

一个爬虫,每发起一个请求,都要等服务器返回响应后,才会执行下一步。而很多时候,由于网络不稳定,加上服务器自身也需要响应的时间,导致爬虫会浪费大量时间在等待上。这也是爬取大量数据时,爬虫的速度会比较慢的原因。

既然一个爬虫爬取大量数据要爬很久,那我们能不能让多个爬虫一起爬取?

同步:就是一个任务结束才能启动下一个

异步:在一个任务未完成时,就可以执行其他多个任务,彼此不受影响

显然,异步执行任务会比同步更加节省时间,因为它能减少不必要的等待。

一种非抢占式的异步方式,叫多协程(在此,多是多个的意思)

它的原理是:一个任务在执行过程中,如果遇到等待,就先去执行其他的任务,当等待结束,再回来继续之前的那个任务。在计算机的世界,这种任务来回切换得非常快速,看上去就像多个任务在被同时执行一样。

爬取8个网站(包括百度、新浪、搜狐、腾讯、网易、爱奇艺、天猫、凤凰)

我们先用之前同步的爬虫方式爬取这8个网站,然后等下再和gevent异步爬取做一个对比。

import requests,time

start = time.time()

url_list = ['https://www.baidu.com/',
'https://www.sina.com.cn/',
'http://www.sohu.com/',
'https://www.qq.com/',
'https://www.163.com/',
'http://www.iqiyi.com/',
'https://www.tmall.com/',
'http://www.ifeng.com/']

for url in url_list:
    r = requests.get(url)
    print(url,r.status_code)

end = time.time()
print(end-start)

运行结果:

https://www.baidu.com/ 200
https://www.sina.com.cn/ 200
http://www.sohu.com/ 200
https://www.qq.com/ 200
https://www.163.com/ 200
http://www.iqiyi.com/ 200
https://www.tmall.com/ 200
http://www.ifeng.com/ 200
1.0101947784423828
from gevent import monkey
monkey.patch_all()
import gevent,time,requests

start = time.time()

url_list = ['https://www.baidu.com/',
'https://www.sina.com.cn/',
'http://www.sohu.com/',
'https://www.qq.com/',
'https://www.163.com/',
'http://www.iqiyi.com/',
'https://www.tmall.com/',
'http://www.ifeng.com/']

def crawler(url):
    r = requests.get(url)
    print(url,time.time()-start,r.status_code)

tasks_list = [gevent.spawn(crawler,url) for url in url_list]
gevent.joinall(tasks_list)

end = time.time()
print(end-start)

运行结果:

http://www.iqiyi.com/ 0.16013050079345703 200
http://www.ifeng.com/ 0.22873139381408691 200
https://www.qq.com/ 0.2317953109741211 200
https://www.baidu.com/ 0.26370882987976074 200
https://www.163.com/ 0.2657487392425537 200
https://www.sina.com.cn/ 0.287355899810791 200
https://www.tmall.com/ 0.3268725872039795 200
http://www.sohu.com/ 0.35901474952697754 200
0.35901474952697754

程序运行后,打印出了网址、每个请求运行的时间、状态码和爬取8个网站最终所用时间。

通过每个请求运行的时间,我们能知道:爬虫用了异步的方式抓取了8个网站,因为每个请求完成的时间并不是按着顺序来的。

且每个请求完成时间之间的间隔都非常短,可以看作这些请求几乎是“同时”发起的。

通过对比同步和异步爬取最终所花的时间,用多协程异步的爬取方式,确实比同步的爬虫方式速度更快。

其实,我们案例爬取的数据量还比较小,不能直接体现出更大的速度差异。如果爬的是大量的数据,运用多协程会有更显著的速度优势。

现在,我们一行行来看刚刚用了gevent的代码。

gevent.monkey模块,能将程序转换成可异步的程序,monkey.patch_all(),它的作用其实就像你的电脑有时会弹出“是否要用补丁修补漏洞或更新”一样。它能给程序打上补丁,让程序变成是异步模式,而不是同步模式。它也叫“猴子补丁”

我们要在导入其他库和模块前,先把monkey模块导入进来,并运行monkey.patch_all()。这样,才能先给程序打上补丁。

由于gevent只能处理gevent的任务对象,不能直接调用普通函数,所以需要借助gevent.spawn()来创建任务对象。gevent.spawn()的参数需为要调用的函数名及该函数的参数。比如,gevent.spawn(crawler,url)就是创建一个执行crawler函数的任务,参数为crawler函数名和它自身的参数url。

最后,调用gevent库里的joinall方法,能启动执行所有的任务。gevent.joinall(tasks_list)就是执行tasks_list这个任务列表里的所有任务,开始爬取。

如果我们要爬的不是8个网站,而是1000个网站,我们可以怎么做?

用我们刚刚的gevent语法,我们可以用gevent.spawn()创建1000个爬取任务,再用gevent.joinall()执行这1000个任务。

但这种方法会有问题:执行1000个任务,就是一下子发起1000次请求,这样子的恶意请求,会拖垮网站的服务器。

那我们能不能只创建成5个任务,但每个任务爬取200个网站?

这么做也还是会有问题的。就算我们用gevent.spawn()创建了5个分别执行爬取200个网站的任务,这5个任务之间是异步执行的,但是每个任务(爬取200个网站)内部是同步的。

这意味着:如果有一个任务在执行的过程中,它要爬取的一个网站一直在等待响应,哪怕其他任务都完成了200个网站的爬取,它也还是不能完成200个网站的爬取。



这时我们可以从实际生活的案例中得到启发。想想银行是怎么在一天内办理1000个客户的业务的。

银行会开设办理业务的多个窗口,让客户取号排队,由银行的叫号系统分配客户到不同的窗口去办理业务。

在gevent库中,也有一个模块可以实现这种功能——queue模块。

queue翻译成中文是队列的意思。我们可以用queue模块来存储任务,让任务都变成一条整齐的队列,就像银行窗口的排号做法。因为queue其实是一种有序的数据结构,可以用来存取数据。

这样,协程就可以从队列里把任务提取出来执行,直到队列空了,任务也就处理完了。就像银行窗口的工作人员会根据排号系统里的排号,处理客人的业务,如果已经没有新的排号,就意味着客户的业务都已办理完毕。

代码实现:

from gevent import monkey
monkey.patch_all()
import gevent,time,requests
from gevent.queue import Queue


start = time.time()

url_list = ['https://www.baidu.com/',
        'https://www.sina.com.cn/',
        'http://www.sohu.com/',
        'https://www.qq.com/',
        'https://www.163.com/',
        'http://www.iqiyi.com/',
        'https://www.tmall.com/',
        'http://www.ifeng.com/']

work = Queue()
for url in url_list:
    work.put_nowait(url)


def crawler():
    while not work.empty():
        url = work.get_nowait()
        r = requests.get(url)
        print(url,work.qsize(),r.status_code)

tasks_list = [gevent.spawn(crawler) for x in range(2)]
gevent.joinall(tasks_list)

end = time.time()
print(end-start)

以上代码
work = Queue()实例化一个队列,可以传入参数,如Queue(10)表示队列只能存储10个成员。
work.put_nowait(url)表示:添加队列的成员
while not work.empty():表示:当这个队列不为空时
url = work.get_nowait()表示:取出队列的成员
work.qsize()表示:队列的长度
tasks_list = [gevent.spawn(crawler) for x in range(2)]相当于创建2个爬虫执行crawler()函数
gevent.joinall(tasks_list)启动所有任务

put_nowait()    # 往队列里存储数据
get_nowait()    # 从队列里取出数据
empty()         # 判断队列是否为空
full()          # 判断队列是否为满
qsize()         # 判断队列还剩多少数量



实例展示:

时光网TOP100链接:http://www.mtime.com/top/tv/top100/

使用多协程和队列,爬取时光网电视剧TOP100的数据(剧名、导演、主演和简介),并用csv模块将数据存储下来。

范例代码:

from gevent import monkey
monkey.patch_all()
from gevent.queue import Queue 
from lxml import etree
import requests,csv,gevent

work = Queue()
work.put_nowait('http://www.mtime.com/top/tv/top100/')
for n in range(2,11):
    url = 'http://www.mtime.com/top/tv/top100/index-%d.html'%n
    work.put_nowait(url)

def info(list_name):
    if list_name==[]:
        return ''
    else:
        return list_name[0]

def crawler():
    while not work.empty():
        url = work.get_nowait()
        headers = {'User-Agent': 'Mozilla/5.0'}
        r = requests.get(url,headers = headers)
        html = etree.HTML(r.text)
        tvs = html.xpath('//ul[@id="asyncRatingRegion"]/li')
        for tv in tvs:
            title = tv.xpath('./div[2]/a/@title')
            director = tv.xpath('./div[3]/p[1]/a/text()')
            actor1 = tv.xpath('./div[3]/p[2]/a[1]/text()')
            actor2 = tv.xpath('./div[3]/p[2]/a[2]/text()')
            introduction = tv.xpath('./div[3]/p[3]/text()')

            title = info(title)
            director = info(director)
            actors = info(actor1)+' '+info(actor2)
            introduction = info(introduction)
            print(title,director,actors,introduction)
            writer.writerow([title,director,actors,introduction])
                
f = open('tv.csv','a',newline = '',encoding = 'utf-8')
writer = csv.writer(f)
writer.writerow(['剧名','导演','主演','简介'])

tasks_list = [gevent.spawn(crawler) for x in range(5)]    
gevent.joinall(tasks_list)

f.close()



>>>阅读更多文章请点击以下链接:

python爬虫从入门到放弃之一:认识爬虫
python爬虫从入门到放弃之二:HTML基础
python爬虫从入门到放弃之三:爬虫的基本流程
python爬虫从入门到放弃之四:Requests库基础
python爬虫从入门到放弃之五:Requests库高级用法
python爬虫从入门到放弃之六:BeautifulSoup库
python爬虫从入门到放弃之七:正则表达式
python爬虫从入门到放弃之八:Xpath
python爬虫从入门到放弃之九:Json解析
python爬虫从入门到放弃之十:selenium库
python爬虫从入门到放弃之十一:定时发送邮件
python爬虫从入门到放弃之十二:多协程
python爬虫从入门到放弃之十三:Scrapy概念和流程
python爬虫从入门到放弃之十四:Scrapy入门使用

上一篇下一篇

猜你喜欢

热点阅读