异步爬虫-aiohttp库、Twisted库

为什么要用异步爬虫?

 爬虫本质上就是模拟客户端与服务端的通讯过程。以浏览器端的爬虫为例,我们在爬取不同网页过程中,需要根据url构建很多HTTP请求去爬取,而如果以单个线程为参考对象,平常我们所采取的编码习惯,通常是基于同步模式的,也就是串行的方式去执行这些请求,只有当一个url爬取结束后才会进行下一个url的爬取,由于网络IO的延时存在,效率非常低。
 到这里可能会有人说,那么我们可以使用多进程+多线程来提高效率啊,为什么要使用异步编程,毕竟异步编程会大大增加编程难度。【进程、线程、协程简单梳理】在这篇整理文章中有提到,多进程/多线程虽然能提高效率,但是在进程/线程切换的时候,也会消耗很多资源。而且就IO密集型任务来说,虽然使用多线程并发可以提高CUP使用率,提升爬取效率,但是却还是没有解决IO阻塞问题。无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,爬虫的执行效率因此就降低了下来。而异步编程则是我们在应用程序级别来检测IO阻塞然后主动切换到其他任务执行,以此来'降低'我们爬虫的IO,使我们的爬虫更多的处于就绪态,这样操作系统就会让CPU尽可能多地临幸我们的爬虫,从而提高爬虫的爬取效率。

补充:常见的IO模型有阻塞、非阻塞、IO多路复用,异步。下面举个小栗子来简单描述一下这四个场景。
当快乐的敲代码时光结束时,没有女朋友的单身狗只能约上好基友去召唤师峡谷傲游,当我秒选快乐风男,然后发送“亚索中单,不给就送后”,在队友一片欢声笑语中进入加载界面,奈何遇到小霸王,加载异常缓慢。。。此时!

  1. 你选择什么也不做,直直地看着辅助妹子的琴女原画,等待加载完成。这是阻塞。
  2. 你选择切换出去到某鱼看球,但是你得时不时的切换回LOL,看看是否加载完成,就这样来来回回,累得要死,还错过很多精彩画面。这是非阻塞。
  3. 你掏出自己的爱疯XL,打开某鱼APP看球,这样就不用来回切换,只是时不时地看一下电脑显示器,看是否加载完成了。这是IO多路复用。
  4. 连泡面都舍不得一次用完一包酱包的你,哪有爱疯XL,但是身为码农的尊严让你写个个小程序,在检测到游戏在加载的时候,自动切换到浏览器,打开某鱼的球星直播间。而当游戏加载完成后,自动切换回LOL游戏界面。无缝切换丝滑感受。这个叫异步。

下面开始进入正题

asyncio

在介绍aiohttp、tornado、twisted之前,先了解下python3.4版本引入的标准库asyncio。它可以帮助我们检测IO(只能是网络IO),实现应用程序级别的切换。它的编程模型是一个消息循环。我们可以从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。

基本使用

import asyncio
import random
import datetime


urls=['www.baidu.com','www.qq.com','www.douyu.com']

@asyncio.coroutine
def crawl(url):
    print("正在抓取:{}-{}".format(url,datetime.datetime.now().time()))
    io_time = random.random()*3 #随机模拟网络IO时间
    yield from asyncio.sleep(io_time) #模拟网络IO
    print('{}-抓取完成,用时{}s'.format(url,io_time))

loop = asyncio.get_event_loop() #获取EventLoop
loop.run_until_complete(asyncio.wait(map(crawl,urls))) #执行coroutine
loop.close()

运行结果:

正在抓取:www.baidu.com-12:45:26.517226
正在抓取:www.douyu.com-12:45:26.517226
正在抓取:www.qq.com-12:45:26.517226
www.douyu.com-抓取完成,用时0.1250027573049739s
www.baidu.com-抓取完成,用时0.450045918339271s
www.qq.com-抓取完成,用时0.6967129499714361s
[Finished in 0.9s]

运行的时候可以发现三个请求几乎是同时发出的,而返回顺序则是根据网络IO完成时间顺序返回的。

由于asyncio主要应用于TCP/UDP socket通讯,不能直接发送http请求,因此,我们需要自己定义http报头。
补充:

  • 客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行消息报头请求正文三个部分。
    例如:GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n
  • asyncio提供的@asyncio.coroutine可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from调用另一个coroutine实现异步操作。为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法asyncawait,可以让coroutine的代码更简洁易读。

概念补充

  • event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
  • coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
  • task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
  • future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。

有了以上知识基础,就可以撸代码啦

import asyncio
import uuid


user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'

def parse_page(host,res):
    print('%s 解析结果 %s' %(host,len(res)))
    with open('%s.html' %(uuid.uuid1()),'wb') as f:
        f.write(res)

async def get_page(host,port=80,url='/',callback=parse_page,ssl=False,encode_set='utf-8'):
    print('下载 http://%s:%s%s' %(host,port,url))

    
    if ssl:
        port=443
    #发起tcp连接, IO阻塞操作
    recv,send=await asyncio.open_connection(host=host,port=port,ssl=ssl) 

    #封装http协议的报头,因为asyncio模块只能封装并发送tcp包,因此这一步需要我们自己封装http协议的包
    request_headers="""GET {} HTTP/1.0\r\nHost: {}\r\nUser-agent: %s\r\n\r\n""".format(url,host,user_agent) 

    request_headers=request_headers.encode(encode_set)

    #发送构造好的http请求(request),IO阻塞
    send.write(request_headers)
    await send.drain()

    #接收响应头 IO阻塞操作
    while True:
        line=await recv.readline()
        if line == b'\r\n':
            break
        print('%s Response headers:%s' %(host,line))

    #接收响应体 IO阻塞操作
    text=await recv.read()

    #执行回调函数
    callback(host,text)

    #关闭套接字
    send.close() #没有recv.close()方法,因为是四次挥手断链接,双向链接的两端,一端发完数据后执行send.close()另外一端就被动地断开


if __name__ == '__main__':
    tasks=[
        get_page('www.gov.cn',url='/',ssl=False),
        get_page('www.douyu.com',url='/',ssl=True),
    ]

    loop=asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

用上async/await关键字,是不是既简洁,也更便于理解了

自己动手封装HTTP(S)报头确实很麻烦,所以接下来就要请出这一小节的正主aiohttp了,它里面已经帮我们封装好了。
补充:asyncio可以实现单线程并发IO操作。如果仅用在客户端,发挥的威力不大。如果把asyncio用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可以用单线程+coroutine实现多用户的高并发支持。asyncio实现了TCPUDPSSL等协议,aiohttp则是基于asyncio实现的HTTP框架。它分为两部分,一部分是Client(我们将要使用的部分,因为我们爬虫是模拟客户端操作嘛),一部分是 Server,详细的内容可以参考官方文档

下面我们用aiohttp来改写上面的代码:

import asyncio
import uuid
import aiohttp


user_agent='Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'

def parse_page(url,res):
    print('{} 解析结果 {}'.format(url,len(res)))
    with open('{}.html'.format(uuid.uuid1()),'wb') as f:
        f.write(res)


async def get_page(url,callback=parse_page):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    if response.reason == 'OK':
        result = await response.read()
    session.close()
    callback(url,result)
    

if __name__ == '__main__':
    tasks=[
        get_page('http://www.gov.cn'),
        get_page('https://www.douyu.com'),
    ]

    loop=asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

是不是更加简洁了呢?


Twisted

twisted是一个网络框架,哪怕刚刚接触python爬虫的萌新都知道的Scrapy爬虫框架,就是基于twisted写的。它其中一个功能就是发送异步请求,检测IO并自动切换。
基于twisted修改上面的代码如下:

from twisted.web.client import getPage,defer
from twisted.internet import reactor
import uuid


def tasks_done(arg):
    reactor.stop() #停止reactor


#定义回调函数
def parse_page(res):
    print('解析结果 {}'.format(len(res)))
    with open('{}.html'.format(uuid.uuid1()),'wb') as f:
        f.write(res)

defer_list=[]#初始化一个列表来存放getPage返回的defer对象

urls=[

    'http://www.gov.cn',
    'https://www.douyu.com',
]

for url in urls:
    obj = getPage(url.encode('utf-8'),) #getPage会返回一个defer对象
    obj.addCallback(parse_page) #给defer对象添加回调函数
    defer_list.append(obj) #将defer对象添加到列表中

defer.DeferredList(defer_list).addBoth(tasks_done) #任务列表结束后停止reactor.stop

reactor.run #启动监听

这只是一个简单的应用,后面会看情况可能会写一篇Twisted的整理文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容