今天这篇分享:使用Go语言做爬虫的实践,包括对接代理和不对接代理的情况。
需求分析
- 允许用户指定关键词去获得数据
- 允许用户输入代理ip,如果不输入代理ip,则默认使用本机ip
- 把采集结果输出到文件中
- 把不可用的代理ip输出到文件中,方便用户更新。
说明
本教程仅供学习研究GO语言技术使用,如果大家要采集数据,请通过正常渠道和官方对接,或者对接聚合API等数据平台。
知识点
下面介绍一下涉及到的知识点,让大家有个系统的认识:
- 首先有和用户交互的文字输入和文件输出:
flag.StringVar()
和os
- ip池的管理:
gcache
的使用 - 使用代理ip请求数据:
http客户端
的使用 - 正则匹配:处理目标数据
代码
说明:下面所有的函数都可以放到同一个文件中,为了方便给大家讲解,我按照业务拆分成了多个子目录。
主程序及main()函数
- 根据是否输入代理ip判断是否通过代理ip采集
- 注意os文件操作的权限
- 管理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()函数
- 合理的休眠,减轻源站压力
- 区分是否使用代理
- 请求超时或者返回的数据为空,则认为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 }
定义代理客户端
- 设置
authority
为源码域名 - 根据是否使用代理决定是否设置
client.SetProxy(ip)
- 返回
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池的方法
- 思路非常简单:我使用了gcache来管理ip池
- 失效的时候就从ip池中移除
- 客户端需要代理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") }
输出结果到文件
- 获得的数据如何和我们预期的数据不完全一致,可以通过使用正则匹配处理数据
re := regexp.MustCompile()
- 如果是循环获得数据,可以根据
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 }