python爬虫:基于gevent异步爬虫的原理及实现

这张真的很好看啊

  当你写爬虫写了一段时间,你开始觉得这个爬虫怎么那么慢,明明代码优美没有bug。所以你不会去想方设法降低你爬虫的时间复杂度或者空间复杂度,你清楚的知道机器的大部分时间花在了网络IO上。想提速怎么办?
  加钱买带宽买机器啊!好的本文结束,大家散了散了。
  哎哎哎,你们刀放下我好好说话。
  看标题猜到,本文爬虫提速方式是用异步机制。先看看这个与你的同步爬虫有什么差别?你需要先了解两(四)个概念:

  • 同步和异步:关注的是消息通信机制 (synchronous communication/ asynchronous communication)
    • 同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。调用者主动等待这个调用的结果。
    • 异步调用在发出之后,这个调用就直接返回了,所以没有返回结果。在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
  • 阻塞和非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态
    • 阻塞调用:指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
    • 非阻塞调用:指在不能得到结果时,该调用不会阻塞当前线程

  你一突然一拍脑袋,完蛋怎么跟线程有关系,不是说python有GIL,多线程都是假的。
  对啊对啊,快来学golang吧。哎哎哎?怎么又是你,把刀放下好好说话。
  python因为GIL并不能做到并行,但可以做到并发。对于计算密集型应用,python的多线程确实没啥用。但对于向网页提交多个request这种IO密集型应用,并发就很有用了。嗯...说你的爬虫不是cpu密集型,是IO密集型你没什么意见吧。
  简单说三个大家应该多多少少了解的概念(为不影响阅读,详细概念我会放在本文最后附录部分)。

  • 进程:拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度
  • 线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)
  • 协程:和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显式调度

  别急,马上引出gevent,基础知识还是要讲讲的。之前说python的多线程其实是串行,但是的确可以提高IO密集型应用的速度,为什么这里不用多线程而要基于gevent(协程)?

  • 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但容易死锁
  • 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高
      来来来,请gevent登场:

Gevent安装:

  直接输入pip install gevent

Gevent核心部分:

  gevent中的主要模式, 它是以C扩展模块形式接入Python的轻量级协程。 全部运行在主程序操作系统进程的内部,但它们被程序员协作式地调度

  • Greenlets:请注意基于Greenlets,先有Greenlets后有Gevent。greenlet你稍微了解这些要点:
    • 每一个greenlet.greenlet实例都有一个parent(可指定,默认为创生新的greenlet.greenlet所在环境),当greenlet.greenlet实例执行完逻辑正常结束、或者抛出异常结束时,执行逻辑切回到其parent
    • 可以继承greenlet.greenlet,子类需要实现run方法,当调用greenlet.switch方法时会调用到这个run方法
  • 确定性:greenlet具有确定性。在相同配置相同输入的情况下,它们总是会产生相同的输出。你爬虫就不要想了,网络响应时间每次都不一样,但这个特性你需要了解。
  • 程序停止:当主程序(main program)收到一个SIGQUIT信号时,调用gevent.shutdown可以退出程序。
  • 超时:通过超时可以对代码块儿或一个Greenlet的运行时间进行约束。
  • 猴子补丁:先了解gevent.monkey.patch_all()

  先看代码吧,结合代码说:

import gevent
import greenlet
def callback(event, args):
    print event, args[0], '===:>>>>', args[1]

# 想象成你的爬虫1
def foo():
    print('Running in foo')
    # 这个时候做了网络IO
    gevent.sleep(0)
    print('Explicit context switch to foo again')

# 想象成你的爬虫2
def bar():
    print('Explicit context to bar')
    # 这个时候做了网络IO
    gevent.sleep(0)
    print('Implicit context switch back to bar')

print 'main greenlet info: ', greenlet.greenlet.getcurrent()
print 'hub info', gevent.get_hub()
oldtrace = greenlet.settrace(callback)
        
gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])
greenlet.settrace(oldtrace)

  你可以直接代码拷过去运行一下,你可以看到gevent的调度方式。我将其转换成图片方便大家阅读理解。你会发现多了个hub,每次从hub切换到一个greenlet后,都会回到hub,然而这就是gevent的关键。

Gevent中调度方式

  采用这种模式个人理解是:

  • hub是事件驱动的核心,每次切换到hub后将继续循环事件。如果在一个greenlet中不出来,那么其它greenlet将得不到调用
  • 维持两者关系肯定比维持多个关系简单。所以每次关心的就是hub以及当前greenlet,不需要全局考虑各个greenlet之间关系。

涉及数据结构:

  嗯...有兴趣深入了解的看官方文档吧?这里主要讲爬虫,爬虫用的到的地方给了解释。

  • 事件
  • 队列
  • 组和池:写爬虫的话最少需要掌握池。
    • 池(pool)是一个为处理数量变化并且需要限制并发的greenlet而设计的结构。
  • 锁和信号量
  • 线程局部变量
  • 子进程
  • Actors

  实际应用到你的爬虫中:
  实在抱歉啊,我尽可能的少说概念了,可是直接上代码就跟网上其他我看的教程一样云里雾里,我觉得这样不是很好,好了快看代码吧。

import gevent
from gevent import Greenlet
from gevent import monkey
import gevent.pool
# 在进行IO操作时,默认切换协程
monkey.patch_all()

# 假设我在这里调用了你的爬虫类接口
def run_Spider(url):
    # do anything what u want
    pass
    
if __name__ == '__main__':
    # 假如你的url写在文件中 用第一个参数传进来
    import sys
    # 限制并发数20
    pool = gevent.pool.Pool(20)
    # 这里也可以用pool.map,我这么写比较无脑
    threads = []
    with open(sys.argv[1], "r") as f:
        for line in f:
            threads.append(pool.spawn(run_Spider,line.strip()))
    gevent.joinall(threads)
    print "finish"

  这样就实现一个基本异步爬虫更加复杂的异步也逃不过这些基础的东西。如果说的不到位,大家指正啊没事,评论私信都行,不想写那么多概念的,可是好像不写不行,会更加云里雾里。


附录:

进程

  • 不共享任何状态
  • 调度由操作系统完成
  • 有独立的内存空间(上下文切换的时候需要保存栈、cpu寄存器、虚拟内存、以及打开的相关句柄等信息,开销大)
  • 通讯主要通过信号传递的方式来实现(实现方式有多种,信号量、管道、事件等,通讯都需要过内核,效率低)

线程

  • 共享变量(解决了通讯麻烦的问题,但是对于变量的访问需要加锁)
  • 调度由操作系统完成
  • 一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会很多)
  • 通讯除了可使用进程间通讯的方式,还可以通过共享内存的方式进行通信(通过共享内存通信比通过内核要快很多)
  • 线程的使用会给系统带来上下文切换的额外负担。

协程

  • 调度完全由用户控制
  • 一个线程(进程)可以有多个协程
  • 每个线程(进程)循环按照指定的任务清单顺序完成不同的任务(当任务被堵塞时,执行下一个任务;当恢复时,再回来执行这个任务;任务间切换只需要保存任务的上下文,没有内核的开销,可以不加锁的访问全局变量)
  • 协程需要保证是非堵塞的且没有相互依赖
  • 协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • 目录 一、开启线程的两种方式 在python中开启线程要导入threading,它与开启进程所需要导入的模块mul...
    CaiGuangyin阅读 2,400评论 1 16
  • 前言 很多朋友对异步编程都处于“听说很强大”的认知状态。鲜有在生产项目中使用它。而使用它的同学,则大多数都停留在知...
    星星在线阅读 2,856评论 2 39
  • 前述 进程 线程 协程 异步 并发编程(不是并行)目前有四种方式:多进程、多线程、协程和异步。 多进程编程在pyt...
    softlns阅读 6,332评论 2 24
  • 年轻的女孩子想嫁给爱情,年长点的就会想着嫁给金钱,成熟的女人知道,应当嫁给人品与涵养,深沉的女子明白,最该嫁给是温...
    炙热玫瑰阅读 134评论 1 1
  • hi 豆苗: 我是小树的队友,钱叔叔(怎么有点怪怪的自我介绍)我们有个超级酷的战队,叫做铁公鸡! 铁公鸡的图形是这...
    黑土钱阅读 347评论 4 7