天天看點

Py3異步爬蟲淺涉Py3.x異步網絡爬蟲淺涉

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():
        yield from asyncio.sleep()
        print('我是A,我在玩遊戲')
    return "A遊戲玩完了!"


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


@asyncio.coroutine
def b_person(x):
    for i in range():
        yield from asyncio.sleep()
        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 =   # 線程數

    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 = 
                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" target="_blank" rel="external nofollow" >山有木兮木有枝,心悅君兮君不知。</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

一個。