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

《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的关键。

《python爬虫:基于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寄存器、虚拟内存、以及打开的相关句柄等信息,开销大)
  • 通讯主要通过信号传递的方式来实现(实现方式有多种,信号量、管道、事件等,通讯都需要过内核,效率低)

线程

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

协程

  • 调度完全由用户控制
  • 一个线程(进程)可以有多个协程
  • 每个线程(进程)循环按照指定的任务清单顺序完成不同的任务(当任务被堵塞时,执行下一个任务;当恢复时,再回来执行这个任务;任务间切换只需要保存任务的上下文,没有内核的开销,可以不加锁的访问全局变量)
  • 协程需要保证是非堵塞的且没有相互依赖
  • 协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高
    原文作者:Tony带不带水
    原文地址: https://www.jianshu.com/p/15c52a7285f7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞