這篇文章的題目有點大,但這並非說我自覺對Python爬蟲這塊有多大看法,我只不過是想將本身的一些經驗付諸於筆,對於如何寫一個爬蟲框架,我想一步一步地結合具體代碼來說述如何從零開始編寫一個本身的爬蟲框架css
2018年到現在,我花精力比較多的一個開源項目算是Ruia
了,這是一個基於Python3.6+
的異步爬蟲框架,當時也得到一些推薦,好比Github Trending Python
語言榜單第二,目前Ruia
還在開發中,Star
數目不過700+
,若是各位有興趣,歡迎一塊兒開發,來波star
我也不會拒絕哈~html
什麼是爬蟲框架
說這個以前,得先說說什麼是框架:python
MVC
開發規範Django
框架就是MVC
的開發框架,但它還提供了其餘基礎功能幫助咱們快速開發,好比中間件、認證系統等框架的關注點在於規範二字,好,咱們要寫的Python爬蟲框架規範是什麼?git
很簡單,爬蟲框架就是對爬蟲流程規範的實現,不清楚的朋友能夠看上一篇文章談談對Python爬蟲的理解,下面總結一下爬蟲流程:github
這三個流程有沒有可能以一種優雅的形式串聯起來,Ruia
目前是這樣實現的,請看代碼示例:shell
能夠看到,Item & Field
類結合一塊兒實現了字段的解析提取,Spider
類結合Request * Response
類實現了對爬蟲程序總體的控制,從而能夠如同流水線通常編寫爬蟲,最後返回的item
能夠根據使用者自身的需求進行持久化,這幾行代碼,咱們就實現了獲取目標網頁請求、字段解析提取、持久化這三個流程express
實現了基本流程規範以後,咱們繼而就能夠考慮一些基礎功能,讓使用者編寫爬蟲能夠更加輕鬆,好比:中間件(Ruia裏面的Middleware)、提供一些hook
讓用戶編寫爬蟲更方便(好比ruia-motor)cookie
這些想明白以後,接下來就能夠愉快地編寫本身心目中的爬蟲框架了session
如何踏出第一步
首先,我對Ruia爬蟲框架的定位很清楚,基於asyncio & aiohttp
的一個輕量的、異步爬蟲框架,怎麼實現呢,我以爲如下幾點須要遵照:框架
什麼是爬蟲框架現在咱們已經很清楚了,如今急須要作的就是將流程規範利用Python語言實現出來,怎麼實現,分爲哪幾個模塊,能夠看以下圖示:

同時讓咱們結合上面一節的Ruia
代碼來從業務邏輯角度看看這幾個模塊究竟是什麼意思:
這四個部分咱們能夠簡單地使用五個類來實現,在開始講解以前,請先克隆Ruia
框架到本地:
# 請確保本地Python環境是3.6+ git clone https://github.com/howie6879/ruia.git # 安裝pipenv pip install pipenv # 安裝依賴包 pipenv install --dev
而後用PyCharm
打開Ruia
項目:
選擇剛剛pipenv
配置好的python解釋器:
此時能夠完整地看到項目代碼:
好,環境以及源碼準備完畢,接下來將結合代碼講述一個爬蟲框架的編寫流程
Request & Response
Request
類的目的是對aiohttp
加一層封裝進行模擬請求,功能以下:
Response
類返回接下來就簡單了,不過就是實現上述需求,首先,須要實現一個函數來抓取目標url
,好比命名爲fetch
:
import asyncio import aiohttp import async_timeout from typing import Coroutine class Request: # Default config REQUEST_CONFIG = { 'RETRIES': 3, 'DELAY': 0, 'TIMEOUT': 10, 'RETRY_FUNC': Coroutine, 'VALID': Coroutine } METHOD = ['GET', 'POST'] def __init__(self, url, method='GET', request_config=None, request_session=None): self.url = url self.method = method.upper() self.request_config = request_config or self.REQUEST_CONFIG self.request_session = request_session @property def current_request_session(self): if self.request_session is None: self.request_session = aiohttp.ClientSession() self.close_request_session = True return self.request_session async def fetch(self): """Fetch all the information by using aiohttp""" if self.request_config.get('DELAY', 0) > 0: await asyncio.sleep(self.request_config['DELAY']) timeout = self.request_config.get('TIMEOUT', 10) async with async_timeout.timeout(timeout): resp = await self._make_request() try: resp_data = await resp.text() except UnicodeDecodeError: resp_data = await resp.read() resp_dict = dict( rl=self.url, method=self.method, encoding=resp.get_encoding(), html=resp_data, cookies=resp.cookies, headers=resp.headers, status=resp.status, history=resp.history ) await self.request_session.close() return type('Response', (), resp_dict) async def _make_request(self): if self.method == 'GET': request_func = self.current_request_session.get(self.url) else: request_func = self.current_request_session.post(self.url) resp = await request_func return resp if __name__ == '__main__': loop = asyncio.get_event_loop() resp = loop.run_until_complete(Request('https://docs.python-ruia.org/').fetch()) print(resp.status)
實際運行一下,會輸出請求狀態200
,就這樣簡單封裝一下,咱們已經有了本身的請求類Request
,接下來只須要再完善一下重試機制以及將返回的屬性封裝一下就基本完成了:
# 重試函數 async def _retry(self): if self.retry_times > 0: retry_times = self.request_config.get('RETRIES', 3) - self.retry_times + 1 self.retry_times -= 1 retry_func = self.request_config.get('RETRY_FUNC') if retry_func and iscoroutinefunction(retry_func): request_ins = await retry_func(weakref.proxy(self)) if isinstance(request_ins, Request): return await request_ins.fetch() return await self.fetch()
最終代碼見ruia/request.py
便可,接下來就能夠利用Request
來實際請求一個目標網頁,以下:
這段代碼請求了目標網頁https://docs.python-ruia.org/
並返回了Response
對象,其中Response
提供屬性介紹以下:
Field & Item
實現了對目標網頁的請求,接下來就是對目標網頁進行字段提取,我以爲ORM
的思想很適合用在這裏,咱們只須要定義一個Item
類,類裏面每一個屬性均可以用Field
類來定義,而後只須要傳入url
或者html
,執行事後Item
類裏面 定義的屬性會自動被提取出來變成目標字段值
可能提及來比較拗口,下面直接演示一下可能你就明白這樣寫的好,假設你的需求是獲取HackerNews
網頁的title
和url
,能夠這樣實現:
import asyncio from ruia import AttrField, TextField, Item class HackerNewsItem(Item): target_item = TextField(css_select='tr.athing') title = TextField(css_select='a.storylink') url = AttrField(css_select='a.storylink', attr='href') async def main(): async for item in HackerNewsItem.get_items(url="https://news.ycombinator.com/"): print(item.title, item.url) if __name__ == '__main__': items = asyncio.run(main())
從輸出結果能夠看到,title
和url
屬性已經被賦與實際的目標值,這樣寫起來是否是很簡潔清晰也很明瞭呢?
來看看怎麼實現,Field
類的目的是提供多種方式讓開發者提取網頁字段,好比:
因此咱們只須要根據需求,定義父類而後再利用不一樣的提取方式實現子類便可,代碼以下:
class BaseField(object): """ BaseField class """ def __init__(self, default: str = '', many: bool = False): """ Init BaseField class url: http://lxml.de/index.html :param default: default value :param many: if there are many fields in one page """ self.default = default self.many = many def extract(self, *args, **kwargs): raise NotImplementedError('extract is not implemented.') class _LxmlElementField(BaseField): pass class AttrField(_LxmlElementField): """ This field is used to get attribute. """ pass class HtmlField(_LxmlElementField): """ This field is used to get raw html data. """ pass class TextField(_LxmlElementField): """ This field is used to get text. """ pass class RegexField(BaseField): """ This field is used to get raw html code by regular expression. RegexField uses standard library `re` inner, that is to say it has a better performance than _LxmlElementField. """ pass
核心類就是上面的代碼,具體實現請看ruia/field.py
接下來繼續說Item
部分,這部分其實是對ORM
那塊的實現,用到的知識點是元類
,由於咱們須要控制類的建立行爲:
class ItemMeta(type): """ Metaclass for an item """ def __new__(cls, name, bases, attrs): __fields = dict({(field_name, attrs.pop(field_name)) for field_name, object in list(attrs.items()) if isinstance(object, BaseField)}) attrs['__fields'] = __fields new_class = type.__new__(cls, name, bases, attrs) return new_class class Item(metaclass=ItemMeta): """ Item class for each item """ def __init__(self): self.ignore_item = False self.results = {}
這一層弄明白接下來就很簡單了,還記得上一篇文章《談談對Python爬蟲的理解》裏面說的四個類型的目標網頁麼:
本質來講就是要獲取網頁的單目標以及多目標(多頁面能夠放在Spider那塊實現),Item
類只須要定義兩個方法就能實現:
target_item
具體實現見:ruia/item.py
Spider
在Ruia
框架中,爲何要有Spider
,有如下緣由:
Spider
能夠對這些進行 有效的管理接下來講說代碼實現,Ruia
框架的API
寫法我有參考Scrapy
,各個函數之間的聯結也是使用回調,可是你也能夠直接使用await
,能夠直接看代碼示例:
from ruia import AttrField, TextField, Item, Spider class HackerNewsItem(Item): target_item = TextField(css_select='tr.athing') title = TextField(css_select='a.storylink') url = AttrField(css_select='a.storylink', attr='href') class HackerNewsSpider(Spider): start_urls = [f'https://news.ycombinator.com/news?p={index}' for index in range(1, 3)] async def parse(self, response): async for item in HackerNewsItem.get_items(html=response.html): yield item if __name__ == '__main__': HackerNewsSpider.start()
使用起來仍是挺簡潔的,輸出以下:
[2019:03:14 10:29:04] INFO Spider Spider started! [2019:03:14 10:29:04] INFO Spider Worker started: 4380434912 [2019:03:14 10:29:04] INFO Spider Worker started: 4380435048 [2019:03:14 10:29:04] INFO Request <GET: https://news.ycombinator.com/news?p=1> [2019:03:14 10:29:04] INFO Request <GET: https://news.ycombinator.com/news?p=2> [2019:03:14 10:29:08] INFO Spider Stopping spider: Ruia [2019:03:14 10:29:08] INFO Spider Total requests: 2 [2019:03:14 10:29:08] INFO Spider Time usage: 0:00:03.426335 [2019:03:14 10:29:08] INFO Spider Spider finished!
Spider
的核心部分在於對請求URL
的請求控制,目前採用的是生產消費者模式來處理,具體函數以下:
詳細代碼,見ruia/spider.py
更多
至此,爬蟲框架的核心部分已經實現完畢,基礎功能一樣一個不落地實現了,接下來要作的就是:
項目地址點擊閱讀原文或者在github
搜索ruia
,若是你有興趣,請參與進來吧!
若是以爲寫得不錯,點個好看來個star
唄~