etlpy: 並行爬蟲和數據清洗工具(開源)

etlpy是python編寫的網頁數據抓取和清洗工具,核心文件etl.py不超過500行,具有以下特色html

  • 爬蟲和清洗邏輯基於xml定義,不需手工編寫
  • 基於python生成器,流式處理,對內存無要求
  • 內置線程池,支持串行和並行處理
  • 內置正則解析,html轉義,json轉換等數據清洗功能,直接輸出可用文件
  • 插件式設計,可以很是方便地增長其餘文件和數據庫格式
  • 可以支持幾乎一切網站,能自動填入cookie

github地址: https://github.com/ferventdesert/etlpy, 歡迎star!python

運行須要python3和lxml, 使用pip3 install lxml便可安裝。內置的工程project.xml,包含了鏈家和大衆點評兩個爬蟲的配置示例。git

etlpy具備鮮明的函數式風格特徵,使用了大量的動態類型,惰性求值,生成器和流式計算。程序員

另外,github上有一個項目,裏面有各類500行左右的代碼實現的系統,看了幾個很是贊https://github.com/aosabook/500linesgithub

二.如何使用

當從網頁和文件中抓取和處理數據時,咱們總會被複雜的細節,好比編碼,奇怪的Html和異步ajax請求所困擾。etlpy可以方便地處理這些問題。ajax

etlpy的使用很是簡單,先加載工程,以後便可返回一個生成器,返回所需數量便可。下面的代碼,可以在20分鐘內,獲取大衆點評網站上海的所有美食列表,總共16萬條,30MB.正則表達式

import etl;
etl.LoadProject('project.xml');
tool = etl.modules['大衆點評門店'];
datas = tool.QueryDatas()
for r in datas:
    print(r)

結果以下:數據庫

{'區域': '川沙', '標題': '胖哥倆肉蟹煲(川沙店)', '區縣': '', '地址': '川沙鎮川沙路5558弄綠地廣場三號樓', '環境': '9.0', '介紹': '', '類型': '其餘', '總店': '胖哥倆肉蟹煲', 'ID': '/shop/19815141', '口味': '9.1', '星級': '五星商戶', '總店id': '19815141', '點評': '2205', '其餘': '訂座:本店支持在線訂座', '均價': 67, '服務': '8.9'}
{'區域': '金楊地區', '標題': '上海小南國(金橋店)', '區縣': '', '地址': '張楊路3611弄金橋國際商業廣場6座2樓', '環境': '8.8', '類型': '本幫江浙菜', 'ID': '/shop/3525763', '口味': '8.6', '星級': '準五星商戶', '點評': '1973', '其餘': '', '均價': 190, '服務': '8.5'}
{'區域': '臨沂/南碼頭', '標題': '新弘聲酒家(臨沂路店)', '區縣': '', '地址': '臨沂路8弄42號', '環境': '8.7', '介紹': '新弘聲酒家!僅售85元!價值100元的午市代金券1份,全場通用,可疊加使用。', '類型': '本幫江浙菜', '總店': '新弘聲酒家', 'ID': '/shop/19128637', '口味': '9.0', '星級': '五星商戶', '總店id': '19128637', '點評': '621', '其餘': '團購:新弘聲酒家!僅售85元!價值100元的午市代金券1份,全場通用,可疊加使用。', '均價': 87, '服務': '8.8'}
{'區域': '張江', '標題': '阿拉人家上海菜(浦東長泰廣場店)', '區縣': '', '地址': '祖沖之路1239弄1號長泰廣場10號樓203', '環境': '8.9', '介紹': '僅售42元,價值50元代金券', '類型': '本幫江浙菜', '總店': '阿拉人家上海菜', 'ID': '/shop/21994899', '口味': '8.8', '星級': '準五星商戶', '總店id': '21994899', '點評': '1165', '其餘': '團購:僅售42元,價值50元代金券', '均價': 113, '服務': '8.8'}

固然,以上方法是串行執行,你也能夠選擇並行執行以獲取更快的速度:編程

tool.mThreadExecute(threadcount=20,execute=False,callback=lambda d:print(d))
可設置線程數,對獲取的每一個數據的回調方法,以及是否執行其中的執行器(下文有解釋)。
etlpy的執行邏輯基於xml文件,不建議手工編寫xml,而是使用筆者開發的另外一款圖形化爬蟲工具,能夠經過圖形拖拽的方式設計並生成工程文件,這套工具也即將開源,由於暫時還沒想到較好的名字。基於C#/WPF開發,經過這套工具,十分鐘內就能完成大衆點評的採集程序的編寫,若是手工編碼,一個熟練的python程序員可能得寫一天。該工具生成的xml,便可被etlpy解析,生成跨平臺的多線程爬蟲。
image
你能夠選擇手工修改xml,或是在代碼中直接修改,來採集不一樣城市,或是輸出到不一樣的文件:
tool.AllETLTools[0].arglists=['1']  #修改城市,1爲上海,2爲北京,參考大衆點評的網頁定義
tool.AllETLTools[-1].NewTableName= 'D:\大衆點評.txt'  #修改導出的文件
 

三.原理

咱們將每一步驟定義爲獨立的模塊,將其串成一條鏈條(咱們稱之爲流)。以下圖所示:json

 image_thumb[4]

C#版本原理

鑑於博客園很多讀者熟悉C#,咱們不妨先用C#的例子來說解:

其本質是動態組裝Linq, 其數據鏈爲IEnumerable<IFreeDocument>。 IFreeDocument是 IDictionary<string, object>接口的擴展。Linq的Select函數可以對流進行變換,在本例中,就是對字典不一樣列的操做(增刪改),不一樣的模塊定義了一個完整的Linq流:

result= source.Take(mount).where(d=>module0.func(d)).select(d=>Module1.func(d)).select(d=>Module2.func(d))….

Python版本原理

python的生成器相似於C#的Linq,是一種流式迭代。etlpy對生成器作了擴展,實現了生成器級聯,並聯和交叉(笛卡爾積)

def Append(a, b):
    for r in a:
        yield r;
    for r in b:
        yield r;

def Cross(a, genefunc, tool):
    for r1 in a:
        for r2 in genefunc(tool, r1):
            for key in r1:
                r2[key] = r1[key]
            yield r2;

 

那麼,生成器生成的是什麼呢?咱們選用了Python的字典,這種鍵值對的結構很好用。能夠將全部的模塊分爲四種類型:

生成器(GE):如生成100個字典,鍵爲1-100,值爲‘1’到‘100’

轉換器(TF):如將地址列中的數字提取到電話列中

過濾器(FT):如過濾全部某一列的值爲空的的字典

執行器(GE):如將全部的字典存儲到MongoDB中。

咱們如何將這些模塊組合成完整鏈條呢?因爲Python沒有Linq,咱們經過組合生成器來獲取新的生成器,這個函數定義以下:

def __generate__(self, tools, generator=None, execute=False):
        for tool in tools:
            if tool.Group == 'Generator':
                if generator is None:
                    generator = tool.Func(tool, None);
                else:
                    if tool.MergeType == 'Append':
                        generator = extends.Append(generator, tool.Func(tool, None));
                    elif tool.MergeType == 'Merge':
                        generator = extends.MergeAll(generator, tool.Func(tool, None));
                    elif tool.MergeType == 'Cross':
                        generator = extends.Cross(generator, tool.Func, tool)
            elif tool.Group == 'Transformer':
                generator = transform(tool, generator);
            elif tool.Group == 'Filter':
                generator = filter(tool, generator);
            elif tool.Group == 'Executor' and execute:
                generator = tool.Func(tool, generator);
        return generator;

如何定義模塊呢?若是是先定義基類,而後從基類繼承,這種方式依然要寫大量的代碼,並且不夠Pythonic(我C#版本的代碼就是這樣寫的)。

以清除字符串中先後空白的字符爲例(C#中的trim, Python中的strip),咱們可以定義這樣的函數:

def TrimTF(etl, data):    
    return data.strip();

以後,經過讀取配置文件,運行時動態地爲一個基礎對象添加屬性和方法,從一個簡單的TrimTF函數,生成一個具有一樣功能的類。 整個etlpy的編寫思路,就是從函數生成類,再最後將類的對象(模塊)組合成流。

至於爬蟲獲取HTML正文的信息,則使用了XPath,而非正則表達式,固然你也可使用正則。XPath也是自動生成的,具體的原理將在以後的博文中講解。etlpy本質上是從新定義了抓取和清洗的原語,是一種新的語言(DSL),從而大大下降了編寫這類應用的成本和複雜度。

(串行模式的QueryDatas函數,有一個etlcount的可選參數,你能夠分別將其值設爲從1到n,觀察數據是如何被一步步地組合出來的)

三.例子

採集鏈家

先以抓取鏈家地產爲例,咱們來說解這種流的強大:如何採集全部二手房數據呢?這涉及到翻頁。

image

翻頁時,咱們會看到頁面是這樣變換的:

http://bj.lianjia.com/ershoufang/pg2/

http://bj.lianjia.com/ershoufang/pg3/

所以,須要構造一串上面的url. 聰明的你確定會想到,應當先生成一組序列,從1到100(假設咱們只抓取前100頁)。

再經過MergeTF函數,從1-100生成上面的url列表。如今總共是100個url.

再經過爬蟲轉換器CrawlerTF,每一個頁面可以生成30個二手房信息,所以可以生成100*30個頁面,但因爲是基於流的,因此這3000個信息是不斷yield出來的,每生成一個,後續的流程,如去除亂碼,提取數字,保存到文件,都會執行。這樣咱們就獲取了全部的信息。

不一樣的流,能夠組合爲更高級的流。例如,想要獲取全部房地產的數據,能夠分別定義鏈家,我愛我家等地產公司的流,再經過流將多個流拼接起來。

採集大衆點評

大衆點評的採集難度更大,每種門類只能翻到第50頁,所以想要獲取所有數據就必須想辦法。

以北京美食爲例,若是按不一樣美食的門類(咖啡廳,火鍋,小吃…)和區域(海淀,西城,東城…)區分,美食頁面就沒有五十頁了。因此,首先生成北京全部區域的流(project中「大衆點評區域」,感興趣的讀者能夠試着獲取這個流看看),再生成全部美食門類的流(大衆點評門類)。而後再將這兩個流作交叉(m*n),再組合獲取了每一個種類的url, 經過url獲取頁面,再經過XPath獲取對應門類的門店數量:

image

上文中的1238,也就是朝陽區的北京菜總共有1238家。

再經過python腳本計算要翻的頁數,由於每頁15個,那麼有int(1238/15.0)+1頁,記做q。 總共要抓取的頁面數量,是一個(m,n,q)的異構立方體,不一樣的(m,n)都對應不一樣的q。 以後,就能夠用相似於鏈家的方法,抓取全部頁面了。

四.優化和細節

爲了保證講解的簡單,我省略了大量實現的細節,其實在其中作了不少的優化。

1. 修改流,獲取不一樣城市的信息

還以大衆點評爲例,咱們但願只修改一個模塊,就能切換北京,上海等美食的信息。

image

北京和上海的美食門類和區域列表都不同,因此兩個子流的隊首的生成器,定義了城市的id。若是想修改城市,須要修改三個生成器。這太麻煩了,所以,etlpy採用了動態替換的方法。 若是主流中定義了與子流中同名的模塊,只要修改了主流,主流就能夠對子流完成修改。

2. 並行優化

最簡單的並行化,應該從流的源頭開始:

image

但若是隊首隻有一個元素,那麼這種方法就很是低下了:

image

一種很是簡單的思路,是將其切成兩個流,並行在流中完成。

image

以大衆點評爲例, 北京有14個區縣,有30種美食類型,那麼先經過流1,獲取420個元素,再以420個元素的基礎上,進行並行,這樣速度就快不少了。你也能夠在14個區縣以後插入並行化,那麼就有14個子任務。etlpy經過一個ToListTF模塊(它什麼都不幹)做爲標識,做爲流1和流2的分割符。

4.一些參數的說明

OneInput=True說明函數只須要字典中的一個值,此時傳到函數裏的只有dict[key],不然傳遞整個dict

OneOutput=True說明函數可能輸出多個值,所以函數直接修改dict並返回null, 不然返回一個value,etlpy在函數外部修改dict.

IsMultiYield=True說明函數會返回生成器。

其餘參數可具體參考python代碼。

五.展望

使用xml做爲工程的配置文件有顯然的好處,由於可以被各類語言方便地讀取,可是噪音太多,不易手工編寫,若是能設計一個專用的數據清洗語言,那麼應該會好不少。其實用圖形化編程,效率會特別高。

etlpy的思想,來自於講解Lisp的一本書《計算機程序的構造與解釋》(SICP),書評在此:Lisp和SICP

可視化軟件會在一個月內所有開源,解放程序員的大腦和雙手,號稱爬蟲的終極武器。敬請期待。

有任何問題,歡迎留言交流,或在Github中討論。

相關文章
相關標籤/搜索