背景
寫爬蟲的時候總會遇到爬取速度過快而被封IP的情況,這個時候就需要使用代理了。在https://github.com/henson/ProxyPool
的啟發下,決定自己實現一個代理池。項目已經開源在github。
2018.03.29更新
- go 版本升級為1.9.4,使用新版本的sync.Map 提高並發讀的效率
開發環境
windows 7,Go 1.8.4
數據來源
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.com
項目結構
目錄 | 作用 |
---|---|
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的相關文檔和教程在我參考的是:
-
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=&tqsl=20&ports=&ktip=&xl=on&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的文檔:
結語
關於項目的介紹到這里就差不多了,新手第一次用go寫項目如果有什么不足和錯誤希望大家多多包涵和指出。如果你有疑問和更好的建議也歡迎大家一起探討~