Go使用代理采集数据实践

1,531 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

前言

本月会持续更新Go语言相关的文章,尤其是GoFrame,感兴趣的同学可以关注我,结伴而行。

同时会沉淀总结一下:《中台开发实践》、《私有化部署实践》、《深入理解goroutine及使用实践》、《如何在开发过程中把GO语言的价值体现出来》。

立志沉淀一些质量高的内容出来。

今天这篇分享:使用Go语言做爬虫的实践,包括对接代理和不对接代理的情况。

需求分析

  1. 允许用户指定关键词去获得数据
  2. 允许用户输入代理ip,如果不输入代理ip,则默认使用本机ip
  3. 把采集结果输出到文件中
  4. 把不可用的代理ip输出到文件中,方便用户更新。

说明

本教程仅供学习研究GO语言技术使用,如果大家要采集数据,请通过正常渠道和官方对接,或者对接聚合API等数据平台。

知识点

下面介绍一下涉及到的知识点,让大家有个系统的认识:

  1. 首先有和用户交互的文字输入和文件输出:flag.StringVar()os
  2. ip池的管理:gcache的使用
  3. 使用代理ip请求数据:http客户端的使用
  4. 正则匹配:处理目标数据

代码

说明:下面所有的函数都可以放到同一个文件中,为了方便给大家讲解,我按照业务拆分成了多个子目录。

主程序及main()函数

  1. 根据是否输入代理ip判断是否通过代理ip采集
  2. 注意os文件操作的权限
  3. 管理ip池的思路是使用用户本地的内存做缓存。
package main

import (
 "flag"
 "fmt"
 "github.com/gogf/gf/frame/g"
 "github.com/gogf/gf/net/ghttp"
 "github.com/gogf/gf/os/gcache"
 "io"
 "io/ioutil"
 "math/rand"
 "os"
 "regexp"
 "strconv"
 "strings"
 "time"
)

var proxyIps string
var IDS []string
var keyword string
var wq string
var filePath string
var fp *os.File
var PriceStart int
var Page int
var fileUnUseIP *os.File
var useProxy bool

const (
 SleepTime   = 3 //每次请求休眠时间
 UnuseIpFile = "不可用ip记录.txt"
 MaxPage     = 100
 MaxPrice    = 2000
)

func main() {
 flag.StringVar(&keyword, "keyword", "", "url关键词")
 flag.StringVar(&proxyIps, "ips", "", "代理ip,多个英文逗号分隔")
 //默认存储到当前文件件下
 flag.StringVar(&filePath, "file", "test.txt", "指定保存数据的文件路径及名称,如 c:/test.txt")
 flag.Parse()

 if "" == keyword {
  fmt.Printf("必须传递keyword")
  return
 }

 var err error
 fp, err = os.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) //0666表示:创建了一个普通文件,所有人拥有对该文件的读、写权限,但是都不可执行
 if nil != err {
  fmt.Printf("打开文件失败,请检查文件路径是否正确,或者您的电脑是否设置了权限,无法读写文件")
  return
 }
 defer fp.Close()

 //失效ip写入文件
 var errUnUseIP error
 fileUnUseIP, errUnUseIP = os.OpenFile(UnuseIpFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666)
 if nil != errUnUseIP {
  fmt.Printf("打开" + UnuseIpFile + "失败,请检查您的电脑是否设置了权限,无法读写文件")
 }
 defer fileUnUseIP.Close()

 if "" != proxyIps {
  useProxy = true
  //初始化ip池
  InitIpPool()
  ips, _ := gcache.Keys()
  g.Dump("代理ip池:", ips)
 } else {
  useProxy = false
  g.Dump("未使用代理ip")
 }

fetchList(useProxy)
}

fetchList()函数

  1. 合理的休眠,减轻源站压力
  2. 区分是否使用代理
  3. 请求超时或者返回的数据为空,则认为ip被封禁,不再可用,从ip池中移除,获得新的代理ip
func fetchList(useProxy bool) (isSkip bool) {
	isSkip = false
	url := "https://search.xxxx.com/search?keyword=" + keyword
	time.Sleep(SleepTime * time.Second)

	var randIp string
	//区分是否使用代理
	if useProxy {
		ips, _ := gcache.Values()
		if len(ips) == 0 {
			isSkip = true
			g.Dump("ip均不可用,程序退出。")
			return
		}

		randIp = GetRandIp()
		g.Dump("当前代理ip:", randIp)
		if randIp == "" {
			g.Dump("代理ip为空")
			return
		}
	}

	client := ProxyClient(randIp, useProxy)
	resp, err := client.Get(url)
	if err != nil {
		fmt.Println(err.Error())
		fmt.Printf("网络连接超时,切换ip重新请求")
		//移除请求超时的代理ip 重新抓取
		if useProxy {
			RemoveIP(randIp)
		}
		fetchList(useProxy)
		return
	}

	defer resp.Body.Close()
	isSkip = WriteFile(resp.Body)

	if isSkip && !useProxy {
		g.Dump("一直采集不到数据,可能本地ip被封禁,请使用代理ip")
	}

	return
}

定义代理客户端

  1. 设置authority为源码域名
  2. 根据是否使用代理决定是否设置client.SetProxy(ip)
  3. 返回http客户端对象
//代理客户端
func ProxyClient(ip string, useProxy bool) (client *ghttp.Client) {
	client = g.Client()
	client.SetHeader("authority", "search.xxx.com")
	client.SetHeader("cache-control", "max-age=0")
	client.SetHeader("sec-ch-ua", "\"Microsoft Edge\";v=\"95\", \"Chromium\";v=\"95\", \";Not A Brand\";v=\"99\"")
	client.SetHeader("sec-ch-ua-mobile", "?0")
	client.SetHeader("sec-ch-ua-platform", "\"Windows\"")
	client.SetHeader("upgrade-insecure-requests", "1")
	client.SetHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30")
	client.SetHeader("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")
	client.SetHeader("sec-fetch-site", "none")
	client.SetHeader("sec-fetch-mode", "navigate")
	client.SetHeader("sec-fetch-user", "?1")
	client.SetHeader("sec-fetch-dest", "document")
	client.SetHeader("accept-language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
	client.SetTimeout(3 * time.Second)
	if useProxy {
		client.SetProxy(ip)
	}
	return
}

维护ip池的方法

  1. 思路非常简单:我使用了gcache来管理ip池
  2. 失效的时候就从ip池中移除
  3. 客户端需要代理ip时从ip池中随机返回一个代理ip
//初始化ip池 维护ip池
func InitIpPool() (ipCount int) {
	ips := proxyIps
	splitStr := strings.Split(ips, ",")
	ipCount = len(splitStr)
	for i := 0; i < ipCount; i++ {
		gcache.Set(splitStr[i], splitStr[i], 0)
	}
	return ipCount
}

//随机获得ip
func GetRandIp() (ip string) {
	ips, _ := gcache.Values()
	rand.Seed(time.Now().Unix())
	randIndex := rand.Intn(len(ips))
	ip = ips[randIndex].(string) //转成string
	return
}

//移除ip
func RemoveIP(ip string) {
	gcache.Remove(ip)
	//失效ip统计
	_, err := fileUnUseIP.WriteString(ip)
	if nil != err {
		fmt.Println("不可用ip写入文件失败:", err)
	}
	_, _ = fileUnUseIP.WriteString("\r\n")
}

输出结果到文件

  1. 获得的数据如何和我们预期的数据不完全一致,可以通过使用正则匹配处理数据re := regexp.MustCompile()
  2. 如果是循环获得数据,可以根据isSkip决定是否跳出本次循环继续执行。
//写入结果
func WriteFile(r io.Reader) (isSkip bool) {
	body, err := ioutil.ReadAll(r)

	if err != nil {
		g.Dump("body err:", err.Error())
	}

	re := regexp.MustCompile(`xxxxxxx`)
	ids := re.FindAllSubmatch(body, -1)

	for _, v := range ids {
		if -1 != strings.Index(string(v[2]), `xxxxxxxx`) {
			_, err := fp.Write(v[1])
			if nil != err {
				fmt.Println("写入文件失败:", err)
			}
			_, _ = fp.WriteString("\r\n")
			IDS = append(IDS, string(v[1]))
		}

	}

	//go没有三目运算
	if len(ids) == 0 {
		isSkip = true
	} else {
		isSkip = false
	}
	return
}

总结

这篇文章简单介绍了数据采集的一般思路和使用Go语言的一般实践,如果要获得三方数据还请大家通过正规的渠道授权获得。

对GO感兴趣的朋友可以查看我之前写的文章,了解一下Go的魅力:

Go语言为什么值得学习?

我的PHP转Go之旅

回顾一下我的Go学习之旅

非常适合PHP和Java转Go学习的框架:GoFrame

欢迎大家关注我的Go语言学习专栏,我会持续更新在Go学习和使用过程中的干货分享。

Go语言学习专栏

最后

感谢阅读,欢迎大家三连:点赞、收藏、投币(关注)!!!

8e95dac1fd0b2b1ff51c08757667c47a.gif