• 用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.comAceDarkknightGoProxyCollectorcollectorselectorCollector.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.comAceDarkknightGoProxyCollectorcollector
    egexCollector.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.comAceDarkknightGoProxyCollector
    esult
    esult.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.comAceDarkknightGoProxyCollectorschedulerscheduler.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.comAceDarkknightGoProxyCollectorstoragestorage.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.comAceDarkknightGoProxyCollectorcollectorConfig.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|((1d{2})|([1-9]?d))).){3}(?:25[0-5]|2[0-4]d|((1d{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写项目如果有什么不足和错误希望大家多多包涵和指出。如果你有疑问和更好的建议也欢迎大家一起探讨~

  • 相关阅读:
    java:transient是什么,有什么作用
    如何阅读java源码
    java里面list是引用的好例子
    sort给文件按照大小排序
    HBase的rowkey排序和scan输出顺序
    记录一次事故——idea,sbt,scala
    一个简单的synchronized多线程问题、梳理与思考
    Android TextView文字描边的实现!!
    android中include标签的使用
    layout_weight 的解释及使用
  • 原文地址:https://www.cnblogs.com/DilonWu/p/8645355.html
Copyright © 2020-2023  润新知