付费代理一般会提供稳定的服务,这篇主要说免费代理的管理。免费代理数量多、可用时间短,如果每次使用时再临时查找,很容易遇到失效地址。对于需要频繁切换代理的爬虫,可以先做一个代理池,定时抓取、检测并淘汰不可用代理。

这个项目 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/ip

Getter

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 再随机返回一个当前可用的地址。

最后修改:2017 年 08 月 20 日
如果觉得我的文章对你有用,请随意赞赏