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

NN7Bvy.jpg 这张真的很好看啊

当你写爬虫写了一段时间,你开始觉得这个爬虫怎么那么慢,明明代码优美没有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的关键。

6B3UJz.png 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寄存器、虚拟内存、以及打开的相关句柄等信息,开销大)

通讯主要通过信号传递的方式来实现(实现方式有多种,信号量、管道、事件等,通讯都需要过内核,效率低)

线程

共享变量(解决了通讯麻烦的问题,但是对于变量的访问需要加锁)

调度由操作系统完成

一个进程可以有多个线程,每个线程会共享父进程的资源(创建线程开销占用比进程小很多,可创建的数量也会很多)

通讯除了可使用进程间通讯的方式,还可以通过共享内存的方式进行通信(通过共享内存通信比通过内核要快很多)

线程的使用会给系统带来上下文切换的额外负担。

协程

调度完全由用户控制

一个线程(进程)可以有多个协程

每个线程(进程)循环按照指定的任务清单顺序完成不同的任务(当任务被堵塞时,执行下一个任务;当恢复时,再回来执行这个任务;任务间切换只需要保存任务的上下文,没有内核的开销,可以不加锁的访问全局变量)

协程需要保证是非堵塞的且没有相互依赖

协程基本上不能同步通讯,多采用异步的消息通讯,效率比较高

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值