天天看點

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

  • 爬蟲的分類

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

網絡爬蟲分為兩類

1. 通用爬蟲: 類似于baidu, google. 他們會把大量的資料挖下來, 儲存到自己的伺服器上. 使用者打開跳轉的時候, 其實先是跳轉到他們自己的伺服器. 

2. 聚焦爬蟲: 其實就是有目标的爬蟲, 比如我隻需要内容資訊. 那我就隻爬取内容資訊. 

通常我們使用的爬蟲都是聚焦爬蟲 

  • 項目總體結構

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 爬蟲的思想很簡單.

1. 寫一段程式, 從網絡上把資料抓下來

2. 儲存到我們的資料庫中

3. 寫一個前端頁面, 展示資料

  • go語言的爬蟲庫/架構

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

  

以上是go語言中已經you封裝好的爬蟲庫或者架構, 但我們寫爬蟲的目的是為了學習. 是以.....不使用架構了

  • 本課程的爬蟲項目

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

1. 不用已有的爬蟲庫和架構

2. 資料庫使用ElasticSearch

3. 頁面展示使用标準庫的http

這個練習的目的,就是使用go基礎.之是以選擇爬蟲,是因為爬蟲有一定的複雜性

  • 爬蟲的主題 

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

哈哈, 要是還沒有女盆友, 又不想花錢的童鞋, 可以自己學習一下爬蟲技術

  • 如何發現使用者

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

1. 通過http://www.zhenai.com/zhenghun頁面進入. 這是一個位址清單頁. 你想要找的那個她(他)是哪個城市的

2. 在使用者的詳情頁, 有推薦--猜你喜歡

  • 爬蟲總體算法 

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 1. 城市清單, 找到一個城市

2. 城市下面有使用者清單. 點選某一個使用者, 進去檢視使用者的詳情資訊

3. 使用者詳情頁右側有猜你喜歡, 連結到一個新的使用者詳情頁

需要注意的是, 使用者推薦, 會出現重複推薦的情況. 第一個頁面推薦了張三, 從上三進來推薦了李四. 從李四進來有推薦到第一個頁面了. 這就形成了死循環, 重複推薦

第十三章 go實作分布式網絡爬蟲---單機版爬蟲
第十三章 go實作分布式網絡爬蟲---單機版爬蟲

我們完成爬蟲, 分為三個階段

1. 單機版. 将所有功能在一個引用裡完成

2. 并發版. 有多個連接配接同時通路, 這裡使用了go的協程

3. 分布式. 多并發演進就是分布式了. 削峰, 減少伺服器的壓力. 

下面開始項目階段

項目

一. 單任務版網絡爬蟲

目标: 抓取珍愛網中的使用者資訊.

1.  抓取使用者所在的城市清單資訊

2. 抓取某一個城市的某一個人的基本資訊, 把資訊存到我們自己的資料庫中

分析: 

1. 通過url擷取網站資料. 拿到我們想要的位址,以及點選位址跳轉的url. 把位址資訊儲存到資料庫.  資料量預估300

2. 通過url循環擷取使用者清單. 拿到頁面詳情url, 在擷取使用者詳情資訊. 把使用者資訊儲存到資料庫. 資料量會比較大. 一個城市如果有10000個人注冊了, 那麼就有300w的資料量.

3. 是以, 資料庫選擇的是elasticSearch

 -------------------

抓取城市清單頁, 也就是目标把這個頁面中我們要的内容抓取下來.

其實就兩個内容, 1. 城市名稱, 2. 點選城市名稱跳轉的url

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

第一步: 抓取頁面内容

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
)

func main() {
    // 第一步, 通過url抓取頁面
    resp, err := http.Get("http://www.zhenai.com/zhenghun")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return
    }

    // 讀取出來body的所有内容
    all, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    //fmt.Printf("%s\n", all)
    printCityList(all)
}      

第二步: 正規表達式, 提取城市名稱和跳轉的url

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "regexp"
)

func main() {
    // 第一步, 通過url抓取頁面
    resp, err := http.Get("http://www.zhenai.com/zhenghun")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return
    }

    // 讀取出來body的所有内容
    all, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    //fmt.Printf("%s\n", all)
    printCityList(all)
}

/**
 * 正規表達式提取城市名稱和跳轉的url
 */
func printCityList(content []byte) {
    re := regexp.MustCompile(`<a href="(http://www.zhenai.com/zhenghun/[a-z1-9]+)" data-v-5e16505f>([^<]+)</a>`)
    all := re.FindAllSubmatch(content, -1)
    for _, line := range all {
        fmt.Printf("city: %s, url: %s\n", line[2], line[1])

    }
}      

 結果如下:

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 這樣第一個頁面就抓取完成了. 第二個和第三個頁面可以了類似處理. 但這樣不好, 我們需要把結構進行抽象提取. 形成一個通用的子產品

再來分析我們的單機版爬蟲項目

項目結構---共有三層結構:

  • 城市清單解析器: 用來解析城市清單
  • 城市解析器: 用來解析某一個城市的頁面内容, 城市裡是使用者清單和分頁
  • 使用者解析器: 從城市頁面點選使用者進入到使用者的詳情頁, 解析使用者的詳情資訊

解析器抽象

既然都是解析器, 那麼我們就把解析器抽象出來.

每一個解析器, 都有輸入參數和輸出參數

輸入參數: 通過url抓取的網頁内容. 

輸出參數: Request{URL, Parse}清單, Item清單

為什麼輸出的第一個參數是Request{URL, Parse}清單呢?

  •  城市清單解析器, 我們擷取到城市名稱和url, 點解url, 要進入的是城市解析器. 是以這裡的解析器應該是城市解析器. 
  • 城市解析器. 我們進入城市以後, 會擷取使用者的姓名和使用者詳情頁的url. 是以這裡的解析器, 應該傳的是使用者解析器.
  • 使用者解析器. 用來解析使用者的資訊. 儲存入庫

項目架構

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 1. 有一個或多個種子頁面, 發情請求到處理引擎. 引擎不是馬上就對任務進行處理的. 他首先吧種子頁面添加到隊列裡去

2. 處理引擎從隊列中取出要處理的url, 交給提取器提取頁面内容. 然後将頁面内容傳回

3. 将頁面内容進行解析, 傳回的是Request{URL, Parse}清單和 Items清單

4. 我們将Request添加到任務隊列中. 然後下一次依然從任務隊列中取出一條記錄. 這樣就循環往複下去了

5. 隊列什麼時候結束呢? 有可能不會結束, 比如循環推薦, 也可能可以結束. 

這樣,結構都有了, 入參出參也定義好了, 接下來就是編碼實作

我們先來改寫上面的抓取城市清單

項目結構 

1. 有一個提取器

2. 有一個解析器. 解析器裡應該有三種類型的解析器

3. 有一個引擎來觸發操作

4. 有一個main方法入口

第一步: Fetcher--提取器

package fetcher

import (
    "fmt"
    "io/ioutil"
    "net/http"
)
// 抓取器
func Fetch(url string) ([]byte, error) {

    // 第一步, 通過url抓取頁面
    client := http.Client{}
    request, err := http.NewRequest("GET", url, nil)
    request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36")
    resp, err := client.Do(request)
    //resp, err := http.Get(url)
    if err != nil {
        return nil, fmt.Errorf("http get error :%s", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("http get error errCode:%d", http.StatusOK)
    }

    // 讀取出來body的所有内容
    return ioutil.ReadAll(resp.Body)
}      

 第二步: 有一個城市解析器

package parser

import (
    "aaa/crawler/zhenai/engine"
    "regexp"
)

const cityListRegexp  = `<a[^href]*href="(http://www.zhenai.com/zhenghun/[a-z1-9]+)"[^>]*>([^<]+)</a>`
func ParseCityList(content []byte) (engine.ParseResult) {
    re := regexp.MustCompile(cityListRegexp)
    all := re.FindAllSubmatch(content, -1)
    pr := engine.ParseResult{}
    count := 1
    for _, line := range all {
        req := engine.Request{
            Url:string(line[1]), 
            ParseFun: ParseCity,
        }
        pr.Req = append(pr.Req, req)

        pr.Items = append(pr.Items, "City: " + string(line[2]))

        count --
        if count <=0 {
            break
        }
    }
    return pr
}      

第三步:定義引擎需要使用的結構體

package engine

type Request struct {
    Url string
    ParseFun func(content []byte) ParseResult
}

type ParseResult struct {
    Req []Request
    Items []interface{}
}

func NilParse(content []byte) ParseResult{
    return ParseResult{}
}      

第四步: 抽象出引擎

package engine

import (
    "aaa/crawler/fetcher"
    "fmt"
    "github.com/astaxie/beego/logs"
)

func Run(seeds ...Request) {

    var que []Request

    for _, seed := range seeds {
        que = append(que, seed)
    }

    for len(que) > 0 {
        cur := que[0]
        que = que[1:]

        logs.Info("fetch url:", cur.Url)
        cont, e := fetcher.Fetch(cur.Url)
        if e != nil {
            logs.Info("解析頁面異常 url:", cur.Url)
            continue
        }

        resultParse := cur.ParseFun(cont)
        que = append(que, resultParse.Req...)

        for _, item := range resultParse.Items {
            fmt.Printf("内容項: %s \n", item)
        }
    }
}      

第五步: 定義程式入口

package main

import (
    "aaa/crawler/zhenai/engine"
    "aaa/crawler/zhenai/parser"
)

func main() {
    req := engine.Request{
        Url:"http://www.zhenai.com/zhenghun", 
        ParseFun: parser.ParseCityList,
    }
    engine.Run(req)

}      

 第六步: 城市解析器

package parser

import (
    "aaa/crawler/zhenai/engine"
    "regexp"
)

const cityRe = `<a[^href]*href="(http://album.zhenai.com/u/[0-9]+)"[^>]*>([^<]+)</a>`
func ParseCity(content []byte) engine.ParseResult{

    cityRegexp:= regexp.MustCompile(cityRe)
    subs := cityRegexp.FindAllSubmatch(content, -1)
    pr := engine.ParseResult{}
    for _, sub := range subs {
        name := string(sub[2])
        // 擷取使用者的詳細位址
        re := engine.Request{
            Url:string(sub[1]),
            // 注意, 這裡定義了一個函數來傳遞, 這樣可以吧name也傳遞過去
            ParseFun: func(content []byte) engine.ParseResult {
                return ParseUser(content, name)
            },
        }
        pr.Req = append(pr.Req, re)

        pr.Items = append(pr.Items, "Name: " + string(sub[2]))
    }

    return pr
}      

城市解析器和城市清單解析器基本類似. 傳回的資料是request和使用者名

 第七步: 使用者解析器

package parser

import (
    "aaa/crawler/zhenai/engine"
    "aaa/crawler/zhenai/model"
    "regexp"
    "strconv"
    "strings"
)

// 個人基本資訊
const userRegexp = `<div[^class]*class="m-btn purple"[^>]*>([^<]+)</div>`
// 個人隐私資訊
const userPrivateRegexp = `<div data-v-8b1eac0c="" class="m-btn pink">([^<]+)</div>`
// 擇偶條件
const userPartRegexp = `<div data-v-8b1eac0c="" class="m-btn">([^<]+)</div>`

func ParseUser(content []byte, name string) engine.ParseResult {
    pro := model.Profile{}
    pro.Name = name
    // 擷取使用者的年齡
    userCompile := regexp.MustCompile(userRegexp)
    usermatch := userCompile.FindAllSubmatch(content, -1)

    pr := engine.ParseResult{}
    for i, userInfo := range usermatch {
        text := string(userInfo[1])
        if i == 0 {
            pro.Marry = text
            continue
        }
        if strings.Contains(text, "歲") {
            age, _ := strconv.Atoi(strings.Split(text, "歲")[0])
            pro.Age = age
            continue
        }
        if strings.Contains(text, "座") {
            pro.Xingzuo = text
            continue
        }
        if strings.Contains(text, "cm") {
            height, _ := strconv.Atoi(strings.Split(text, "cm")[0])
            pro.Height = height
            continue
        }

        if strings.Contains(text, "kg") {
            weight, _ := strconv.Atoi(strings.Split(text, "kg")[0])
            pro.Weight = weight
            continue
        }

        if strings.Contains(text, "工作地:") {
            salary := strings.Split(text, "工作地:")[1]
            pro.Salary = salary
            continue
        }

        if strings.Contains(text, "月收入:") {
            salary := strings.Split(text, "月收入:")[1]
            pro.Salary = salary
            continue
        }

        if i == 7 {
            pro.Occuption = text
            continue
        }

        if i == 8 {
            pro.Education = text
            continue
        }
    }
    pr.Items = append(pr.Items, pro)

    return pr
}      

看一下抓取的效果吧

抓取的城市清單

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 抓取的某個城市的使用者清單

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

具體某個人的詳細資訊

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

 至此, 完成了單機版爬蟲. 再來回顧一下. 

做完了感覺, 這個爬蟲其實很簡單, 之前用java都實作過.隻不過這次是用go實作的

  • 有一個種子頁面, 從這個頁面進來, 會擷取到源源不斷的使用者資訊
  • 遇到一個403的問題. 需要使用自定義的http請求, 設定header 的User-agent,否則伺服器請求被拒絕
  • 使用函數式程式設計. 函數的特點就是靈活. 靈活多變. 想怎麼封裝都行. 這裡是在cityParse解析出user資訊的時候,使用了函數式程式設計.把使用者名傳遞過去了

二. 并發版網絡爬蟲

三. 分布式網絡爬蟲

第十三章 go實作分布式網絡爬蟲---單機版爬蟲

繼續閱讀