爬虫教程(2)性能进阶

2016-02-25 21:15:24

本博客采用创作共用版权协议, 要求署名、非商业用途和保持一致. 转载本博客文章必须也遵循署名-非商业用途-保持一致的创作共用协议

这里我们介绍爬虫的性能进阶,

一般的进阶,从性能上来看,一般是这么几个模块:

  • threading
  • multiprocessing
  • twisted
  • gevent
  • tornado

threading


首先,第一个,由于python内部又 GIL 锁这种东西的存在,python的多线程编程其实并不算十分友好的,python想保证他的线程安全,线程内共享资源共享很多东西,而且更好的做通信,但是最近和几个大牛谈过之后,他们都说python多线程不好,我也就没多试,这里给出一个样板吧

这里给出一个多线程的爬虫例子:

import urllib2
import time
import Queue
import threading

hosts=["http://baidu.com",
   "http://jianshu.com",
   "http://taobao.com",
   "http://tmall.com",
   "http://jd.com"]

queue=Queue.Queue()

class ThreadUrl(threading.Thread):
    def __init__(self,queue):
        threading.Thread.__init__(self)
        self.queue=queue

    def run(self):
        while True:
            host=self.queue.get()
            url=urllib2.urlopen(host)
            print url.geturl()
            print self.getName()

            self.queue.task_done()


start=time.time()

def main():
    for i in range(5):
        t=ThreadUrl(queue)
        t.setDaemon(True)
        t.start()

    for host in hosts:
        queue.put(host)
    queue.join()

if __name__=="__main__":
    main()

print "it use time:%s" % (time.time()-start)

这里请一定要用队列queue来进行任务队列的设置,然后最好设置成 setDaemon(True)这样可以保护线程

multiprocessing


你要充分利用好自己的cpu资源,那么肯定是要选择多进程的,多进程的程序问题在资源的通信上面,换在爬虫上,就是任务队列的设置,通常,放在生产上面,我们会使用 redis或者 celery这样的来做我们的任务队列

那么python对于多进程支持算是还不错的,通常你复制出一个子进程,是这么写:

import os
pid2=os.fork()
print str(os.getpid)
print str(os.getppid)

这样可以获得一个子进程,然后这个子进程的资源是完全复制父进程的,你这样写可以获得父进程的pid号码

python的multiprocessing模块,方便我们进行多进程编写,下面是一个多进程爬虫的例子:

from multiprocessing.dummy import Pool
import requests
import urllib
import time
import os

def get_source(url):
    print "we crawled:%s" % url
    html=urllib.urlopen(url)
    print html.geturl()
    print os.getpid()

urls=[]

for i in range(1,21):
    new_page="http://tieba.baidu.com/p/3522395718?pn="+str(i)
    urls.append(new_page)

time1=time.time()

for url in urls:

    get_source(url)

time2=time.time()
print "it take:",(time2-time1)

pool=Pool(4)
time3=time.time()
res=pool.map(get_source,urls)
pool.close()
pool.join()
time4=time.time()
print "now,it take,",(time4-time3)
  • 这里的pool是启动一个进程池,一般是你有几核的CPU你有写几个,你通过调pool.map对你的队列进行任务映射
  • 最后时一定记得要pool.close之后才 pool.join这样最后关掉你的进程

gevent


首先我们先来讲讲什么的 协程

协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。

所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

这样可以实现大并发的执行,但是却又不消耗太多资源

python对协程的支持,是以迭代器来支持的,通常是使用 yield表达协程

这里我们使用 gevent,它内部基于greenlet的机制,非常敏捷

gevent我早早便在我的django的web应用里面,让他配合gunicorn一起使用,并发的性能比起uwsgi好了一个级别

那么我们来看看普通协程的程序怎么写:

import gevent.monkey
gevent.monkey.patch_socket()

import gevent
import urllib2

def fetch(pid):
   response = urllib2.urlopen('http://aljun.me')
   result = response.read()


   print('Process %s: %s' % (pid, datetime))


def synchronous():
    for i in range(1,10):
        fetch(i)

def asynchronous():
    threads = []
    for i in range(1,10):
        threads.append(gevent.spawn(fetch, i))
    gevent.joinall(threads)

print('Synchronous:')
synchronous()

print('Asynchronous:')
asynchronous()

对比同步异步,时间上快乐相当多

  • 这里使用非常简单,我们只需把我们的function 放进 gevent.spawn 里面
  • 然后,将这些,传给 gevent.joinall便可以了

同样的,我们也可以使用诸如:

from gevent.pool import Pool

p = Pool(4)

p.map()

就和multiprocessing一样的操作。

同时,gevent模块的monkey_patch可以将你的socket类型修改,提升性能,因为我对这个研究不算多,暂且不说

异步


首先,我们先谈谈什么是异步:

pic

首先,这个是我同时有三个任务需要去做,那么通常我们是完成了一个任务,再去做下一个,这样明显的很慢

pic

这个是多线程版本,我们三个任务同时分配给三个线程去做,这样人多力量大,可能更快

pic

这个便是我们的异步版本了,首先,你要知道,一个任务,可能会堵塞在某个地方,比如硬盘读写,数据库操作等等,这样一个程序就必须等待你的操作完成,才能继续下去,这无疑是对计算资源的极大浪费,那么如果,你在堵塞的时候,用某种机制,能使你快速脱离堵塞的状态,开启下一个程序,等待你的操作完成,在来继续上一个程序的工作,这个模型就是不堵塞的,这样资源的到了极大的利用,我们的效率自然提升

pic

单线程堵塞的模型图片

而twisted是基于一个react的异步模型,scrapy就是基于他做的,我们已经见识过scrapy的威力。,因为twisted,人如其名,文档也很扭曲,看的十分不易:

pic

这个是twisted的模型,

下面我给一个基于twisted的爬虫例子把:

from twisted.internet import defer,reactor,task
from twisted.web.client import getPage

maxRun=2

urls=[
    'http://zhaiduixueshe.com',
    'http://baidu.com',
    'http://taobao.com',
]

def pageCallback(res):
    print len(res)
    return res

def doWork():
    for url in urls:
        d=getPage(url)
       d.addCallback(pageCallback)
       yield d

def finish(ign):
    reactor.stop()

def test():
    defereds=[]
    coop=task.Cooperator()
    work=doWork()
    for i in xrange(maxRun):
        d=coop.coiterate(work)
       defereds.append(d)

    dl=defer.DeferredList(defereds)
    dl.addCallback(finish)

test()
reactor.run()

而tornado的tornado.web.AsynHTTPClient算是一种服务,他基本算是HTTP库,所以先不讨论

综上的话,我是推荐:

  • multiprocessing
  • gevent

这两个组合,你在使用mongodb做数据存取,用redis做任务的url缓存,用celery做调度队列,那么跑一个百万级别的爬虫还说相当简单的

python爬虫 返回首页

Designed and built with all the love in the world by the Mr.ALJUN.

@SERVER BY NGINX AND POWER BY DIGITALOCEAN.

© COPYRIGHT BY GAGASALAMER 2015