简介
CDP全称Chrome DevTools Protocol
,是一种控制开发者工具的协议。可以通过已开放的功能,来间接操纵浏览器完成一系列自动化工作。
我使用的工具
chromedp
是一个使用Golang封装CDP的项目,它的功能庞大,包括一些常用的元素点击、滑动事件等等,如果是做 自动化测试 的话,选用它更加合适。mafredri/cdp
也是一个使用Golang封装CDP的项目,但是它仅仅是做了封装,并保持整体API风格的统一,减少我们使用过程中的不适。本文将使用这个框架进行开发。headless-shell
是一个基于debian的无界面chrome浏览器的docker镜像,可以集成到我们常用的微服务架构里。
搭建使用CDP的整体框架
虽然mafredri/cdp
提供了最基础的使用方法,但是我们需要在它的基础上修改,以便能满足我们的需求,和持续稳定爬取的要求。
初始化与开发者工具的远程连接
var (
cdpHost = "127.0.0.1"
cdpPort = "9222"
devt *devtool.DevTools
)
func init() {
devt = devtool.New(fmt.Sprintf("http://%s:%d", cdpHost, cdpPort))
}
通过访问页面来获取所有请求返回的数据
func Run(url string) {
ctx, cancel := context.WithTimeout(context.Background(), timeout) //该页面的超时时间,最好增加多一些缓冲时间,不然页面线程可能比获取数据的线程早退出
defer cancel()
//打开一个空白页
pt, err := devt.Create(ctx)
if err != nil {
return
}
defer devt.Close(ctx, pt) //退出时关闭
conn, err := rpcc.DialContext(ctx, pt.WebSocketDebuggerURL)
if err != nil {
return
}
defer conn.Close()
c := cdp.NewClient(conn)
// 该注释部分是模拟某种设备
// err = c.Emulation.ClearDeviceMetricsOverride(ctx)
// if err != nil {
// return
// }
// err = c.Emulation.SetDeviceMetricsOverride(ctx,
// &emulation.SetDeviceMetricsOverrideArgs{
// Width: config.Device.WindowWidth,
// Height: config.Device.WindowHeight,
// DeviceScaleFactor: 1,
// Mobile: config.Device.Mobile})
// if err != nil {
// return
// }
domContent, err := c.Page.DOMContentEventFired(ctx)
if err != nil {
return
}
defer domContent.Close()
// 批量处理错误
if err = runBatch(
func() error { return c.DOM.Enable(ctx) },
func() error { return c.Network.Enable(ctx, nil) },
func() error { return c.Page.Enable(ctx) },
); err != nil {
return
}
// 监听每个请求收到的数据
go func() {
responseReceived, err := c.Network.ResponseReceived(ctx)
if err != nil {
return
}
defer responseReceived.Close()
ticker := time.NewTicker(timeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
return
case <-ctx.Done():
return
case <-responseReceived.Ready(): //每个请求获得的数据
ev, err := responseReceived.Recv()
if err != nil {
time.Sleep(time.Millisecond * 100)
break
}
if bodyReply, err := c.Network.GetResponseBody(ctx, &network.GetResponseBodyArgs{RequestID: ev.RequestID}); err == nil {
//异步执行回调任务
// go func() {
// callback(ev, bodyReply.Body)
// }()
}
}
}
}()
_, err = c.Page.Navigate(ctx, page.NewNavigateArgs(url))
if err != nil {
return
}
time.Sleep(timeout)
// 此处后加入你需要进行的操作,一个网站加载所有请求,和渲染页面都需要时间, 超时时间需要酌情设置。
// 比如点击事件啥的玩意
}
type runBatchFunc func() error
func runBatch(fn ...runBatchFunc) error {
eg := errgroup.Group{}
for _, f := range fn {
eg.Go(f)
}
return eg.Wait()
}
其实前面的操作已经可以获取到页面中所有请求返回的数据了,剩下的就是根据接口数据的特征来提取想要的数据。
封装点击事件
其实如果要更多功能,直接用chromedp
可能是一个更好的选择。但是我们如果使用了mafredri/cdp
,我们也可以参考chromedp
的实现方式来封装一些常用功能。
点击某个点
func Click(clt *cdp.Client, x, y float64) (err error) {
ts := input.TouchPoint{X: x, Y: y}
err = clt.Input.DispatchTouchEvent(clt.Ctx, &input.DispatchTouchEventArgs{
Type: "touchStart",
TouchPoints: []input.TouchPoint{ts},
})
if err != nil {
return
}
err = clt.Input.DispatchTouchEvent(clt.Ctx, &input.DispatchTouchEventArgs{
Type: "touchEnd",
TouchPoints: []input.TouchPoint{},
})
if err != nil {
return
}
return
}
点击某个元素
func ClickElement(clt *cdp.Client, el, text string) (err error) {
expression := fmt.Sprintf(`
function contains(selector, text) {
var elements = document.querySelectorAll(selector);
return Array.prototype.filter.call(elements, function(element){
return RegExp(text).test(element.textContent);
});
}
contains('%s', '%s').forEach((value, index, array) => {
value.click();
});
`, el, text)
evalArgs := runtime.NewEvaluateArgs(expression)
_, err = clt.Runtime.Evaluate(c.Ctx, evalArgs)
if err != nil {
return
}
return
}
额外的话
如果要搭建好整个基于cdp的爬虫,当然要做很多封装的工作。比如我们可以封装一台IPhone6 😏
type Device struct {
WindowWidth int
WindowHeight int
Mobile bool
}
func IPhone6() Device {
return Device{
WindowWidth: 375,
WindowHeight: 667,
Mobile: true,
}
}
启动一个爬虫任务
func init() {
NewCrawler(
NewDriver(Config{Timeout: timeout, Device: IPhone6()}),
).Run()
}