付费代理一般会提供稳定的服务,这篇主要说免费代理的管理。免费代理数量多、可用时间短,如果每次使用时再临时查找,很容易遇到失效地址。对于需要频繁切换代理的爬虫,可以先做一个代理池,定时抓取、检测并淘汰不可用代理。
这个项目 fork 自 henson/ProxyPool。原项目使用 MongoDB 存储代理,接口返回 JSON:
{
"ip": "27.4.70.129:53281",
"type": "http"
}我把 MongoDB 换成了 Redis,增加 cron 定时任务,只保存代理地址,接口直接返回文本:
27.4.70.129:53281项目主要分为 API、Getter、Storage 和 Tasks 四部分。
API
API 使用 Go 自带的 net/http,提供 /v1/ip 接口,从 Redis 中随机取出一个代理:
const VERSION = "/v1"
func Serve() {
cfg := util.NewConfig()
mux := http.NewServeMux()
mux.HandleFunc(VERSION+"/ip", ProxyHandler)
log.Println("Starting server", cfg.Host)
log.Fatal(http.ListenAndServe(cfg.Host, mux))
}
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ip := storage.ProxyGet()
if ip == "" {
http.Error(w, "no proxy available", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(ip))
}服务监听本机 9001 端口时,请求下面的地址即可得到一个代理:
GET http://127.0.0.1:9001/v1/ipGetter
Getter 负责从免费代理网站抓取地址。原项目已经实现了多个来源,下面是 66ip 的抓取代码:
func IP66() (result []string) {
pollURL := "http://www.66ip.cn/mo.php?tqsl=100"
_, body, errs := gorequest.New().Get(pollURL).End()
if errs != nil {
log.Println(errs)
return
}
body = strings.Split(body, "var sogou_ad_width=960;")[1]
body = strings.Split(body, "</script>")[1]
body = strings.Split(body, "</div>")[0]
body = strings.TrimSpace(body)
body = strings.Replace(body, "\t", "", -1)
temp := strings.Split(body, "<br />")
for _, value := range temp[:len(temp)-1] {
result = append(result, strings.TrimSpace(value))
}
log.Println("IP66 done.")
return
}这种解析方式依赖目标页面的 HTML 结构,页面一旦改版就需要调整。增加新的来源时,只要实现一个返回 []string 的抓取函数,再加入任务列表即可。抓取公开页面时也要控制频率,并遵守目标站点的使用规则。
Storage
Storage 负责代理的保存、读取和删除。Redis 的 Set 正好适合这个场景:SADD 可以自动去重,SRANDMEMBER 可以随机取出一个成员。
func (s *RedisStorage) Add(ip string) error {
conn := s.GetRedisConn()
defer conn.Close()
_, err := conn.Do("SADD", s.key, ip)
return err
}
func (s *RedisStorage) GetOne() (string, error) {
conn := s.GetRedisConn()
defer conn.Close()
return redis.String(conn.Do("SRANDMEMBER", s.key))
}免费代理不太稳定,入库前和使用过程中都需要检查可用性。下面通过代理请求 httpbin,并设置超时时间:
func CheckIP(ip string) bool {
start := time.Now()
pollURL := "http://httpbin.org/get"
resp, _, errs := gorequest.New().
Proxy("http://" + ip).
Get(pollURL).
Timeout(util.Cfg.Timeout * time.Second).
End()
log.Printf("Check IP:(%v) Time:(%v)", ip, time.Since(start))
if errs != nil || resp == nil {
return false
}
return resp.StatusCode == http.StatusOK
}检测失败的代理会从 Redis 中删除,避免继续返回给调用方。这里的“可用”只表示在超时时间内完成了测试请求,并不代表速度、匿名程度和所有目标网站都符合要求。
Tasks
Tasks 包含两个定时任务:抓取新代理,以及检查 Redis 中已有代理的可用性。
func Schedule() {
c := cron.New()
c.AddFunc(util.Cfg.Cron.GetProxyCron, func() {
log.Printf("Get proxy cron running at: %v", time.Now())
if len(IpChan) < 100 {
go run(IpChan)
}
})
c.AddFunc(util.Cfg.Cron.CheckRedisCron, func() {
log.Printf("Check redis cron running at: %v", time.Now())
go storage.CheckProxyInRedis()
})
c.Start()
}执行 Getter 时,每个来源使用一个 goroutine,最后通过 WaitGroup 等待全部抓取完成:
func run(ipChan chan<- string) {
var wg sync.WaitGroup
functions := []func() []string{
getter.Data5u,
getter.IP66,
getter.KDL,
getter.GBJ,
getter.Xici,
getter.XDL,
getter.IP181,
getter.PLP,
}
for _, f := range functions {
wg.Add(1)
go func(getterFunc func() []string) {
defer wg.Done()
for _, ip := range getterFunc() {
ipChan <- ip
}
}(f)
}
wg.Wait()
log.Println("All getters finished.")
}配置
配置文件包括 Redis 地址、API 监听端口、检测超时和 cron 表达式:
{
"redis": {
"addr": "127.0.0.1:6379",
"key": "ProxyPool.proxy_ip_set",
"maxIdle": 5,
"maxActive": 50
},
"host": "127.0.0.1:9001",
"timeout": 30,
"crons": {
"getProxyCron": "0 0 1 * * ?",
"checkRedisCron": "0 0 2 * * ?"
}
}这里让 API 和 Redis 都只监听本机。需要提供给其他机器使用时,至少要在前面增加鉴权、访问控制和限流,Redis 的 6379 端口不要直接暴露到公网。
修改版项目地址:goribun/ProxyPool。整体流程不复杂:Getter 获取候选代理,检测通过后写入 Redis,Tasks 定期补充和清理,API 再随机返回一个当前可用的地址。