上一篇博客《Golang實現簡單爬蟲框架(1)——項目介紹與環境準備》中咱們介紹了go語言的開發環境搭建,以及爬蟲項目介紹。html
本次爬蟲爬取的是珍愛網的用戶信息數據,爬取步驟爲:golang
注意:在本此爬蟲項目中,只會實現一個簡單的爬蟲架構,包括單機版實現、簡單併發版以及使用隊列進行任務調度的併發版實現,以及數據存儲和展現功能。不涉及模擬登陸、動態IP等技術,若是你是GO語言新手想找練習項目或者對爬蟲感興趣的讀者,請放心食用。正則表達式
首先咱們實現一個單任務版的爬蟲,且不考慮數據存儲與展現模塊,首先把基本功能實現。下面是單任務版爬蟲的總體框架segmentfault
下面是具體流程說明:數據結構
項目目錄架構
在正式開始講解前先看一下項目中的數據結構。併發
// /engine/types.go package engine // 請求結構 type Request struct { Url string // 請求地址 ParseFunc func([]byte) ParseResult // 解析函數 } // 解析結果結構 type ParseResult struct { Requests []Request // 解析出的請求 Items []interface{} // 解析出的內容 }
Request
表示一個爬取請求,包括請求的URL
地址和使用的解析函數,其解析函數返回值是一個ParseResult
類型,其中ParseResult
類型包括解析出的請求和解析出的內容。解析內容Items
是一個interface{}
類型,即這部分具體數據結構由用戶本身來定義。app
注意:對於Request
中的解析函數,對於每個URL使用城市列表解析器仍是用戶列表解析器,是由咱們的具體業務來決定的,對於Engine
模塊沒必要知道解析函數具體是什麼,只負責Request
中的解析函數來解析傳入的URL對應的網頁數據框架
須要爬取的數據的定義函數
// /model/profile.go package model // 用戶的我的信息 type Profile struct { Name string Gender string Age int Height int Weight int Income string Marriage string Address string }
Fetcher模塊任務是獲取目標URL的網頁數據,先放上代碼。
// /fetcher/fetcher.go package fetcher import ( "bufio" "fmt" "io/ioutil" "log" "net/http" "golang.org/x/net/html/charset" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) // 網頁內容抓取函數 func Fetch(url string) ([]byte, error) { client := &http.Client{} req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalln(err) } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36") resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // 出錯處理 if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("wrong state code: %d", resp.StatusCode) } // 把網頁轉爲utf-8編碼 bodyReader := bufio.NewReader(resp.Body) e := determineEncoding(bodyReader) utf8Reader := transform.NewReader(bodyReader, e.NewDecoder()) return ioutil.ReadAll(utf8Reader) } func determineEncoding(r *bufio.Reader) encoding.Encoding { bytes, err := r.Peek(1024) if err != nil { log.Printf("Fetcher error %v\n", err) return unicode.UTF8 } e, _, _ := charset.DetermineEncoding(bytes, "") return e }
由於許多網頁的編碼是GBK,咱們須要把數據轉化爲utf-8編碼,這裏須要下載一個包來完成轉換,打開終端輸入gopm get -g -v golang.org/x/text
能夠把GBK編碼轉化爲utf-8編碼。在上面代碼
bodyReader := bufio.NewReader(resp.Body) e := determineEncoding(bodyReader) utf8Reader := transform.NewReader(bodyReader, e.NewDecoder())
能夠寫爲utf8Reader := transform.NewReader(resp.Body, simplifiedchinese.GBK.NewDecoder())
也是能夠的。可是這樣問題是通用性太差,咱們怎麼知道網頁是否是GBK編碼呢?此時還能夠引入另一個庫,能夠幫助咱們判斷網頁的編碼。打開終端輸入gopm get -g -v golang.org/x/net/html
。而後把判斷網頁編碼模塊提取爲一個函數,如上代碼所示。
// /zhenai/parser/citylist.go package parser import ( "crawler/engine" "regexp" ) const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[0-9a-z]+)"[^>]*>([^<]+)</a>` // 解析城市列表 func ParseCityList(bytes []byte) engine.ParseResult { re := regexp.MustCompile(cityListRe) // submatch 是 [][][]byte 類型數據 // 第一個[]表示匹配到多少條數據,第二個[]表示匹配的數據中要提取的任容 submatch := re.FindAllSubmatch(bytes, -1) result := engine.ParseResult{} //limit := 10 for _, item := range submatch { result.Items = append(result.Items, "City:"+string(item[2])) result.Requests = append(result.Requests, engine.Request{ Url: string(item[1]), // 每個城市對應的URL ParseFunc: ParseCity, // 使用城市解析器 }) //limit-- //if limit == 0 { // break //} } return result }
在上述代碼中,獲取頁面中全部的城市與URL,而後把每一個城市的URL
做爲下一個Request
的URL
,對應的解析器是ParseCity
城市解析器。
在對ParseCityList
進行測試的時候,若是ParseFunc: ParseCity,
,這樣就會調用ParseCity
函數,可是咱們只想測試城市列表解析功能,不想調用ParseCity
函數,此時能夠定義一個函數NilParseFun
,返回一個空的ParseResult
,寫成ParseFunc: NilParseFun,
便可。
func NilParseFun([]byte) ParseResult { return ParseResult{} }
由於http://www.zhenai.com/zhenghun
頁面城市比較多,爲了方便測試能夠對解析的城市數量作一個限制,就是代碼中的註釋部分。
注意:在解析模塊,具體解析哪些信息,以及正則表達式如何書寫,不是本次重點。重點是理解各個解析模塊之間的聯繫與函數調用,同下
// /zhenai/parse/city.go package parser import ( "crawler/engine" "regexp" ) var cityRe = regexp.MustCompile(`<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`) // 用戶性別正則,由於在用戶詳情頁沒有性別信息,因此在用戶性別在用戶列表頁面獲取 var sexRe = regexp.MustCompile(`<td width="180"><span class="grayL">性別:</span>([^<]+)</td>`) // 城市頁面用戶解析器 func ParseCity(bytes []byte) engine.ParseResult { submatch := cityRe.FindAllSubmatch(bytes, -1) gendermatch := sexRe.FindAllSubmatch(bytes, -1) result := engine.ParseResult{} for k, item := range submatch { name := string(item[2]) gender := string(gendermatch[k][1]) result.Items = append(result.Items, "User:"+name) result.Requests = append(result.Requests, engine.Request{ Url: string(item[1]), ParseFunc: func(bytes []byte) engine.ParseResult { return ParseProfile(bytes, name, gender) }, }) } return result }
package parser import ( "crawler/engine" "crawler/model" "regexp" "strconv" ) var ageRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)歲</div>`) var heightRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)cm</div>`) var weightRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([\d]+)kg</div>`) var incomeRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>月收入:([^<]+)</div>`) var marriageRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>([^<]+)</div>`) var addressRe = regexp.MustCompile(`<div class="m-btn purple" [^>]*>工做地:([^<]+)</div>`) func ParseProfile(bytes []byte, name string, gender string) engine.ParseResult { profile := model.Profile{} profile.Name = name profile.Gender = gender if age, err := strconv.Atoi(extractString(bytes, ageRe)); err == nil { profile.Age = age } if height, err := strconv.Atoi(extractString(bytes, heightRe)); err == nil { profile.Height = height } if weight, err := strconv.Atoi(extractString(bytes, weightRe)); err == nil { profile.Weight = weight } profile.Income = extractString(bytes, incomeRe) profile.Marriage = extractString(bytes, marriageRe) profile.Address = extractString(bytes, addressRe) // 解析完用戶信息後,沒有請求任務 result := engine.ParseResult{ Items: []interface{}{profile}, } return result } func extractString(contents []byte, re *regexp.Regexp) string { submatch := re.FindSubmatch(contents) if len(submatch) >= 2 { return string(submatch[1]) } else { return "" } }
Engine模塊是整個系統的核心,獲取網頁數據、對數據進行解析以及維護任務隊列。
// /engine/engine.go package engine import ( "crawler/fetcher" "log" ) // 任務執行函數 func Run(seeds ...Request) { // 創建任務隊列 var requests []Request // 把傳入的任務添加到任務隊列 for _, r := range seeds { requests = append(requests, r) } // 只要任務隊列不爲空就一直爬取 for len(requests) > 0 { request := requests[0] requests = requests[1:] // 抓取網頁內容 log.Printf("Fetching %s\n", request.Url) content, err := fetcher.Fetch(request.Url) if err != nil { log.Printf("Fetch error, Url: %s %v\n", request.Url, err) continue } // 根據任務請求中的解析函數解析網頁數據 parseResult := request.ParseFunc(content) // 把解析出的請求添加到請求隊列 requests = append(requests, parseResult.Requests...) // 打印解析出的數據 for _, item := range parseResult.Items { log.Printf("Got item %v\n", item) } } }
Engine
模塊主要是一個Run
函數,接收一個或多個任務請求,首先把任務請求添加到任務隊列,而後判斷任務隊列若是不爲空就一直從隊列中取任務,把任務請求的URL傳給Fetcher
模塊獲得網頁數據,而後根據任務請求中的解析函數解析網頁數據。而後把解析出的請求加入任務隊列,把解析出的數據打印出來。
package main import ( "crawler/engine" "crawler/zhenai/parser" ) func main() { engine.Run(engine.Request{ // 配置請求信息便可 Url: "http://www.zhenai.com/zhenghun", ParseFunc: parser.ParseCityList, }) }
在main
函數中直接調用Run
方法,傳入初始請求。
本次博客中咱們用Go語言實現了一個簡單的單機版爬蟲項目。僅僅聚焦與爬蟲核心架構,沒有太多複雜的知識,關鍵是理解Engine
模塊以及各個解析模塊之間的調用關係。
缺點是單機版爬取速度太慢了,並且沒有使用到go語言強大的併發特性,因此咱們下一章會在本次項目的基礎上,重構項目爲併發版的爬蟲。
若是想獲取Google工程師深度講解go語言視頻資源的,能夠在評論區留言。
項目的源代碼已經託管到Github上,對於各個版本都有記錄,歡迎你們查看,記得給個star,在此先謝謝你們了。
以爲文章不錯的話就點個贊吧~~謝謝