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


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

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

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

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

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

另外,github上有一個項目,里面有各種500行左右的代碼實現的系統,看了幾個非常贊https://github.com/aosabook/500lines

二.如何使用

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

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'  #修改導出的文件
 

三.原理

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

 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中討論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM