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


  • 爬蟲的分類

 

網絡爬蟲分為兩類

1. 通用爬蟲: 類似於baidu, google. 他們會把大量的數據挖下來, 保存到自己的服務器上. 用戶打開跳轉的時候, 其實先是跳轉到他們自己的服務器. 

2. 聚焦爬蟲: 其實就是有目標的爬蟲, 比如我只需要內容信息. 那我就只爬取內容信息. 

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


 

  • 項目總體結構

 

 

 爬蟲的思想很簡單.

1. 寫一段程序, 從網絡上把數據抓下來

2. 保存到我們的數據庫中

3. 寫一個前端頁面, 展示數據


 

  • go語言的爬蟲庫/框架

 

 

 

  

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


 

  • 本課程的爬蟲項目

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

2. 數據庫使用ElasticSearch

3. 頁面展示使用標准庫的http

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


 

  • 爬蟲的主題 

 

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


 

  • 如何發現用戶

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

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

 


 

  • 爬蟲總體算法 

 1. 城市列表, 找到一個城市

2. 城市下面有用戶列表. 點擊某一個用戶, 進去查看用戶的詳情信息

3. 用戶詳情頁右側有猜你喜歡, 鏈接到一個新的用戶詳情頁

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


 

 

 

 

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

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

2. 並發版. 有多個連接同時訪問, 這里使用了go的協程

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


 

下面開始項目階段

項目

一. 單任務版網絡爬蟲

目標: 抓取珍愛網中的用戶信息.

1.  抓取用戶所在的城市列表信息

2. 抓取某一個城市的某一個人的基本信息, 把信息存到我們自己的數據庫中

分析: 

1. 通過url獲取網站數據. 拿到我們想要的地址,以及點擊地址跳轉的url. 把地址信息保存到數據庫.  數據量預估300

2. 通過url循環獲取用戶列表. 拿到頁面詳情url, 在獲取用戶詳情信息. 把用戶信息保存到數據庫. 數據量會比較大. 一個城市如果有10000個人注冊了, 那么就有300w的數據量.

3. 所以, 數據庫選擇的是elasticSearch

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

抓取城市列表頁, 也就是目標把這個頁面中我們要的內容抓取下來.

其實就兩個內容, 1. 城市名稱, 2. 點擊城市名稱跳轉的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

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])

    }
}

 

 結果如下:

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


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

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

  • 城市列表解析器: 用來解析城市列表
  • 城市解析器: 用來解析某一個城市的頁面內容, 城市里是用戶列表和分頁
  • 用戶解析器: 從城市頁面點擊用戶進入到用戶的詳情頁, 解析用戶的詳情信息

解析器抽象

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

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

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

輸出參數: Request{URL, Parse}列表, Item列表

為什么輸出的第一個參數是Request{URL, Parse}列表呢?

  •  城市列表解析器, 我們獲取到城市名稱和url, 點解url, 要進入的是城市解析器. 所以這里的解析器應該是城市解析器. 
  • 城市解析器. 我們進入城市以后, 會獲取用戶的姓名和用戶詳情頁的url. 所以這里的解析器, 應該傳的是用戶解析器.
  • 用戶解析器. 用來解析用戶的信息. 保存入庫

項目架構

 

 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
}

看一下抓取的效果吧

抓取的城市列表

 

 抓取的某個城市的用戶列表

 

具體某個人的詳細信息

 

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

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

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

 

二. 並發版網絡爬蟲

 

 

 

 

三. 分布式網絡爬蟲

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM