【Golang】使用Golang为老婆编写一个爬虫工具
有些工作内容,站在程序员的角度:写3天的代码,节省N天的工作量。不为什么,就喜欢省事的感觉。
春节前,博主的老婆的假期比博主更早开始,但是放假第二天她返回公司寻找产品设备,一问原由,才知道是项目设备离线,但是老婆公司的后台系统并没有相关的预警机制,未避免“文人相轻”的嫌疑,这里不对系统做评价。
作为老婆的工作,需要登录到系统后台中去查看各个项目的设备状态,然后根据她的公司运营规则去判断是否上报。 博主在查看老婆在后台系统中的一顿操作,寻思着,以从前爬教务处网站的成绩的经验,初步判断是可以通过代码来进行:
- 自动登录网站
- 爬取需要的数据
- 导出数据
一提到爬虫,C#
的童鞋说,C#
写爬虫也只需要一句话的代码,主攻python
的童鞋立马精神了,讲道,python
只需要半句话。
——摘自某技术群讨论内容。
但是一想到是给老婆使用的,最好什么依赖也不要有,也不要去配置什么环境变量,就扔一个可执行文件,exe
,一键导出即可。目光来到了Go
,没错,就是它,支持跨平台交叉编译,直接将代码编程成二进制文件,不需要任何依赖。
python
童鞋告诉我python
能打包成exe
,粗略看了下打包流程,还是觉得麻烦,不想折腾了。
说干就干,Let’s Go.
1.抓包
像所有的爬虫那样,都需要对我们爬取的目标进行一个分析。**抓包这个动作就必可少。**由于是网站请求,走http
协议,所以这次选择的抓包工具是Fiddler,由于网站使用了加密传输的HTTPS
,所以还需要对Fiddler
进行设置,否则抓包查看也是加密的乱码。为了使其能够正常抓包,还需要对Fiddler进行一番设置。这里就不详述了,附上链接,参考图文介绍:https://www.jianshu.com/p/690eb9bebe3c
Fiddler抓包登录过程
网站需要登录,下面就是通过网站手动登录,并通过fiddler进行抓包的过程:
A域名下Post用户名、密码
-
POST
携带用户名、密码请求https://A.com/login
状态码为
302
location
为https://A.com/oauth/authorize?client_id=11E0ED550C5&redirect_uri=https://B.com/api/login&response_type=code&scope=READ&state=GKWzlE
看到这个location
,熟悉OAuth2.0
或者OpenId Connect
的童鞋肯定暗暗窃喜,比如博主,这个我熟悉,这个就是走的 授权码流程。根据302进行重定向。
A域名下重定向授权端点、换取授权码
-
GET
请求https://A.com/oauth/authorize?client_id=11E0ED550C5&redirect_uri=https://B.com/api/login&response_type=code&scope=READ&state=GKWzlE
,去换取授权码。状态码
302
location
为https://B.com/api/login?code=4jl6Ud&state=GKWzlE
看吧,换来了授权码,继续302重定向
继续重定向至B域名
-
GET
请求https://B.com/api/login?code=4jl6Ud&state=GKWzlE
,就这样切换了网站状态码302
location为
https://B.com/api//index
这里在请求中突然冒出来一个Cookie:SESSIONID=3c818c6d-97a1-407a-9512-9de604dde83d
,根据开发经验这个SESSIONID
有大用。
后续还有几次重定向,但是基本上是以保持登录状态(Cookie)在B网站中进行重定向。
Cookie: SESSIONID=3c818c6d-97a1-407a-9512-9de604dde83d; proxyUser=null; acw_tc=2760827b16125811058633717e8fce8be9948b90272f2fb598d2313cae0ba2; Hm_lvt_8aa7b2541dc5c9cfc124ff1d86098aaa=1612579177,1612579299,1612579497,1612581149; Hm_lpvt_8aa7b2541dc5c9cfc124ff1d86098aaa=1612581149
PS:后面Cookie中的Hm_lvt_
都是百度统计需要的,这不属于我们关心的内容,作为干扰项不予处理。
2.尝试编码
经过一番搜索,试用,甄别,博主最终选择了colly这个包。
colly
这个包非常强大,它的设计与使用也非常优雅,所有的处理逻辑,都采用的是事件注册的方式。
OnRequest
请求执行之前调用OnResponse
响应返回之后调用OnHTML
监听执行 selectorOnXML
监听执行 selectorOnHTMLDetach
,取消监听,参数为 selector 字符串OnXMLDetach
,取消监听,参数为 selector 字符串OnScraped
,完成抓取后执行,完成所有工作后执行OnError
,错误回调
然后就是请求,博主这边主要使用:
Request()
:请求,由开发者指定GET
、POST
请求,URL
等参数Post()
:Post
请求,由开发者指定POST
请求,及HTTP报文体Body
还有暂未使用的:
Visit()
:启动网页访问,博主本次没有使用这个方法。
2.1 安装colly包
博主这里使用的是最新的colly
v2
的版本,看了下,与原来的colly
是同一个github Repository
,放心使用。
go get -u github.com/gocolly/colly/v2
2.2 编码登录
package main
import (
"fmt"
"log"
"net/http"
"github.com/gocolly/colly/v2"
)
func PostLogin() (err error) {
c := colly.NewCollector(
colly.UserAgent(sameUserAgent),
)
var requestData = make(map[string]string, 3)
requestData["username"] = username
requestData["password"] = password
c.OnResponse(func(r *colly.Response) {
fmt.Println("response received", r.StatusCode)
if r.StatusCode == 200 {
fmt.Println("Firstly,Login Action And Redirect Success...")
}
})
c.OnRequest(func(r *colly.Request) {
r.Headers.Add("Cookie", cookie)
})
c.OnError(func(r *colly.Response, err error) {
// fmt.Println(r)
fmt.Println(err)
fmt.Println("Login Action And Redirect occurs error... Please Contact 287572291@qq.com")
})
//1.Login Action
err = c.Post(loginURL, requestData)
c.Wait()
}
3.问题出现了
3.1 状态码是200
OnResponse
中返回了状态码200,这下可懵逼了,并不是302.
由于博主爬虫经验尚少,起初是想通过自己捕获Response
报文的Header
中的Location
,然后用代码手动做重定向。经过实验:
- 请求重放
Wireshark
抓包代码的请求报文与Fiddler
捕获的请求对比
明明Request
是一模一样,但是Response
却完全不一样。
经过一番苦苦思考与寻觅,最后了解到很多爬虫框架,像python
的爬虫框架scrapy
,这些框架会帮助我们自适应302
的重定向,并不需要我们去做手动的重定向。这里还走了很多冤枉路,比如想着如何阻止重定向,博主列举一下自己在bing
上搜索过的关键字:
- go colly Redirect
- go colly prevent defautl redirect
3.2 网页没有跳转
了解到这一层,问题还是没有解决,因为通过在OnResponse
中 r.Request.URL
`中查看请求的URL,URL还是 https://A.com/login,这是为什么呢?实在搞不懂啦。
4.Postman登场
由于colly
的Response
的Body
都是字节切片:
// Response is the representation of a HTTP response made by a Collector
type Response struct {
// StatusCode is the status code of the Response
StatusCode int
// Body is the content of the Response
Body []byte
// Ctx is a context between a Request and a Response
Ctx *Context
// Request is the Request object of the response
Request *Request
// Headers contains the Response's HTTP headers
Headers *http.Header
// Trace contains the HTTPTrace for the request. Will only be set by the
// collector if Collector.TraceHTTP is set to true.
Trace *HTTPTrace
}
调试查看非常不方便,而且如上所述,现在也有问题横在前面,是时候换一个工具了postman
,这就不多介绍了,只是提一下:postman默认也是自动适应302重定向,当然你也可以关闭这个选项
加之,postman
还能具备了很多浏览器的动作,如通过获取Response
中的Set-Cookie
,然后自动设置Cookie
值,这都为我们调试提供了方便。
4.1 新发现
通过postman
进行登录,状态码的确是200,页面是没有跳转,如下图:
原来是返回Body
里面是有JavaScript
脚本的。看代码,应该是开发人员为测试还预留了代码。通过逻辑可以很容易看到,浏览器会发生重定向https://B.com/api/index.shtml
,这里Fidder
根本没有捕获到。
4.2 模拟登录操作,浏览器后续动作
这时查看Postman为我们管理的Cookie
请求B域名
按照javascript
脚本请求B.com/api/index.shtml
,注意,就在这里,浏览器已经从域名A切换至域名B。
新Cookie
很明显,这里我们Cookie
要增加了,没错就是B域名的Cookie
第一节的Fiddler
抓包的Cookie
中还有SESSIONID
,博主已经大致猜测到需要这个SESSIONID
值才能维持登录状态;为了让博主自己和看到这篇文章的童鞋死心,还是随便找了一个登录后才能访问的接口进行请求:
没错,We need it
5.新的问题
正如看到的那样,新的问题又摆在博主面前:
SESSIONID
在哪里?SESSIONID
从哪里来?Fiddler
捕获的oauth/authorize
授权端点,获取授权码的流程也没有经历,也绝对没有重定向,因为上面GET
请求.shtml
苦恼呀,算了,出去打会儿篮球换个脑子吧。
6.重大发现
就在博主一筹莫展的时候,那边为了重放请求开启的Firefox
浏览器好像已经登录超时了,再次操作B网站时,发生了重定向,且依然留在原来的页面,这一特殊的场景,让博主一下子精神起来,而且也被Fidder
捕获到了,在这个过程中发现了请求一次B.com/api/index
,注意看,没有.shtml
的后缀。马上拿着连接再在Postman
做测试,再次查看管理的Cookie
:
没错就是它了,为了进一步验证:设置关闭重定向,进行重放请求:
继续关闭重定向,拿着上图中的location
进行请求:
问题清晰
终于看到了我们前面通过Fiddler
抓包获取到的A域名下的oauth/authorize
端点。至于怎么获取到SESSIONID
,博主已经不想再去研究了,多半是类似IdentityServer4
这种第三方中间件自己做的各种骚操作,比如:
- 返回html,html中又有
iframe
自动请求
- 返回html,里面存在一个表单,然后脚本自动触发提交表单。
<script>window.addEventListener('load', function(){document.forms[0].submit();});</script>
登录成功
反正我们已经登录成功了。
7.编码正式开始
7.1 登录
很明显,**整个登录过程中,出现了很多次的A、B域名之间的反复重定向。**我们需要在代码中保护好在整个反复重定向过程中产生的Cookie
值,否则也是登录不成功,这是在通过Postman
的Cookie
管理中悟出的,调整为如下代码(注意看注释):
package main
import (
"fmt"
"log"
"net/http"
"github.com/gocolly/colly/v2"
)
func PostLogin() (err error) {
//1.Login Action Start
c := colly.NewCollector(
colly.UserAgent(sameUserAgent),
)
var requestData = make(map[string]string, 3)
requestData["username"] = username
requestData["password"] = password
c.OnResponse(func(r *colly.Response) {
fmt.Println("response received", r.StatusCode)
if r.StatusCode == 200 {
fmt.Println("Firstly,Login Action And Redirect Success...")
}
})
c.OnRequest(func(r *colly.Request) {
r.Headers.Add("Cookie", cookie)
})
c.OnError(func(r *colly.Response, err error) {
// fmt.Println(r)
fmt.Println(err)
fmt.Println("Login Action And Redirect occurs error... Please Contact 287572291@qq.com")
})
//1.Login Action End
err = c.Post(loginURL, requestData)
c.Wait()
//2.After Login Action, the Redirection Of "window.location.href" Start
c.OnResponse(func(r *colly.Response) {
fmt.Println("response received", r.StatusCode)
if r.StatusCode == 200 {
fmt.Println("After Login Action,by the \"window.location.href\",Get Cookie success...")
}
AfterLoginActionCookie = r.Headers.Get("Set-Cookie")
})
c.OnError(func(r *colly.Response, err error) {
// log.Println(r)
fmt.Println(err)
fmt.Printf("%s Redirect occurs error... Please Contact 287572291@qq.com", AfterLoginSuccessURL)
})
//2.After Login Action, the Redirection Of "window.location.href" End
err = c.Request("GET", AfterLoginSuccessURL, nil, nil, nil)
c.Wait()
//3.Obtain AfterLoginSuccess-Cookie Start
c.OnResponse(func(r *colly.Response) {
fmt.Println("response received", r.StatusCode)
fmt.Println("Get Redirect Cookie success...")
LoginSuccessCookie = r.Request.Headers.Get("cookie")
fmt.Printf("After Login Success,Obtain Full Cookie: %s", LoginSuccessCookie)
})
client = http.DefaultClient
client.CheckRedirect = func(req *http.Request, via []*http.Request) (err error) {
for _,v:=range via{
fmt.Println(v.URL)
}
return
}
c.OnRequest(func(r *colly.Request) {
r.Headers.Add("Cookie", AfterLoginActionCookie)
})
c.OnError(func(r *colly.Response, err error) {
log.Println(r)
log.Println(err)
log.Printf("%s After Login Success,Obtain Full Cookie occurs error... Please Contact 287572291@qq.com",
LoginSessionIDURL)
})
//3.Obtain AfterLoginSuccess-Cookie End
err = c.Request("GET", LoginSessionIDURL, nil, nil, nil)
c.Wait()
return
}
- Login Action
POST
用户名密码至A网站-
- After Login Action, the Redirection Of “window.location.href”
GET
window.location.href
中的B网站 B.com/api/index.shtml 获取acw_tc
开头的Cookie
- Obtain AfterLoginSuccess-Cookie
GET
B.com/api/index 获取SESSIONID
通过上述代码登录成功后,获取请求的Cookie
值,保存全局变量以供后续爬取数据使用:
//3.Obtain AfterLoginSuccess-Cookie Start
c.OnResponse(func(r *colly.Response) {
fmt.Println("response received", r.StatusCode)
fmt.Println("Get Redirect Cookie success...")
LoginSuccessCookie = r.Request.Headers.Get("cookie")
fmt.Printf("After Login Success,Obtain Full Cookie: %s", LoginSuccessCookie)
})
7.2 爬取数据
后续爬取数据难度不大,只需要认真分析请求与请求之间的关系,比如:
- B请求的参数来源于A请求的响应;
- 一系列的后续操作,来源于切换项目请求后的操作;
这些纯靠编码调整,就不做过多介绍。
注意一点
encoding/json
包的反序列化方法需要Struct
字段大写,否则反序列化会失败,且不会报错,截取代码片段如下:
var projectData ProjectResponse
c.OnResponse(func(r *colly.Response) {
log.Println("response received", r.StatusCode)
// fmt.Println(string(r.Body))
err = json.Unmarshal(r.Body, &projectData)
if err != nil {
fmt.Println(err)
}
// fmt.Println("反序列化结果:", projectData)
})
type ProjectResponse struct{
Code string
Action string
Msg string
Data ProjectData
Status bool
}
7.3 数据整理
预先设定数据展示字段,为其定义struct
,然后定义struct
切片用于保存不断增加的数据。
7.4 导出excel
这里主要是为了把数据导出至Excel表格,更方便查看,使用了github.com/tealeg/xlsx
包
安装xlsx包
go get -u github.com/tealeg/xlsx
编码
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
"github.com/gocolly/colly/v2"
"github.com/tealeg/xlsx"
)
var result []DeviceStatusResult
// LoopObtain 循环项目
func Export() (err error) {
// jsonResult,err:=json.Marshal(result)
// fmt.Println(string(jsonResult))
file := xlsx.NewFile()
sheet, err := file.AddSheet("Sheet1")
for _, v := range result {
row := sheet.AddRow()
row.SetHeightCM(1) //设置每行的高度
for i, d := range v.DeviceInfo {
if i == 0 {
cell := row.AddCell()
cell.Value = v.ProjectName
cell = row.AddCell()
cell.Value = d.DeviceNo
cell = row.AddCell()
cell.Value = d.Status
} else {
row := sheet.AddRow()
row.SetHeightCM(1) //设置每行的高度
cell := row.AddCell()
cell.Value = ""
cell = row.AddCell()
cell.Value = d.DeviceNo
cell = row.AddCell()
cell.Value = d.Status
}
}
}
err = file.Save("设备状态表.xlsx")
fmt.Println("Excel Export Success...")
return
}
xlsx.NewFile()
新建文件file.AddSheet("Sheet1")
增加表sheet.AddRow()
增加行row.AddCell()
行增加单元格
对于这个需求,上面的简单导出方法足矣,其余使用方法,感兴趣的童鞋可以自行查阅文档。
8.优化思考
经过测试,登录请求与数据爬取还是很快的。但是由于这个后台管理系统本身的具有一定操作局限性,比如上面提到的情况:一系列的后续请求,必须建立在切换项目请求发生后;因为切换后的状态由SESSION
保存。导致整个爬取流程必须按顺序经历:
- 选择项目
- 切换项目
- 请求A
- 请求B
- 请求C
一切都是有顺序性,想快也快不起来。
如何优化
但是我们还是想更快一点,就要想办法,博主的思考就是进行多个登录操作大概5-10个:
- 保存多个Cookie,用以维护好多个登录状态
- 请求并保存所有的切换的项目列表
- 建立一个
goroutine pool
- 创建多个爬取数据的goroutine
- 创建channel通道,用以接收项目,
chan<-项目
- 通过channel通道向goroutine传递项目
这样,效率应该会大幅度提高,毕竟只是为老婆省点事,把并发量搞大了没必要,搞不好会被阿里云判断为DDOS攻击就惨了。
- 原文作者:Garfield
- 原文链接:http://www.randyfield.cn/post/2021-02-19-go-spider/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。