為了承載和封裝資料,需要先宣告一些基本的資料結構。網路爬蟲框架中的各個模組都會用到這些資料結構,所以可以說它們是這一程式的基礎。
在分析網路爬蟲框架的需求時,提到過這樣幾類資料——請求、響應、條目,下面我們逐個講解它們的宣告和設計理念。
請求用來承載向某一個網路地址發起的 HTTP 請求,它由排程器或分析器生成並傳遞給下載器,下載器會根據它從遠端伺服器下載相應的內容。因此,它有一個 net/http.Request 型別的欄位。
不過,為了減少不必要的零值生成(http.Request 是一個結構體型別,它的零值不是 nil)和範例複製,我們把 *http.Request 作為該欄位的型別。下面是 base.Request 型別的宣告的第一個版本:
//資料請求的型別
type Request struct {
// HTTP請求
httpReq *http.Request
}
我把基本資料結構的宣告都放到了範例專案下的程式碼包 gopcp.v2/chapter6/webcra-wler/module 中。因此,其他程式碼包中的程式碼在存取這些型別時一般會用到限定符 module。範例專案大家可以從我的網路硬碟中下載(連結:https://pan.baidu.com/s/1yzWHnK1t2jLDIcTPFMLPCA 提取碼:slm5)。
從已經提到的相關需求來看,這樣的宣告已經足夠了。不過,我也說過網路爬蟲能夠在爬取過程結束之後自動停止。那麼,網路爬蟲在對一個網站上的內容爬取到什麼程度才結束呢?量化內容爬取程度的一個比較常用的方法,是計算每個下載的網路內容的深度。
網路爬蟲可以根據最大深度的預設值忽略掉對“更深”的網路內容的下載。當所有在該最大深度範圍內的網路內容都下載完成時,就意味著爬取過程即將結束。待這些內容分析和處理完成後,就能夠判定網路爬蟲對爬取過程的執行是否真正結束了。因此, 為了記錄網路內容的深度,我們還應該在 Request 型別的宣告中加入一個欄位,它的第二個版本如下:
//資料請求的型別
type Request struct {
// HTTP請求
httpReq *http.Request
//請求的深度
depth uint32
}
//用於建立一個新的請求範例
func NewRequest(httpReq *http.Request, depth uint32) *Request {
return &Request{httpReq: httpReq, depth: depth}
}
//用於獲取 HTTP 請求
func (req *Request) HTTPReq() *http.Request {
return req.httpReq
}
//用於獲取請求的深度
func (req *Request) Depth() uint32 {
return req.depth
}
我希望這個型別的值是不可變的。也就是說,在該型別的一個值建立和初始化之後, 當前程式碼包之外的任何程式碼都不能更改它的任何欄位值。對於這樣的需求,一般會通過以下 3 個步驟來實現。
1) 把該型別的所有欄位的存取許可權都設定為包級私有。也就是說,要保證這些欄位 的名稱首字母均為小寫。
2) 編寫一個建立和初始化該型別值的函數。由於該型別的所有欄位均不能被當前程式碼包之外的程式碼直接存取,所以它們自然也就無法為這樣的欄位賦值。這也是需要編寫這樣一個函數的原因。這類函數的名稱一般都以“New”為字首,它們會接受一些引數值,然後以此為基礎初始化一個目標型別的值並將其作為函數結果返回。
3) 編寫必要的用來獲取欄位值的方法。這一步驟並不是必需的。不編寫這樣的方法的原因可能是想要完全隱藏欄位值,也可能是欄位的型別導致不宜公開其值。比如,如果欄位是參照型別的,那麼只要它的值可以被外部獲取,就等於讓外部有了修改許可權。
注意,NewRequest 函數的結果型別是 *Request,而不是 Requesto 這樣做的主要原因是要為 Request 型別編寫指標方法而非值方法,並以此讓 *Request 成為某個介面型別的實現型別。更深層次的原因是,值在作為引數傳遞給函數或者作為結果由函數返回時會被複製一次。指標值往往更能減小複製的開銷。
這裡再說明一下 Request 型別的 depth欄位。理論上,uint32 型別已經可以使 depth 欄位的值足夠大了。由於深度值不可能是負數,所以也不需要為此犧牲正整數的部分取值範圍。傳遞給排程器的首次請求的深度值是 0,這也是首次請求的一個標識。
那麼,後續請求的深度值應該怎樣計算和傳遞呢?假設下載器發出了首次請求“A”並成功接收到了響應,經過分析器的分析,其中找到了兩個新的網路地址並生成了新的請求“B”和“C”,那麼這兩個新請求的深度值就為 1。
如果在接收並分析了請求“B”的響應之後又生成了一個新請求“D”,那麼後者的深度值就是 2,以此類推。我們可以把首次請求看作請求“B”和請求“C”的父請求,反過來講,可以把請求“B”和請求“C”視作首次請求的子請求。
因此,就有了這樣一條規則:一個請求的深度值等於對它的父請求的深度值遞增一次後的結果。
理解了剛剛對請求深度值計算方法的描述之後,你可能會發現:只有對某個請求的響應內容進行分析之後,才可能需要生成新的請求。並且,排程器並不會直接把請求作為引數傳遞給分析器。這樣不符合我們先前對資料流轉方式的設計,同時也會使這兩個處理模組之間的互動變得混亂。
顯然,響應也攜帶深度值。一方面,這可以算作標示響應深度的一種方式。另一方面,也是更重要的一方面,它可以作為新請求的深度值的計算依據。因此,Response 型別的宣告如下:
//資料響應的型別
type Response struct {
// HTTP響應
httpResp *http.Response
//響應的深度
depth uint32
}
//用於建立一個新的響應範例
func NewResponse(httpResp *http.Response, depth uint32) *Response {
return &Response{httpResp: httpResp, depth: depth}
}
//用於獲取HTTP響應
func (resp *Response) HTTPResp() *http.Response {
return resp.httpResp
}
//用於獲取響應深度
func (resp *Response) Depth() uint32 {
return resp.depth
}
這個型別的宣告不再做解釋,其各部分的含義與 Request 型別類似。
除了請求和響應這兩個有著對應關係的資料結構之外,還需要定義條目的結構。條目的範例需要儲存的內容比請求和響應複雜得多。因為對響應的內容進行篩選並生成出條目的規則也是由網路爬蟲框架的使用者自己制定的。
因此,條目的結構足夠靈活,其範例可以容納所有可能從響應內容中篩選出的資料。基於此,我這樣定義條目的型別宣告:
//條目的型別
type Item map[string]interface{}
我們把 Item 型別宣告為字典型別 map[string]interface{} 的別名型別,這樣就可以最大限度地儲存多樣的資料了。由於條目處理器也是由網路爬蟲框架的使用者提供,所以這裡並不用考慮字典中的各個元素值是否可以被條目處理器正確理解的問題。
好了,我們需要的 3 個基本資料型別都在這裡了。為了能夠用一個型別從整體上標識這 3 個基本資料型別,我們又宣告了 Data 介面型別:
//資料的介面型別
type Data interface {
//用於判斷資料是否有效
Valid() bool
}
這個介面型別只有一個名為 Valid 的方法,可以通過呼叫該方法來判斷資料的有效性。顯然,Data介面型別的作用更多的是作為資料型別的一個標籤,而不是定義某種型別的行為。為了讓表示請求、響應或條目的型別都實現 Data 介面,又在當前的原始碼檔案中新增了這樣幾個方法:
//用於判斷請求是否有效
func (req *Request) Valid() bool {
return req.httpReq != nil && req.httpReq.URL != nil
}
//用於判斷響應是否有效
func (resp *Response) Valid() bool {
return resp.httpResp != nil && resp.httpResp.Body != nil
}
//用於判斷條目是否有效
func (item Item) Valid() bool {
return item != nil
}
這樣一來,這 3 個型別因 Data 介面型別而被歸為一類。在後面,你會了解到這樣做還有另外的功效。
至此,實現網路爬蟲框架需要用到的基本資料型別均已編寫完成。不過,這裡我們還需要一個額外的型別,這個型別是作為 error 介面型別的實現型別而存在的。它的主要作用是封裝爬取過程中出現的錯誤,並以統一的方式生成字串形式的描述。
我們知道, 只要某個型別的方法集合中包含了下面這個方法,就等於實現了 error 介面型別:
func Error() string
為此,首先宣告了一個名為 CrawlerError 的介面型別:
//爬蟲錯誤的介面型別
type CrawlerError interface {
//用於獲得錯誤的型別
Type() ErrorType
//用於獲得錯誤提示資訊
Error() string
}
我們把它放在了 gopcp.v2/chapter6/webcrawler/errors 程式碼包中,其中 Type 方法的結果型別 ErrorType 只是一個 string 型別的別名型別而已。另外,由於 CrawlerError 型別的宣告中也包含了 Error 方法,所以只要某個型別實現了它,就等於實現了 error 介面型別。
先編寫這樣一個介面型別而不是直接編寫出 error 介面型別的實現型別的原因有兩個。第一,我們在程式設計過程中應該遵循面向介面程式設計的原則,這個原則我已經提過多次了。第二是為了擴充套件 error 介面型別。網路爬蟲框架擁有多個處理模組,錯誤型別值可以表明該錯誤是哪一個處理模組產生的,這也是 Type 方法起到的作用。
下面就讓我們來實現這個介面型別。遵照本書中對實現型別的命名風格,我們宣告了結構體型別 myCrawlerError:
//爬蟲錯誤的實現型別
type myCrawlerError struct {
//錯誤的型別
errType ErrorType
//錯誤的提示資訊
errMsg string
//完整的錯誤提示資訊
fullErrMsg string
}
欄位 errMsg 的值由初始化 myCrawlerError 型別值的一方給出,這與傳遞給 errors.New 函數的引數值的含義類似。作為附加資訊,errType 欄位的值就是該型別的 Type 方法的結果值,它代表了錯誤型別。為了便於使用者為該欄位賦值,還宣告了一些常數:
//錯誤型別常數
const (
//下載器錯誤
ERROR_TYPE_DOWNLOADER ErrorType = "downloader error"
//分祈器錯誤
ERROR_TYPE_ANALYZER ErrorType = "analyzer error"
//條目處理管道錯誤
ERROR_TYPE_PIPELINE ErrorType = "pipeline error"
//調排程器錯誤
ERROR_TYPE_SCHEDULER ErrorType = "scheduler error"
)
可以看到,這 4 個常數的型別都是 ErrorType,它們分別與網路爬蟲框架中的主要模組相對應。當某個模組在執行過程中出現了錯誤,程式就會使用對應的 ErrorType 型別的常數來初始化一個 CrawlerError 型別的錯誤值。具體的初始化方法就是使用 NewCrawler-Error 函數,其宣告如下:
//用於建立一個新的爬蟲錯誤值
func NewCrawlerError(errType ErrorType, errMsg string) CrawlerError {
return &myCrawlerErro:r{
errType: errType,
errMsg: strings.TrimSpace(errMsg),
}
}
從該函數的函數體可以看出,*myCrawlerError 型別是 CrawlerError 型別的一個實現型別。*myCrawlerError 型的方法集合中包含 CrawlerError 口型別中的 Type 方法和 Error 方法:
func (ce *myCrawlerError) Type() ErrorType {
return ce.errType
}
func (ce *myCrawlerError) Error() string {
if ce.fullErrMsg == "" {
ce.genFullErrMsg()
}
return ce.fullErrMsg
}
你可能已經發現,Error 方法中用到了 myCrawlerError 型的 fullErrMsg 欄位。並且,它還呼叫了一個名為 genFullErrMsg 的方法,該方法的實現如下:
//用於生成錯誤提示資訊,並給相應的欄位賦值
func (ce *myCrawlerError) genFullErrMsg() {
var buffer bytes.Buffer
buffer.Writestring("crawler error:")
if ce.errType != "" {
buffer.WriteString(string(ce.errType))
buffer.WriteString(":")
}
buffer.WriteString(ce. errMsg)
ce.fullErrMsg = fmt.Sprintf("%s", buffer.String())
return
}
genFullErrMsg 方法同樣是 myCrawlerError 型別的指標方法,它的功能是生成 Error 方法需要返回的結果值。可以看到,這裡沒有直接用 errMsg 欄位的值,而是以它為基礎生成了一條更完整的錯誤提示資訊。在這條資訊中,明確顯示岀它是一個網路爬蟲的錯誤,也給出了錯誤的型別和詳情。
注意,這條錯誤提示資訊快取在 fullErrMsg 欄位中。回顧該型別的 Error 方法的實現,只有當 fullErrMsg 欄位的值為 "" 時,才會呼叫 genFullErrMsg 方法,否則會直接把 fullErrMsg 欄位的值作為 Error 方法的結果值返回。
這也是為了避免頻繁地拼接字串給程式效能帶來的負面影響。在 genFullErrMsg 方法的實現中使用了 bytes.Buffer 型別值作為拼接錯誤資訊的手段。
雖然這樣做確實可以大大減小這一負面影響,但是由於 myCrawlerError 型別的值是不可變的,所以快取錯誤提示資訊還是很有必要的。其根本原因是,對這樣的不可變值的快取永遠不會失效。
前面展示的這些型別對於承載資料(不論是正常資料還是錯誤資訊)來說已經足夠了,它們是網路爬蟲框架中最基本的元素。