截取網頁的架構設計
我們先前談到了抓取網頁用的工具,但是光有工具是不夠的,良好的設計也是必要的,這次我們就來談談設計
最早我寫一個抓取amazon.com商品資料的程式,當時沒仔細思考設計的問題,而且在一開始低估了問題的複雜程度,導至後來在修改時整個是一團混亂,我把抓取網頁的流程分成幾個部份,然而這些流程之間有著很多細小複雜的溝通和互相依賴的關係,到了後來我發現我哪裡在寫程式,這根本是一團義大利麵! 此時我終於深刻體會到什麼叫漣漪效應,當我修改一小部份程式,有很多依賴此的程式也跟著需要修改,間接依賴的程式片段一樣需要修改,程式的改變有如投入池子中的石子,修改在程式碼間散播開來,再也沒有比這個更糟的設計了,除了修改的問題,測試更是一個嚴重的問題,當我發現一個錯誤,我無法確定這個錯誤到底是屬於誰的,它們之間有這麼多關係,每個人都有嫌疑,我明白這程式繼續寫下去就只有死路一條,於是我開始思考怎樣的設計才是正確的
最明顯的錯誤
最明顯的錯誤就是,流程和流程之間有太多關係了,它們互相依賴,造成藕合度大大提升,牽一髮動全身,因此我意識到每個流程應該都要和其它任何流程沒有任何關係,它們只在乎它們的輸入,以及如何處理它們,於是我就聯想到了責任鏈設計模式,以及工廠的生產線,我想像資料進來,從最先的原料,進入一開始的工廠加工,接著被送到下一個工廠處理,每個工廠只知道如何對進入的產品加工,對於前一個工廠、後一個工廠,對所有工廠都一無所知,這才是將處理流程從藕合的關係中鬆綁的正確設計,於是我做出了設計上的修改,重新寫過了程式,我將幾個大流程分開成獨立的物件,並單獨做測試,確定每個流程都如預期般,有如預期的正確輸出,最後再將這些流程串起來,果然這樣的做法果然解決了問題,但是問題還沒有結束
一直有種強烈的直覺
很多時候沒辦法明白的說出來,但就是有些強烈的直覺,感覺到設計上可以做到某種程度,但卻還無法用言語表達,將每個流程分開程獨立的程式處理,雖然鬆綁藕合,但是卻沒有良好的重用性,到後來我發現Twisted,使用它的Deferred物件,從getPage的Deferred物件中增加parsing的callback,我意識到,不止一個大的流程,從抓取網頁、解析網頁、儲存資料,每一個步驟事實上應該都是獨立的,如果以一大流程做為單位,當我需要使用其它抓取網頁的機制,解析網頁的程式卻要從那個大流程中取出來,很明顯地,以大流程做為單位太大了,而且流程應該是由幾個小步驟組合而成,不應該把它們綁在一起,在這時我還沒有明確的想法,直到…
遇到GStreamer
為了寫某些程式的需要,我找到了一款Open source的影音串流處理的函式庫,我看到了它Pipeline的機制,此時我眼睛為之一亮,這不就是我想要的設計嗎? 由元素組成串在一起的pipeline,每個元素只有特定處理資料的方式,然後往後面的元素傳
我的設計
於是我開始構思我的網頁抓取的pipeline,起先我試著模仿它那種pipeline的設計方式,但是它的Element相當複雜,並不適合網頁抓取的pipeline,於是我把心思拉回處理網頁的每個單一步驟該有的特性,最後我做出了這樣的設計
在這其中,我除了考慮到藕合上的問題,同樣也考慮到效率等等的問題,Element本身只專注在於處理資料這件事,資料由input方法輸入,而handleData處理資料,當資料處理完成需要輸出使用outputEvent來通知外界資料處理完成,當資料都處理完成也輸出完成時,就用requestEvent通知外界可以輸入更多資料,然而如果在handleData的過程中發生問題,就使用failureEvent告知外界錯誤訊息,大至上的行為是如此,但在細節上還有一些問題沒想清楚
而Bin本身是一個Element的容器,負責接受來自Element的事件通知,然後做出對應的處理,Pipeline就是一個Bin的子類別,它負責轉送到對應的Element去,或是放入queue中等待request事件再送出,它都使用CallInThread來呼叫element的input方法,這表示,所有工作都是非同步進行的,不需要等一筆資料被處理完成後,才能接受新的資料,只要一個Element的資料處理完,可以馬上再處理下一筆,這是提升效率的方法之一
值得注意的是,Pipeline和Bin本身一樣也是一個Element,這表示它可以被當做Element和其它Element串在一起,或是加入其它Bin中,這讓設計上更有彈性
Retry裝飾者
有了這樣的概念確立後,我引進了更多設計上的應用,例如裝飾者設計樣式,我設計了一個Retry的裝飾者,可以把getPage等等可能會失敗的Element包裝起來,自動進行特定次數的重試,而不必為了getPage特地重寫重試的版本,有其它可能出錯的Element都可以用此裝飾者包裝起來
Dispatcher
然而,到目前為止我們的效能還是不夠好,因為同一個時間裡只有一條pipeline在運行,我們需要的是在同一個時間內可以有多條的pipeline運作,因此設計了一個Dispatcher的Bin,可以新增很多條Pipeline在其中,然後input資料就會自動分派給pipeline
If判斷元素
只有這樣還不夠,考慮一下我們如果需要抓取分頁的網頁,我們一頁一頁往下抓,要判斷是否有下一頁,繼續網下抓取或是停止,為此我設計了If判斷用的Element,它會根據一個條件函數來決定輸出的port
當這一切組合在一起
到目前為止所有提到的都是Element,這表示他們可以被替換掉在任何以上的結構中,Dispatcher所分派的對象不一定要是Pipeline,也可以是其它Element,我為了我目前工作用這樣的東西重新組合重寫一次,組合起來像是這樣
而Dispatcher在外面將這個Pipeline重覆了幾次,因此同時可以執行很多個Pipeline,GetPage、Parser等等都是常用的Element,而只有Get book page detail等Element是我們自行設計的Element,這將重覆利用發揮到了極緻,有強大的彈性,你可以在裡面看到一個迴圈,事實上它只是串接著一個If的Element然後Link條件為真的Port和為假的Port到不同元素而已
我最早的設計是如上圖所示,但是後來考慮到Amazon網路書店的Review頁面,往往是商品資料頁面的好幾百倍和千倍,因此,到後來我將商品資料和Review資料的Pipeline分開,分別用Dispatcher包裝起來後串接在一起,Review的Pipeline數量應該要是商品資料的很多倍,如此一來才能更快消化完Review頁面的工作
未來
到此,相信已經可以感受到這樣設計的威力,我希望將我的程式整理成函式庫,釋放出來成為Open source的Project,專案的名字我還在想,目前考慮用中文字”川”或是DStreamer之類的名字,如果有任何想法,也歡迎提供給我 😛
除此之外,如果你有玩過GStreamer,應該知道它有一個用指令來播放串流媒體的功能,像這樣子
gst-launch -v filesrc location=sine.ogg ! oggdemux ! vorbisdec ! audioconvert ! alsasink
就可以播放ogg的音樂檔案,這只是簡單的例子,它還可以做更複雜的事,例如從麥克風讀取音訊即時壓縮成ogg或mp3格式,且同時播放出來之類的
除了Library,我同樣希望寫出一個像這樣的工具程式,讓某些抓網頁的程式可以被簡化成像這樣的指令,例如
dst-launch GetPage url=”http://www.google.com” ! RegexParser re=”<title>(.*?)</title>” output=”$1″ ! FileOutput filename=”google_title.txt”
像這樣簡單的指令就能抓取Google網頁的標題然後存成文字檔,除此之外,甚至是GUI介面來像UML那樣設計抓取網頁的流程
有興趣參與或有任何想法都歡迎給個評論,謝謝
之前有向你提過的東西,
http://master.branda.to/downloads/pywebtool/
目錄裡面除了主程式,還包括一些使用範例 (script),請參考!
感謝你 我會參考看看 😛
請問最後有做成獨立的project嗎?? 好期待唷~