Py3异步爬虫浅涉

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/chenlnehc/article/details/77871861

Py3.x异步网络爬虫浅涉

异步的概念

举个例子,A正在玩游戏,B去叫A一起吃饭,这个时候B有两种选择,一是等A玩完游戏一起去吃饭,二是去干其他事情,并告诉A玩完通知他。是的,前一种选择就是单线程,后一种是多线程,但是,如果使用多线程做这件事就会出现B为了得到A的通知不得不隔一段时间停下手中的活看看A是否通知,而使用while循环似乎又不太恰当,降低程序性能,这个时候就可用异步处理。

异步示例

异步是py3.4+的版本引入的,在py3.5+中正式定义其操作符async与await等,以下我将使用py3.4+的语法写一些异步程序,这些程序在之后的版本中也是可以运行的。

import asyncio

# 获取事件循环
loop = asyncio.get_event_loop()


@asyncio.coroutine
def a_persion():
    for i in range(5):
        yield from asyncio.sleep(3)
        print('我是A,我在玩游戏')
    return "A游戏玩完了!"


def a_statu(say):
    print(say.result())


@asyncio.coroutine
def b_person(x):
    for i in range(7):
        yield from asyncio.sleep(3)
        print('我是B,我在等A,顺便看书', x)

a_task = asyncio.ensure_future(a_persion())  # 新建任务
a_task.add_done_callback(a_statu)  # 设置回调函数
b_task = asyncio.ensure_future(b_person('无聊'))

tasks = [a_task, b_task] # 任务列表

loop.run_until_complete(asyncio.wait(tasks))

print('A和B高高兴兴的吃饭去了!')

执行结果:

我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
我是A,我在玩游戏
我是B,我在等A,顺便看书 无聊
A游戏玩完了!
我是B,我在等A,顺便看书 无聊
我是B,我在等A,顺便看书 无聊
A和B高高兴兴的吃饭去了!

很清楚的,我们看到,A与B的事情都同时在做,并且最后他们都可以同时出去吃饭,这就是异步,如果让你用多线程方式去做是否比较麻烦?

用异步用来写一个爬虫框架

网络爬虫的决速步骤在下载网页源码步骤上,此步耗时最大,因此,我将用异步的方式对其优化,实现并行地下载页面。我们会用到aiohttp这个目前还是第三方的库,可通过pip安装之。同时,我将使用Scrapy的风格写出这个框架,也是向Scrapy团队致敬。我们暂且叫它py3spider吧!

引入相应的包

import asyncio
import aiohttp
import warnings

定义请求类

class Request:

    def __init__(self, url, callback, meta=None):
        self.url = url
        self.callback = callback
        self.meta = meta

用于封装Request请求,实现对url及其解析函数的暂存

定义响应类

class Response:

    def __init__(self, body, url, meta=None, callback=None):
        self.body = body
        self.url = url
        self.meta = meta
        self.callback = callback

用于对下载完毕的数据进行暂存

定义中间件

class Middleware:

    def __init__(self):
        self.web = aiohttp.ClientSession()

    def __del__(self):
        self.web.close()

    @asyncio.coroutine
    def get(self, req):
        response = yield from self.web.get(req.url)
        body = yield from response.read()
        response.close()
        return Response(body, req.url, req.meta, req.callback)

    def save(self, data):
        print(data)

用于下载页面与保存数据等

定义爬虫接口

class Spider:
    start_urls = []

    def parse(self, response):
        pass

所有爬虫都需实现此接口,start_urls是入口列表,parse函数是第一个调用的解析函数

定义调度器

class Scheduler:

    def __init__(self, spider, middleware=Middleware()):
        self.__spider = spider
        self.__midw = middleware
        self.__request = []  # 请求队列
        self.__response = []  # 回应队列
        self.thread_num = 16  # 线程数

    def __start(self):
        for url in self.__spider.start_urls:
            self.__request.append(Request(url, self.__spider.parse))

    def __putRequest(self):
        loop = asyncio.get_event_loop()

        def putRes(fur):
            self.__response.append(fur.result())
        tasks = []
        while self.__request:
            try:
                num = 0
                if len(self.__request) < self.thread_num:
                    num = len(self.__request)
                else:
                    num = self.thread_num
                for i in range(num):
                    task = asyncio.ensure_future(
                        self.__midw.get(self.__request.pop()))
                    task.add_done_callback(putRes)
                    tasks.append(task)
                if tasks:
                    loop.run_until_complete(asyncio.wait(tasks))
                    self.__doResponse()
                tasks.clear()
            except Exception as e:
                warnings.warn(e)
        loop.close()

    def __doResponse(self):
        while self.__response:
            res = self.__response.pop()
            try:
                for req in res.callback(res):
                    if type(req) == Request:
                        self.__request.append(req)
                    else:
                        self.__midw.save(req)
            except Exception as e:
                warnings.warn(e)

    def start(self):
        self.__start()
        self.__putRequest()
        print("The end ...")

执行爬虫的调度任务,爬虫、中间件、线程都可以自己设置。将上面的代码封装成py3spider ,使用方法与Scrapy类似,只需要修改极少代码甚至不需要修改Scrapy的爬虫源码即可使用。

调用示例

以古诗文网的名句项目为例说明,想了解古诗文网的详情可以用浏览器打开http://so.gushiwen.org/mingju/

这个网址查看。

from py3spider import *
from bs4 import BeautifulSoup
import re


class MingSpider(Spider):

    start_urls = ['http://so.gushiwen.org/mingju/']

    def parse(self, response):
        soup = BeautifulSoup(response.body.decode('utf-8', 'ignore'), 'lxml')
        div = soup.find('div', attrs = {'class': 'sright'})
        for a in div.find_all('a', attrs={'href': re.compile('^/mingju/Default.aspx.*')}):
            yield Request('http://so.gushiwen.org/'+a.attrs['href'], callback=self.parseDetail, meta={'title': a.text})

    def parseDetail(self, response):
        soup = BeautifulSoup(response.body.decode('utf-8', 'ignore'), 'lxml')
        # <a style=" float:left;" target="_blank" href="/mingju/ju_162.aspx">山有木兮木有枝,心悦君兮君不知。</a>
        for a in soup.find_all('a', attrs={'href': re.compile('^/mingju/ju_.*')}):
            if a and a.text:
                yield {'title': response.meta['title'], 'text': a.text}
        nt = soup.find('a', text='下一页')
        if nt and nt.attrs and 'href' in nt.attrs:
            yield Request('http://so.gushiwen.org/mingju/'+nt.attrs['href'], callback=self.parseDetail, meta=response.meta)


class MyMiddleware(Middleware):

    def __init__(self):
        super(MyMiddleware, self).__init__()

    def save(self, data):
        with open(data['title']+'.txt', 'a') as f:
            f.write(data['text']+'\n')

scheduler = Scheduler(MingSpider(), middleware = MyMiddleware())
scheduler.start()

执行上面的代码即可很快的爬行名句项目。关于py3spider这个项目我已上传pypi ,可以通过pip 安装之。

相比于Scrapy,这个安装更容易,第三方库仅aiohttp一个。

猜你喜欢

转载自blog.csdn.net/chenlnehc/article/details/77871861