Golang實現簡單爬蟲框架(2)——單任務版爬蟲

上一篇博客《Golang實現簡單爬蟲框架(1)——項目介紹與環境準備》中咱們介紹了go語言的開發環境搭建,以及爬蟲項目介紹。html

本次爬蟲爬取的是珍愛網的用戶信息數據,爬取步驟爲:golang

注意:在本此爬蟲項目中,只會實現一個簡單的爬蟲架構,包括單機版實現、簡單併發版以及使用隊列進行任務調度的併發版實現,以及數據存儲和展現功能。不涉及模擬登陸、動態IP等技術,若是你是GO語言新手想找練習項目或者對爬蟲感興趣的讀者,請放心食用。正則表達式

一、單任務版爬蟲架構

首先咱們實現一個單任務版的爬蟲,且不考慮數據存儲與展現模塊,首先把基本功能實現。下面是單任務版爬蟲的總體框架segmentfault

圖片描述

下面是具體流程說明:數據結構

  • 一、首先須要配置種子請求,就是seed,存儲項目爬蟲的初始入口
  • 二、把初始入口信息發送給爬蟲引擎,引擎把其做爲任務信息放入任務隊列,只要任務隊列不空就一直從任務隊列中取任務
  • 三、取出任務後,engine把要請求的任務交給Fetcher模塊,Fetcher模塊負責經過URL抓取網頁數據,而後把數據返回給Engine
  • 四、Engine收到網頁數後,把數據交給解析(Parser)模塊,Parser解析出須要的數據後返回給Engine,Engine收到解析出的信息在控制檯打印出來

項目目錄架構

圖片描述

二、數據結構定義

在正式開始講解前先看一下項目中的數據結構。併發

// /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的實現

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。而後把判斷網頁編碼模塊提取爲一個函數,如上代碼所示。

四、Parser模塊實現

(1)解析城市列表與URL:
// /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做爲下一個RequestURL,對應的解析器是ParseCity城市解析器。

在對ParseCityList進行測試的時候,若是ParseFunc: ParseCity,,這樣就會調用ParseCity函數,可是咱們只想測試城市列表解析功能,不想調用ParseCity函數,此時能夠定義一個函數NilParseFun,返回一個空的ParseResult,寫成ParseFunc: NilParseFun,便可。

func NilParseFun([]byte) ParseResult {
    return ParseResult{}
}

由於http://www.zhenai.com/zhenghun頁面城市比較多,爲了方便測試能夠對解析的城市數量作一個限制,就是代碼中的註釋部分。

注意:在解析模塊,具體解析哪些信息,以及正則表達式如何書寫,不是本次重點。重點是理解各個解析模塊之間的聯繫與函數調用,同下

(2)解析用戶列表與URL
// /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
}
(3)解析用戶數據
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/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模塊獲得網頁數據,而後根據任務請求中的解析函數解析網頁數據。而後把解析出的請求加入任務隊列,把解析出的數據打印出來。

六、main函數

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,在此先謝謝你們了。

以爲文章不錯的話就點個贊吧~~謝謝

相關文章
相關標籤/搜索