美文网首页python爬虫
异步爬虫-aiohttp库、Twisted库

异步爬虫-aiohttp库、Twisted库

作者: 放风筝的富兰克林 | 来源:发表于2018-08-08 17:29 被阅读0次

    为什么要用异步爬虫?

     爬虫本质上就是模拟客户端与服务端的通讯过程。以浏览器端的爬虫为例,我们在爬取不同网页过程中,需要根据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的整理文章。

    相关文章

      网友评论

        本文标题:异步爬虫-aiohttp库、Twisted库

        本文链接:https://www.haomeiwen.com/subject/vdmtbftx.html