用golang 實現一個代理池

背景

寫爬蟲的時候總會遇到爬取速度過快而被封IP的狀況,這個時候就須要使用代理了。在https://github.com/henson/ProxyPool
的啓發下,決定本身實現一個代理池。項目已經開源在github。php

https://github.com/AceDarkknight/GoProxyCollectorgit

2018.03.29更新

  • go 版本升級爲1.9.4,使用新版本的sync.Map 提升併發讀的效率

開發環境

windows 7,Go 1.8.4github

數據來源

http://www.xicidaili.com
http://www.89ip.cn
http://www.kxdaili.com/
https://www.kuaidaili.com
http://www.ip3366.net/
http://www.ip181.com/
http://www.data5u.com
https://proxy.coderbusy.comweb

項目結構

目錄 做用
collector 收集器,抓取各個網站的代理
result 表示抓取的結果
scheduler 負責任務調度,包括啓動collector和入庫
server 啓動一個web服務,提供取結果的API
storage 存儲結果,經過接口可使用別的數據庫
util 一些經常使用的工具方法
verifier ip的驗證與入庫出庫

實現

  • collector
    collector 支持兩種模式,分別是使用goquery對網頁元素進行選擇和使用正則表達式匹配咱們須要的信息。直接上代碼吧。
// github.com\AceDarkknight\GoProxyCollector\collector\selectorCollector.go
func (c *SelectorCollector) Collect(ch chan<- *result.Result) {
    // 退出前關閉channel。
    defer close(ch)

    response, _, errs := gorequest.New().Get(c.currentUrl).Set("User-Agent", util.RandomUA()).End()
    
    /* 省略部分代碼 */

    // 有些網站不是UTF-8編碼的,須要進行轉碼。
    var decoder mahonia.Decoder
    if c.configuration.Charset != "utf-8" {
        decoder = mahonia.NewDecoder(c.configuration.Charset)
    }

    // 使用goquery。
    doc, err := goquery.NewDocumentFromReader(response.Body)
    if err != nil {
        seelog.Errorf("parse %s error:%v", c.currentUrl, err)
        return
    }

    // 大部分代理網站的代理列表都放在一個table裏,先選出table再循環裏面的元素。
    selection := doc.Find(c.selectorMap["table"][0])
    selection.Each(func(i int, sel *goquery.Selection) {
        var (
            ip       string
            port     int
            speed    float64
            location string
        )

        // 咱們須要的信息的名字和路徑存在collectorConfig.xml。
        nameValue := make(map[string]string)
        for key, value := range c.selectorMap {
            if key != "table" {
                var temp string
                if len(value) == 1 {
                    temp = sel.Find(value[0]).Text()
                } else if len(value) == 2 {
                    temp, _ = sel.Find(value[0]).Attr(value[1])
                }

                // 轉碼.
                if temp != "" {
                    if decoder != nil {
                        temp = decoder.ConvertString(temp)
                    }

                    nameValue[key] = temp
                }
            }
        }

        /* 省略部分代碼 */

        // 過濾一些不符合條件的結果
        if ip != "" && port > 0 && speed >= 0 && speed < 3 {
            r := &result.Result{
                Ip:       ip,
                Port:     port,
                Location: location,
                Speed:    speed,
                Source:   c.currentUrl}

            // 把符合條件的結果放進channel
            ch <- r
        }
    })
}

// github.com\AceDarkknight\GoProxyCollector\collector\regexCollector.go
func (c *RegexCollector) Collect(ch chan<- *result.Result) {
    response, bodyString, errs := gorequest.New().Get(c.currentUrl).Set("User-Agent", util.RandomUA()).End()
    
    /* 省略部分代碼 */

    // 用正則匹配。
    regex := regexp.MustCompile(c.selectorMap["ip"])
    ipAddresses := regex.FindAllString(bodyString, -1)
    if len(ipAddresses) <= 0 {
        seelog.Errorf("can not found correct format ip address in url:%s", c.currentUrl)
        return
    }

    for _, ipAddress := range ipAddresses {
        temp := strings.Split(ipAddress, ":")
        if len(temp) == 2 {
            port, _ := strconv.Atoi(temp[1])
            if port <= 0 {
                continue
            }

            r := &result.Result{
                Ip:     temp[0],
                Port:   port,
                Source: c.currentUrl,
            }

            ch <- r
        }
    }
}
  • result
    result很簡單,只是用來表示collector爬取的結果。
// github.com\AceDarkknight\GoProxyCollector\result\result.go
type Result struct {
    Ip       string  `json:"ip"`
    Port     int     `json:"port"`
    Location string  `json:"location,omitempty"`
    Source   string  `json:"source"`
    Speed    float64 `json:"speed,omitempty"`
}
  • scheduler
    scheduler負責完成一些初始化的工做以及調度collector任務。不一樣的任務在不一樣的goroutine中運行,goroutine之間經過channel進行通訊。
// github.com\AceDarkknight\GoProxyCollector\scheduler\scheduler.go
func Run(configs *collector.Configs, storage storage.Storage) {
    /* 省略部分代碼 */

    for {
        var wg sync.WaitGroup

        for _, configuration := range configs.Configs {
            wg.Add(1)
            go func(c collector.Config) {
                // 防止死鎖。
                defer wg.Done()

                // 處理panic。
                defer func() {
                    if r := recover(); r != nil {
                        seelog.Criticalf("collector %s occur panic %v", c.Name, r)
                    }
                }()

                col := c.Collector()
                done := make(chan bool, 1)

                go func() {
                    runCollector(col, storage)
                    // 完成時發送信號。
                    done <- true
                }()

                // 設置timeout防止goroutine運行時間過長。
                select {
                case <-done:
                    seelog.Debugf("collector %s finish.", c.Name)
                case <-time.After(7 * time.Minute):
                    seelog.Errorf("collector %s time out.", c.Name)
                }

            }(configuration)
        }

        // 等待全部collector完成。
        wg.Wait()
        seelog.Debug("finish once, sleep 10 minutes.")
        time.Sleep(time.Minute * 10)
    }
}
  • server
    server啓動了一個服務器,提供API
  • storage
    storage提供了存儲相關的interface和實現。
// github.com\AceDarkknight\GoProxyCollector\storage\storage.go
type Storage interface {
    Exist(string) bool
    Get(string) []byte
    Delete(string) bool
    AddOrUpdate(string, interface{}) error
    GetAll() map[string][]byte
    Close()
    GetRandomOne() (string, []byte)
}

目前項目的數據都是存儲在boltdb。github上面關於boltdb的簡介以下:正則表達式

Bolt is a pure Go key/value store inspired by Howard Chu's LMDB project. The goal of the project is to provide a simple, fast, and reliable database for projects that don't require a full database server such as Postgres or MySQL.
Since Bolt is meant to be used as such a low-level piece of functionality, simplicity is key. The API will be small and only focus on getting values and setting values. That's it.數據庫

考慮到代理池的數據量比較小,並且當初的想法是實現一個開箱即用的代理池,選擇boltdb這樣的嵌入式數據庫顯然是比使用MySQL和MongoDB更加簡單、便捷。固然,若是之後須要使用不一樣的數據庫時,只須要實現storage的接口便可。使用boltdb的相關文檔和教程在我參考的是:json

http://www.javashuo.com/article/p-tggqzyzd-cz.htmlsegmentfault

https://godoc.org/github.com/boltdb/boltwindows

  • util
    util實現了一些通用方法,例如取一個隨機的user-agent,具體就不展開了。
  • verifier

    verifier負責驗證collector拿到的ip是否可用,可用的入庫,不可用的就從數據庫中刪除。服務器

    配置

    collector是經過配置文件驅動的。配置文件是:

github.com\AceDarkknight\GoProxyCollector\collectorConfig.xml

舉個例子:

<config name="coderbusy">
    <urlFormat>https://proxy.coderbusy.com/classical/https-ready.aspx?page=%s</urlFormat>
    <urlParameters>1,2</urlParameters>
    <collectType>0</collectType>
    <charset>utf-8</charset>
    <valueNameRuleMap>
        <item name="table" rule=".table tr:not(:first-child)"/>
        <item name="ip" rule="td:nth-child(2)" attribute="data-ip"/>
        <item name="port" rule=".port-box"/>
        <item name="location" rule="td:nth-child(3)"/>
        <item name="speed" rule="td:nth-child(10)"/>
    </valueNameRuleMap>
</config>
<config name="89ip">
    <urlFormat>http://www.89ip.cn/tiqv.php?sxb=&amp;tqsl=20&amp;ports=&amp;ktip=&amp;xl=on&amp;submit=%CC%E1++%C8%A1</urlFormat>
    <collectType>1</collectType>
    <charset>utf-8</charset>
    <valueNameRuleMap>
        <item name="ip" rule="((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))):[1-9]\d*"/>
    </valueNameRuleMap>
</config>
  • name是collector的名字,主要做用是方便調試和出錯時查問題。
  • urlFormat和urlParameters用來拼接出須要爬取的網址。urlParameters能夠爲空。例如上面第一個配置就是告訴爬蟲要爬的網站是:

    https://proxy.coderbusy.com/classical/https-ready.aspx?page=1

    https://proxy.coderbusy.com/classical/https-ready.aspx?page=2

  • collectType表示是用哪一個collector,0表明selectorCollector,1表明regexCollector。
  • charset表示網站用的是哪一種編碼。默認編碼是UTF-8,若是設置錯了可能會拿不到想要的數據。
  • valueNameRuleMap表示須要的點的規則。對於使用selectorCollector的網站,大部分結果經過table表示,因此table是必須的,其餘點根據不一樣網站配置便可。相關rule的配置能夠參考goquery的文檔:

    https://github.com/PuerkitoBio/goquery

結語

關於項目的介紹到這裏就差很少了,新手第一次用go寫項目若是有什麼不足和錯誤但願你們多多包涵和指出。若是你有疑問和更好的建議也歡迎你們一塊兒探討~

相關文章
相關標籤/搜索