用golang 实现一个代理池

背景

写爬虫的时候总会遇到爬取速度过快而被封IP的情况,这个时候就需要使用代理了。在https://github.com/henson/ProxyPool

的启发下,决定自己实现一个代理池。项目已经开源在github。

https://github.com/AceDarkknight/GoProxyCollector

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的相关文档和教程在我参考的是:

https://segmentfault.com/a/1190000010098668

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

  • 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写项目如果有什么不足和错误希望大家多多包涵和指出。如果你有疑问和更好的建议也欢迎大家一起探讨~

上一篇:前端菜鸟的编程之路之css的用法


下一篇:[转]PLSQL Developer备份恢复oracle数据