记一次爬虫使用日记(1)--- colly修炼手册

766 阅读6分钟

当我们使用golang进行网络编程时,通常使用自带库 net/http 足够应付日常各类需求。以及在应对简单的登录请求脚本编写,直接使用 http 即可。本次我们主题不是如何使用 http 网络编程,而是如何实现某某网站的自动登录脚本,并进行日常调用,无需进行人工操作。要完成这项需求,我们就离不开爬虫框架的帮助。相比于Python丰富的爬虫框架,go中爬虫框架寥寥无几,其中colly是go实现的轻量化爬虫框架,但是在查阅相应的资料文献却也是少的可怜,这时候我们只有依赖官网文档来学习这个框架。

1、理论篇

通过访问github.com/gocolly/col… 链接,直接上官网用例

import {
    "fmt"
    "github.com/gocolly/colly"
}

func main() {
    // 创建collector
    c := colly.NewCollector()

    // 监听事件,找到所有页面链接并访问
    c.OnHTML("a[href]", func(e *colly.HTMLElement) {
	e.Request.Visit(e.Attr("href"))
    })
    // 请求配置
    c.OnRequest(func(r *colly.Request) {
	fmt.Println("Visiting", r.URL)
    })

    c.Visit("http://go-colly.org/")
}

github文档写的很简单,在网上搜罗了一大圈,都是一如既地简洁,但这一点也不妨碍我们使用体验,无非还是从官方用例和源码解析来一步一步学习,我们下面以实战项目来逐步了解colly框架。

step1. 创建项目文件目录

此处以mac os系统为系统环境进行操作:

mkdir /Desktop/scrapshell

该项目基于go1.19版本进行创建,故我们采用Go module的方式进行项目的初始化过程。因此我们需要在本地执行go mod init <module 名>命令

go mod init io.adana/sakura/scrapshell

step2. 安装colly库

安装命令如下:

go get -u github.com/gocolly/colly

最新的go版本可以使用命令如下:

go install github.com/gocolly/colly

step3. 编写代码

首先,我们应该知道colly框架的“三板斧”基本套路:

a. 创建collector
colly.NewCollector()

在这里我们阅读下源码:

// NewCollector 创建一个新的带有默认配置的collector
func NewCollector(options ...func(*Collector)) *Collector {
	c := &Collector{}
	c.Init()

	for _, f := range options {
		f(c)
	}

	c.parseSettingsFromEnv()

	return c
}

这里可以很轻易看到带有一个定义Collector类型的可变参数options,不传即为默认配置。那我们看看Collector这个结构体到底有哪些可自定义的配置属性。

// Collector 提供爬虫作业的一个爬取实例
type Collector struct {
	// UserAgent 是 User-Agent字符串用于HTTP请求
	UserAgent string
	// MaxDepth 限制访问URLs的递归深度.
	// 对于无限递归默认给定为0.
	MaxDepth int
	// AllowedDomains 是域名白名单
	AllowedDomains []string
	// DisallowedDomains 是域名黑名单
	DisallowedDomains []string
	// DisallowedURLFilters 是限定范围链接的一个规则列表,当任意一条规则能匹配到该链接,则请求将立即停止访问
        // DisallowedURLFilters 会优先于URLFilters规则匹配
	DisallowedURLFilters []*regexp.Regexp
	// URLFilters 与 DisallowedURLFilters 相反
	URLFilters []*regexp.Regexp

	// AllowURLRevisit 允许同一个链接多次下载
	AllowURLRevisit bool
	// MaxBodySize 是限制检索到的响应实体字节数
	// 0 代表无限制.
	// MaxBodySize 默认值是 10MB (10 * 1024 * 1024 bytes).
	MaxBodySize int
	// CacheDir 指定Get请求缓存为文件的位置,当没给定值,则该配置无效
	CacheDir string
	// IgnoreRobotsTxt 允许该Collector忽略任何目标宿主robots.txt文件设置性限制.访问http://www.robotstxt.org/查看更多详情
	IgnoreRobotsTxt bool
	// Async 开启异步网络链接,使用Collector.Wait()可以确保完成所有的请求
	Async bool
	// ParseHTTPErrorResponse 允许解析不是2xx状态码的http响应.colly默认只会解析成功状态的http响应。
	// 设置 ParseHTTPErrorResponse 为 true可以使它生效.
	ParseHTTPErrorResponse bool
	// ID 是特定collector的唯一标识
	ID uint32
	// DetectCharset 能在没有显式字符集声明下,针对非utf-8编码的响应体开启编码检测,该特点使用参见 https://github.com/saintfish/chardet
	DetectCharset bool
	// RedirectHandler允许控制如何管理一个重定向
	RedirectHandler func(req *http.Request, via []*http.Request) error
	// CheckHead会在每次Get请求预检响应后执行一次head请求
	CheckHead         bool
	...
}
b. 事件监听配置
// 请求相关属性
c.OnRequest(func(r *colly.Resquest) {
    r.Header.Set("key", "value")
})

// 响应相关属性
c.OnResponse(func(r *colly.Response) {
    r.Header.Set("key", "value")
})

// html页面信息获取;goSelector 选择器可参见 https://github.com/PuerkitoBio/goquery
c.OnHtml("#currencies-all tbody tr", func(e *colly.HTMLElement) {
    
})

// xml监听,并执行xml标签所在的xml内容 https://github.com/antchfx/xmlquery
c.OnXml("xxxxx", func(x *colly.XMLElement) {
    
})

// 取消对指定选择器的html监听
c.OnHTMLDetach("#currencies-all tbody tr")

// 取消对指定xml标签的xml监听
c.OnXmlDetach("xxxxxx")

// http请求错误后回调
c.OnError(func (r *colly.Response, e Error) {
    
})

// 在抓取工作完成后执行,该函数将在OnHtml后执行
c.OnScraped(func(r *colly.Response) {
    
})

// 自定义http配置
c.WithTransport(&http.Transport {
    Proxy: http.ProxyFromEnvironment,
    DisableKeepAlives: true, // keep-alive关闭
    TLSClientConfig: &tls.Config{InsecureSkipVerify: true},// 开启TLS配置项
    DialContext: (&net.Dialer{        // 拨号内容(针对无加密TCP连接)
        Timeout:   30 * time.Second,  // 超时时间
        KeepAlive: 30 * time.Second,  // keepAlive 超时时间
        DualStack: true,		// 开启支持RFC 6555快速回退,即ipv6出现故障,快速切回ipv4
    }).DialContext,
})
c. 开启网址访问
c.Visit("https://xxx.xxx.com")

2、试炼篇

通过前面官方文档go-colly.org/docs/ 入门手册的学习,我们看的出官方给出的使用方法很简单,但不难发现保留了强大的自定义配置功能。在进入实战篇前,有些避免网站反爬虫措施是必要的。下面把常见的措施列举出来。

调试 debugger

package main

import (
    "fmt"
    "github.com/gocolly/colly"
    "github.com/gocolly/colly/debug"
)


func main() {
    // 创建collector
    c := colly.NewCollector(
        // 开启debugger模式
        colly.Debugger(&debug.LogDebugger{})
    )
	
}

防止IP禁用

package main 
import (
    "github.com/gocolly/colly"
    "github.com/gocolly/colly/proxy"
)

func main() {
    c := colly.NewCollector()
    // 轮询切换代理
    switcher, err := proxy.RoundRobinProxySwitcher(
        "https://127.0.0.1:9998",
        "http://127.0.0.1:9999"
    )
    if err != nil {
	return 
    } else {
	c.SetProxyFunc(switcher)
    }
}

随机UserAgent、重复访问

重复访问

c := colly.NewCollector(
    colly.AllowURLRevisit(),
)

随机UserAgent

直接粘贴官方案例

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandomString() string {
	b := make([]byte, rand.Intn(10)+10)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

c := colly.NewCollector()

c.OnRequest(func(r *colly.Request) {
	r.Headers.Set("User-Agent", RandomString())
})

还有一种方式:

import (
    "log"
    "github.com/gocolly/colly"
    "github.com/gocolly/colly/extensions"
)

func main() {
    c := colly.NewCollector()
    // 扩展提供随机UserAgent
    extensions.RandomUserAgent(c)
}

3.实战篇

下面就是基于某网站实现抓取验证码并识别文本内容

package main

import (
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gocolly/colly"
	"github.com/gocolly/colly/debug"
	"github.com/gocolly/colly/extensions"
	"github.com/otiai10/gosseract/v2"
	"log"
	"net/http"
	"net/http/cookiejar"
	"os"
)

func main() {
	var captchaId, img string
	var ip = "**.***.**.**"
	c := colly.NewCollector(
		colly.Debugger(&debug.LogDebugger{}),
		colly.AllowedDomains(ip),
	)
	handleCommonBiz(c)

	c.OnResponse(func(r *colly.Response) {

		assembleResponseHeaders(r)
		fmt.Println("response status:", r.StatusCode)

		body := r.Body
		captcha := &Captcha{}
		convertBodyToJson(body, captcha)
		// write to temp file.
		writeImgToFile(captcha, img)
		// get context from ocr
		client := gosseract.NewClient()
		defer client.Close()
		client.SetImage("test.png")
		text, _ := client.Text()
		fmt.Println("context:", text)

	})
	// get captcha
	err := c.Visit("https://" + ip + "/usmapi/v1/misc/captcha?r=0.2388254867807843&type=login_image")
	if err != nil {
		log.Fatal(err)
		return
	}
}

func handleCommonBiz(c *colly.Collector) {
    extensions.RandomUserAgent(c)
	cookie, _ := cookiejar.New(nil)
	c.SetCookieJar(cookie)
    // 解决无证书的https请求无内容的问题
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	c.WithTransport(tr)
	c.OnRequest(func(r *colly.Request) {
		assembleRequestHeader(r)
	})
}

func assembleRequestHeader(r *colly.Request) {
	r.Headers.Set("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")
	r.Headers.Set("Accept-Encoding", "gzip, deflate, br")
	r.Headers.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
	r.Headers.Set("Cache-Control", "no-cache")
	r.Headers.Set("Connection", "keep-alive")
	r.Headers.Set("sec-ch-ua", "Not?A_Brand\";v=\"8\", \"Chromium\";v=\"108\", \"Microsoft Edge\";v=\"108")
	r.Headers.Set("sec-ch-ua-mobile", "?0")
	r.Headers.Set("sec-ch-ua-platform", "Windows")
	r.Headers.Set("Sec-Fetch-Dest", "document")
	r.Headers.Set("Sec-Fetch-Mode", "navigate")
	r.Headers.Set("Sec-Fetch-Site", "same-origin")
	r.Headers.Set("Sec-Fetch-User", "?1")
}

func assembleResponseHeaders(r *colly.Response) {
	r.Headers.Set("Server", "nginx")
	r.Headers.Set("Content-Type", "text/json")
	r.Headers.Set("Transfer-Encoding", "chunked")
	r.Headers.Set("Connection", "keep-alive")
	r.Headers.Set("ETag", "W/\"631adc36-26f\"")
	r.Headers.Set("X-Frame-Options", "sameorigin")
}

func writeImgToFile(captcha *Captcha, img string) {
	img = captcha.Data.Image
	data, err := base64.StdEncoding.DecodeString(img)

	if err != nil {
		log.Fatal(err)
	}
	f, _ := os.OpenFile("test.png", os.O_RDWR|os.O_CREATE, os.ModePerm)
	defer func(f *os.File) {
		err := f.Close()
		if err != nil {

		}
	}(f)
	_, err = f.Write(data)
	if err != nil {
		return
	}
}

func convertBodyToJson(body []byte, captcha *Captcha) {
	err := json.Unmarshal(body, captcha)
	if err != nil {
		log.Fatal("analyse the json has error:", err)
		return
	}
}

type Data struct {
	CaptchaId string `json:"captcha_id"`
	Image     string `json:"image"`
}

type Captcha struct {
	Code string `json:"code"`
	Data Data
	Msg  string `json:"msg"`
}

总结

通过上面一个小需求,我们还是可以一窥colly爬虫框架的使用之道。当然在实战爬虫中这个代码还是一个初步版本的源代码,在后续系列文章中我会持续完善该功能。本章只是简单了解下colly框架的基本使用,其中还有很多特点并未一一展示出来,有需要的童鞋可以直接访问go-colly.org/docs/