Python异步模块asyncio/aiohttp(链家爬虫实例)


一、写在开头


虽然用scrapy框架来爬信息已经够快了,再用aiohttp来爬链家有点重复造轮子的嫌疑,但还是有助于我对异步编程的理解。以下内容都是出于自己对于异步的理解写出来的,毕竟不是计算机专业,没法用专业的语言来表述,用的都是通俗口语化的文字,其中肯定有些地方也写的并不对,但目前只能这样了,待以后有更深入理解之后再来完善吧。

这是最终的效果(代码放在最后):
同步方式
异步方式

二、几个概念


为了尽可能的发挥出cpu的性能,使程序运行的更有效率,软件跑的更快,可以通过多种方法来实现,比如多进程、多线程或者异步等等。

先解释一下这几个概念:进程线程协程。专业的可以参照《进程、线程与协程的比较》一文。

举个例子来粗浅的理解这几个概念,假设在有一间屋子需要打扫卫生,打扫卫生包含了擦桌子、扫地、拖地、擦窗户。

  • 如果是按顺序一件一件的完成这些事,那么这就是同步编程,打扫卫生的人就是进程;
  • 如果有2个人来一起来打扫卫生,那么就有了2个进程,打扫的速度自然就快了一倍;
  • 如果每个人的左手和右手也可以分别擦桌子和擦窗户,那么每个进程就有了2个线程,打扫速度又快了很多;
  • 但上面都是按顺序来打扫卫生的(同步编程),先擦桌子,再扫地,再拖地,最后擦窗户。假设完成每件事情需要5分钟,其中拖地要拖5分钟然后等地面干燥10分钟,那么1个人完成所有事情的时间就是5+5+15+5=30分钟,2个进程则需要15分钟。
    如果需要更快呢?
    那就可以在等地面干燥时,先去擦窗户,擦完窗户之后再看看地面有没有干,如果干了,则事情就完成了。如果是1个人打扫房间,整个时间就直接缩短为擦桌子5分钟,扫地5分钟,5分钟拖地,5分钟擦窗户,再等待5分钟,共计25分钟。
    这就是异步编程。

但对于程序来说,cpu的计算速度实在是太快了,速度只能用快的飞起来形容,大部分任务中耗时的是磁盘读取和写入、等待服务器响应等等,所以cpu通常都是1秒就做完了自己的事情(其实远没有1秒,可能只要毫秒或者纳秒),而要傻傻的等磁盘或者网络加载到天荒地老。

此时在编程中就需要采取异步的思想,cpu做完自己的事情后,可以先去干别的事情,让磁盘或者网络慢慢的去写入和加载,cpu只需要偶尔过来瞄一眼看看磁盘有没有做完,如果磁盘做完了事情,那么cpu就继续往下做事,如果磁盘还没做完,cpu就继续做别的事情,直到磁盘做完。

一个cpu就是一个进程,一个进程可以分支出多个线程,每个线程可以采取异步的方式来完成任务。如果把这几个特点进行结合起来使用,应该可以有效的提升效率。然而在Python中,由于该语言在当初设计时存在GIL,所以并不能很好的利用多线程。

另外,也不是所有的程序在设计时都需要采用多进程、多线程或者异步思想,盲目的使用可能还会降低效率。

因为任务在不同进程(线程)之间切换也需要消耗时间和资源,对于一些以cpu计算为主的任务(cpu密集型),cpu绝大部分时间已经花在运算上了,来回切换进程(线程)反而降低了效率;而对于大量从磁盘读取写入文件或者对网站服务器发起请求并等待响应的任务(IO密集型),cpu大部分时间处于闲置等待状态,这时采用多进程(线程)则可以显著提升软件的运行时间。

可以参照《Python中单线程、多线程和多进程的效率对比实验---by饒木陽
中的例子:

CPU密集型操作 IO密集型操作 网络请求密集型操作
线性操作 94.91824996469 22.46199995279 7.3296000004
多线程操作 101.1700000762 24.8605000973 0.5053332647
多进程操作 53.8899999857 12.7840000391 0.5045000315

三、Python中实现异步


Python中关于异步的一个模块是asyncio,是英文asynchronous异步的input输入output输出三个单词的缩写。

Python3.6以前,async def只能用装饰器的方式来实现,await要通过yield from来实现。

import asyncio
@asyncio.coroutine
def to_do_something():
   print('do something')
   yield from asyncio.sleep(2)
   print('something done!')

在Python3.6后,可以通过关键词async def来定义一个coroutine协程,协程就相当于未来需要完成的任务,多个协程就是多个需要完成的任务,多个协程可以进一步封装到一个task对象中,task就是一个储存任务的盒子。此时,装在盒子里的任务并没有真正的运行,需要把它接入到一个监视器中使它运行,同时监视器还要持续不断的盯着盒子里的任务运行到了哪一步,这个持续不断的监视器就用一个循环对象loop来实现。

那么,整个过程应该就是通过下面这种方式来实现:

import asyncio
import time

#定义第1个协程,协程就是将要具体完成的任务,该任务耗时3秒,完成后显示任务完成
async def to_do_something(i):
    print('第{}个任务:任务启动...'.format(i))
    #遇到耗时的操作,await就会使任务挂起,继续去完成下一个任务
    await asyncio.sleep(i)
    print('第{}个任务:任务完成!'.format(i))
#定义第2个协程,用于通知任务进行状态
async def mission_running():
    print('任务正在执行...')

start = time.time()
#创建一个循环
loop = asyncio.get_event_loop()
#创建一个任务盒子tasks,包含了3个需要完成的任务
tasks = [asyncio.ensure_future(to_do_something(1)),
         asyncio.ensure_future(to_do_something(2)),
         asyncio.ensure_future(mission_running())]
#tasks接入loop中开始运行
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(end-start)

程序运行的过程看起来就像是这个样子:

紫框--tasks中的第一个任务;
黄框--tasks中的第二个任务;
绿框--tasks中的第三个任务;
红线--程序运行路线
虚线--程序阻塞,出让函数的控制权

遇到需要等待的地方时,当前任务就会挂起,先去做第2个任务,然后继续挂起,再继续做第3个任务,最终完成任务时,总耗时则应该是2秒,而不是1秒+2秒=3秒。

程序实际运行的状态也确实如此,时间是2秒多一点,多出来的时间可能就是cpu本身的运算时间。

需要说明的是,3个任务的运行顺序是按照tasks列表里的顺序进行的,如果把顺序换一下,上面的程序运行图就不一样了。
#下面两个tasks运行的结果是不一样的
tasks = [asyncio.ensure_future(to_do_something(1)),
         asyncio.ensure_future(to_do_something(2)),
         asyncio.ensure_future(mission_running())]
####################################################
tasks = [asyncio.ensure_future(to_do_something(1)),
         asyncio.ensure_future(mission_running()),
         asyncio.ensure_future(to_do_something(2))]

如果想通过异步的方式对网络发起请求,则还需要借助aiohttp模块,这个模块相当于requests模块的异步版本,使用方法极其相似,有小部分差异,只要习惯就好。

import asyncio
import aiohttp
async def get_info(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url,timeout=5) as resp:
            if resp.status != 200:
                url_lst_failed.append(url)
            else:
                url_lst_successed.append(url)
            r = await resp.text()

四、其它一些


为什么要用到循环loop?

程序是按照设定的顺序从头执行到尾,运行的次数也是完全按照设定。当在编写异步程序时,必然其中有部分程序的运行耗时是比较久的,需要先让出当前程序的控制权,让其在背后运行,让另一部分的程序先运行起来。当背后运行的程序完成后,也需要及时通知主程序已经完成任务可以进行下一步操作,但这个过程所需的时间是不确定的,需要主程序不断的监听状态,一旦收到了任务完成的消息,就开始进行下一步。loop就是这个持续不断的监视器。

链家爬虫代码可以看《链家爬虫代码_asyncio》

对比以前的版本,可以手动输入价格区间,采用二分法自动切割价格区间,将各个区间房屋数量缩到3000以内(链家限制了显示条目,最多只有3000)。

另外也还存在一个问题,在对比同步爬取和异步爬取得结果时,发现当url数量比较多时,会有较大一部分的url在采用异步爬取时不会被发起请求,导致异步的结果数量比同步的结果数量少了很多。希望有人能够解答这个问题。

我最后采用了一个笨办法解决了这个问题,将首次运行后未被发起请求的url再放入一个列表中,然后再次循环,直至所有url都被发起请求,最终实现抓取全部信息。


五、参考资料


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

推荐阅读更多精彩内容

  • 《论语》讲的是“做”,讲的是“做人做事”,而不是说把道理放在这里,只是让读者看一看、读一读。如果只是看一看、读一读...
    纠偏人阅读 1,069评论 0 1
  • 2017区块链闯入大众视野元年,一时间区块链信息满天飞,就算对信息超级不敏感的普通人,恐怕也不会说完全没有听过区块...
    践行哲阅读 245评论 0 0
  • 今天是我在ole面包部上班的第八天,前面六天一直是在卖场做营业,前两天在操作间内学习了巧克力凹蛋糕,榴莲芝士的制作...
    唯yixin阅读 211评论 0 0
  • #! /usr/bin/python# -*- coding:utf-8 -*-class P1(object):...
    thebeeman阅读 405评论 0 0