Skip to content

Go使用代理采集数据实践

一起养成写作习惯!这是我参与「掘金日新计划 · 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池的思路是使用用户本地的内存做缓存。
go
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
go
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客户端对象
go
//代理客户端 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
go
//初始化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决定是否跳出本次循环继续执行。
go

```//写入结果 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语言为什么值得学习?](https://juejin.cn/post/7064778754979004447 "https://juejin.cn/post/7064778754979004447")

[我的PHP转Go之旅](https://juejin.cn/post/6976059413966618638 "https://juejin.cn/post/6976059413966618638")

[回顾一下我的Go学习之旅](https://juejin.cn/post/6949109361331568670 "https://juejin.cn/post/6949109361331568670")

[非常适合PHP和Java转Go学习的框架:GoFrame](https://juejin.cn/post/7075098594151235597 "https://juejin.cn/post/7075098594151235597")

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

[Go语言学习专栏](https://juejin.cn/column/7064777730532835336 "https://juejin.cn/column/7064777730532835336")

# 最后

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

![8e95dac1fd0b2b1ff51c08757667c47a.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c92c98dc810d47b18f07ac141e38bf4c~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp)

🚀 学习遇到瓶颈?想进大厂?

看完这篇技术文章,如果还是觉得不够系统,或者想在实战中快速提升?
王中阳的就业陪跑训练营,提供定制化学习路线 + 企业级实战项目 + 简历优化 + 模拟面试。

了解训练营详情