上一篇博客《Golang實現簡單爬蟲框架(1)——項目介紹與環境準備》中咱們介紹了go語言的開發環境搭建,以及爬蟲項目介紹。html
本次爬蟲爬取的是珍愛網的用戶信息數據,爬取步驟爲:git
注意:在本此爬蟲項目中,只會實現一個簡單的爬蟲架構,包括單機版實現、簡單併發版以及使用隊列進行任務調度的併發版實現,以及數據存儲和展現功能。不涉及模擬登陸、動態IP等技術,若是你是GO語言新手想找練習項目或者對爬蟲感興趣的讀者,請放心食用。github
首先咱們實現一個單任務版的爬蟲,且不考慮數據存儲與展現模塊,首先把基本功能實現。下面是單任務版爬蟲的總體框架golang
下面是具體流程說明:正則表達式
項目目錄數據結構
在正式開始講解前先看一下項目中的數據結構。架構
// /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{}
類型,即這部分具體數據結構由用戶本身來定義。併發
注意:對於Request
中的解析函數,對於每個URL使用城市列表解析器仍是用戶列表解析器,是由咱們的具體業務來決定的,對於Engine
模塊沒必要知道解析函數具體是什麼,只負責Request
中的解析函數來解析傳入的URL對應的網頁數據app
須要爬取的數據的定義框架
// /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,在此先謝謝你們了。