項目地址:https://github.com/zhaojigang/go-crawler
注意:接下來的三節(jié)爬蟲項目全部來源于《Google資深工程師深度講解Go語言》的學(xué)習(xí)筆記塌西。
- 單人版爬蟲:一個 Goroutine 運行整個爬蟲項目
- 并發(fā)版爬蟲:多個 Goroutine 在一臺機器上實現(xiàn)爬蟲項目
- 分布式爬蟲:多個 Goroutine 在多臺機器上實現(xiàn)爬蟲項目
一他挎、爬蟲整體算法
該爬蟲項目爬取的是珍愛網(wǎng)的數(shù)據(jù),總體算法如下
- 首先根據(jù)城市列表 Url 爬取城市列表捡需,爬取出來的內(nèi)容通過城市列表解析器解析出來每一個城市的 Url
- 然后根據(jù)每一個城市的 Url 爬取該城市的用戶信息列表办桨,通過城市解析器將用戶信息列表中的用戶 Url 解析出來
- 最后根據(jù)每一個用戶的 Url 爬取該用戶的詳細(xì)信息,并進行解析
三種 Url 示例:
城市列表 Url:http://www.zhenai.com/zhenghun
城市 Url:http://www.zhenai.com/zhenghun/aba
用戶 Url:http://album.zhenai.com/u/1902329077
二站辉、單任務(wù)版爬蟲架構(gòu)
- 首先會將種子 Url Seed 連同其解析器 Parser 封裝為一個 Request呢撞,放入 Engine 引擎中的任務(wù)隊列(其實就是 []Request 切片)中贸街,啟動爬取任務(wù)(這里的 Seed 就是城市列表 Url)
- 之后 Engine 使用 Fetcher 爬取該 Url 的內(nèi)容 text,然后使用對應(yīng) Url 的Parser 解析該 text狸相,將解析出來的 Url(例如薛匪,城市 Url)和其 Parser 封裝為 Request 加入 Engine 任務(wù)隊列,將解析出來的 items(例如脓鹃,城市名)打印出來
- 然后 Engine 不斷的從其任務(wù)隊列中獲取任務(wù) Request 一個個進行串行執(zhí)行(使用 Fetcher 對 Request.Url 進行爬取逸尖,使用 Request.Parser 對爬取出來的 text 進行解析,將解析出來的內(nèi)容部分進行封裝為Request瘸右,進行后續(xù)循環(huán)娇跟,部分進行打印)
三太颤、代碼實現(xiàn)
3.1 請求與解析結(jié)果封裝體 type.go
package engine
// 請求任務(wù)封裝體
type Request struct {
// 需爬取的 Url
Url string
// Url 對應(yīng)的解析函數(shù)
ParserFunc func([]byte) ParseResult
}
// 解析結(jié)果
type ParseResult struct {
// 解析出來的多個 Request 任務(wù)
Requests []Request
// 解析出來的實體(例如苞俘,城市名),是任意類別(interface{}龄章,類比 java Object)
Items []interface{}
}
3.2 執(zhí)行引擎 engine.go
package engine
import (
"github.com/zhaojigang/crawler/fetcher"
"log"
)
func Run(seeds ...Request) {
// Request 任務(wù)隊列
var requests []Request
// 將 seeds Request 放入 []requests吃谣,即初始化 []requests
for _, r := range seeds {
requests = append(requests, r)
}
// 執(zhí)行任務(wù)
for len(requests) > 0 {
// 1. 獲取第一個 Request,并從 []requests 移除做裙,實現(xiàn)了一個隊列功能
r := requests[0]
requests = requests[1:]
// 2. 使用爬取器進行對 Request.Url 進行爬取
body, err := fetcher.Fetch(r.Url)
// 如果爬取出錯岗憋,記錄日志
if err != nil {
log.Printf("fetch error, url: %s, err: %v", r.Url, err)
continue
}
// 3. 使用 Request 的解析函數(shù)對怕渠道的內(nèi)容進行解析
parseResult := r.ParserFunc(body)
// 4. 將解析體中的 []Requests 加到請求任務(wù)隊列 requests 的尾部
requests = append(requests, parseResult.Requests...)
// 5. 遍歷解析出來的實體,直接打印
for _, item := range parseResult.Items {
log.Printf("getItems, url: %s, items: %v", r.Url, item)
}
}
}
3.3 爬取器 fetcher.go
package fetcher
import (
"fmt"
"io/ioutil"
"net/http"
)
func Fetch(url string) ([]byte, error) {
// 1. 爬取 url
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wrong statusCode, %d", resp.StatusCode)
}
// 2. 讀取響應(yīng)體并返回
return ioutil.ReadAll(resp.Body)
}
3.4 三種解析器
城市列表解析器 citylist.go
package parser
import (
"github.com/zhaojigang/crawler/engine"
"regexp"
)
const cityListRe = `<a href="(http://www.zhenai.com/zhenghun/[0-9a-z]+)"[^>]*>([^<]*)</a>`
// cityList 的 ParserFunc func([]byte) ParseResult
// 解析種子頁面 - 獲取城市列表
func ParseCityList(contents []byte) engine.ParseResult {
result := engine.ParseResult{}
// 正則表達式:()用于提取
rg := regexp.MustCompile(cityListRe)
allSubmatch := rg.FindAllSubmatch(contents, -1)
// 遍歷每一個城市的匹配字段(城市 Url 和城市名)锚贱,并且將 Url 和城市解析器封裝為一個 Request
// 最后將該 Request 添加到 ParseResult 中
for _, m := range allSubmatch {
result.Items = append(result.Items, "city "+string(m[2]))
result.Requests = append(result.Requests, engine.Request{
Url: string(m[1]),
ParserFunc: ParseCity,
})
}
// 返回 ParseResult
return result
}
學(xué)習(xí) Go 正則表達式的使用
城市解析器 city.go
package parser
import (
"github.com/zhaojigang/crawler/engine"
"regexp"
)
// match[1]=url match[2]=name
const cityRe = `<a href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`
// 解析單個城市 - 獲取單個城市的用戶列表
func ParseCity(contents []byte) engine.ParseResult {
result := engine.ParseResult{}
rg := regexp.MustCompile(cityRe)
allSubmatch := rg.FindAllSubmatch(contents, -1)
for _, m := range allSubmatch {
name := string(m[2])
result.Items = append(result.Items, "user "+name)
result.Requests = append(result.Requests, engine.Request{
Url: string(m[1]),
ParserFunc: func(c []byte) engine.ParseResult {
return ParseProfile(c, name) // 函數(shù)式編程仔戈,使用函數(shù)包裹函數(shù)
},
})
}
return result
}
學(xué)習(xí)函數(shù)式編程:使用函數(shù)包裹函數(shù),即函數(shù)的返回值和入?yún)⒍伎梢允呛瘮?shù)拧廊。
用戶解析器 profile.go
package parser
import (
"github.com/zhaojigang/crawler/engine"
"github.com/zhaojigang/crawler/model"
"regexp"
"strconv"
)
var ageRe = regexp.MustCompile(`<td><span class=""label">年齡:</span>([\d])+歲</td>`)
var incomeRe = regexp.MustCompile(`<td><span class=""label">月收入:</span>([^<]+)</td>`)
// 解析單個人的主頁
func ParseProfile(contents []byte, name string) engine.ParseResult {
profile := model.Profile{}
// 1. 年齡
age, err := strconv.Atoi(extractString(contents, ageRe))
if err == nil {
profile.Age = age
}
// 2. 月收入
profile.Income = extractString(contents, incomeRe)
// 3. 姓名
profile.Name = name
result := engine.ParseResult{
Items: []interface{}{profile},
}
return result
}
func extractString(body []byte, re *regexp.Regexp) string {
match := re.FindSubmatch(body) // 只找到第一個match的
if len(match) >= 2 {
return string(match[1])
}
return ""
}
profile 實體類
package model
type Profile struct {
// 姓名
Name string
// 年齡
Age int
// 收入
Income string
}
3.5 啟動器 main.go
package main
import (
"github.com/zhaojigang/crawler/engine"
"github.com/zhaojigang/crawler/zhenai/parser"
)
func main() {
engine.Run(engine.Request{
// 種子 Url
Url: "http://www.zhenai.com/zhenghun",
ParserFunc: parser.ParseCityList,
})
}
解析器測試類
package parser
import (
"io/ioutil"
"testing"
)
func TestParseCityList(t *testing.T) {
expectRequestsLen := 470
expectCitiesLen := 470
// 表格驅(qū)動測試
expectRequestUrls := []string{
"http://www.zhenai.com/zhenghun/aba",
"http://www.zhenai.com/zhenghun/akesu",
"http://www.zhenai.com/zhenghun/alashanmeng",
}
expectRequestCities := []string{
"city 阿壩",
"city 阿克蘇",
"city 阿拉善盟",
}
body, err := ioutil.ReadFile("citylist_test_data.html")
if err != nil {
panic(err)
}
result := ParseCityList(body)
if len(result.Requests) != expectRequestsLen {
t.Errorf("expect requestLen %d, but %d", expectRequestsLen, len(result.Requests))
}
if len(result.Items) != expectCitiesLen {
t.Errorf("expect citiesLen %d, but %d", expectCitiesLen, len(result.Items))
}
for i, url := range expectRequestUrls {
if url != result.Requests[i].Url {
t.Errorf("expect url %s, but %s", url, result.Requests[i].Url)
}
}
for i, city := range expectRequestCities {
if city != result.Items[i] {
t.Errorf("expect url %s, but %s", city, result.Items[i])
}
}
}
學(xué)習(xí)經(jīng)典的 Go 表格驅(qū)動測試监徘。
執(zhí)行 main 函數(shù)發(fā)現(xiàn)執(zhí)行的很慢,因為只有一個 main Goroutine 在執(zhí)行吧碾,還有網(wǎng)絡(luò) IO凰盔,所以比較慢,接下來滤港,將單任務(wù)版的改造成多個 Goroutine 共同執(zhí)行的并發(fā)版的爬蟲廊蜒。