zhangguanzhang's Blog

golang headless browser包chromedp初探

字数统计: 2.8k阅读时长: 13 min
2019/07/14

什么是cdp

前天晚上想写个网站自动投稿,但是 chrome F12 抓的包里请求的几个参数里的值不知道 js 咋生成的,看不懂 js。询问了下网友,网友看我截图请求蛮多的,说有空帮我看看。并且他说到了模拟过程虽然能成功但是可能反爬措施强会导致封号,建议我用无头浏览器整。
搜了下相关概念,无头浏览器的话 python 里就是 selenium驱动的,广泛使用的 headless browser 解决方案 PhantomJS 已经宣布不再继续维护,转而推荐使用 headless chrome。Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的 gui 前提下,使用所有 Chrome 支持的特性运行你的程序。
反爬措施的目的就是保证正常用户的访问,拒绝爬虫的访问。这个时候,我们就在思索一件事,不管他步骤怎样复杂化,他还是要对正常的浏览器提供业务支持,换而言之,他再复杂的请求步骤也会被浏览器完美执行。使用浏览器自己当爬虫,加大了资源消耗,爬取速度明显变慢,但是简化了开发步骤,缩短了开发周期,在某些情况下,这个技术还是非常有利可图的。
golang 里驱动headless chrome有着开源库chromedp(在2017年的gopher大会上有展示过),它是使用Chrome Debugging Protocol(简称cdp) 并且没有外部依赖 (如Selenium, PhantomJS等)。
浏览器本身其实还充当着一个服务端的角色,大家应该都用过chrome浏览器的F12,也就是devtools,其实这是一个web应用,当你使用devtools的时候,而你看到的浏览器调试工具界面,其实只是一个前端应用,在这中间通信的,就是 cdp,他是基于 websocket 的,一个让 devtools 和浏览器内核交换数据的通道。cdp的官方文档地址 https://chromedevtools.github.io/devtools-protocol/ 可以点击查阅。

chromedp能做什么

  • 反爬虫js,例如有的网页后台js自动发送心跳包,浏览器里会自动运行,不需要我们自动处理
  • 针对于前端页面的自动化测试
  • 解决类似VueJS和SPA之类的渲染
  • 解决网页的懒加载
  • 网页截图和pdf导出,而不需要额外的去学习其他的库实现
  • seo训练和刷点击量
  • 执行javascript 代码
  • 设置dom的标签属性

使用前提

懂一点html和 css 以及js,因为操作 html 的 dom 元素需要用到 xpath 和 css 选择器之类的,如果 F12 的 element 里会右击复制 selector 也行,但是复杂的选择器还得需要 xpath 或者 css 选择器。不会使用的话简单教下:
chrome 打开网页 F12 后下面的调试工具出来后点击Elements,然后点击elements右边的那个框框里的鼠标箭头,点击后变蓝色,然后放到网页上选中区域点击一下,下面的内容就跳到对应地方,然后下面右击html的标签->Copy->COpy selector或者xpath,就能复制选择器了。

安装

拉不下来的自行开GO111MODULE并且设置goproxy

1
go get -u github.com/chromedp/chromedp@master

场景一

  • 打开必应页面https://cn.bing.com/?mkt=zh-CN
  • 输入zhangguanzhang
  • 点击搜索
  • 打印第一个搜索结构的超链接地址
  • 截图浏览器看到的界面

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"context"
"io/ioutil"
"log"
"time"

"github.com/chromedp/chromedp"
)

func main() {

var buf []byte

// create chrome instance
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()

// create a timeout
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()

// navigate to a page, wait for an element, click
var example string
err := chromedp.Run(ctx,
//访问打开必应页面
chromedp.Navigate(`https://cn.bing.com/?mkt=zh-CN`),
// 等待右下角图标加载完成
chromedp.WaitVisible(`#sh_cp_in`),
//搜索框内输入zhangguanzhang
chromedp.SendKeys(`#sb_form_q`, `zhangguanzhang`, chromedp.ByID),
// 点击搜索图标
chromedp.Click(`#sb_form_go`, chromedp.NodeVisible),
// 获取第一个搜索结构的超链接
chromedp.Text(`#b_results > li:nth-child(2) > div > div > cite`, &example),
chromedp.CaptureScreenshot(&buf),
)
if err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile("fullScreenshot.png", buf, 0644); err != nil {
log.Fatal(err)
}
log.Printf("example: %s", example)
}

运行结果

1
2
3
2019/07/14 16:20:25 example: https://zhangguanzhang.github.io

Process finished with exit code 0

截图图片为:

s1

Run函数接收一个context和Action接口的切片

1
func Run(ctx context.Context, actions ...Action) error {

godoc页面为 https://godoc.org/github.com/chromedp/chromedp

action不止Action,还有QueryAction,NavigateAction,MouseAction,KeyAction…,自行查看godoc。其中的QueryAction是依赖于元素定位去操作的,例如点击和文本框的输入,你得指定第一个参数传入xpath或者selector来筛选操作的标签去执行

1
func XXXX(sel interface{}, opts ...QueryOption) QueryAction

第二个参数是QueryOption,缺省是chromedp.BySearch,允许使用CSS或XPath选择器查询元素,包装DOM.performSearch

常用选择器

1
2
3
4
5
6
chromedp.BySearch // 如果不写,默认会使用这个选择器,类似devtools ctrl+f 搜索,效果等同于`document.querySelector(...)` 去测下
chromedp.ByID // 只id来选择元素
chromedp.ByQuery // 根据document.querySelector的规则选择元素,返回单个节点
chromedp.ByQueryAll // 根据document.querySelectorAll返回所有匹配的节点
chromedp.ByNodeIP // 检索特定节点(必须先有分配的节点IP),这个暂时没用过也没看到过例子,如果有例子可以发给我看下
chromedp.ByJSPath

其他的自行去看go doc里讲解吧。下面说些其他的

调试和其他

讲解简单调和一些场景

UA

实际动手的时候发现一直hang住一样,才醒悟到网站应该检测了user agent了,下面代码借助网站返回ua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"context"
"log"
"time"

"github.com/chromedp/chromedp"
)

func main() {

var ua string
// create chrome instance
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()

// create a timeout
ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
defer cancel()

err := chromedp.Run(ctx,
chromedp.Navigate(`https://www.whatsmyua.info/?a`),
chromedp.WaitVisible(`#custom-ua-string`),
chromedp.Text(`#custom-ua-string`, &ua),
)
if err != nil {
log.Fatal(err)
}
log.Printf("user agent: %s", ua)
}

输出

1
2019/07/14 17:21:09 user agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/75.0.3770.100 Safari/537.36

网站应该拦截了HeadlessChrome,所以需要自行设置ua
这是包里默认的flag数组,记住是数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var DefaultExecAllocatorOptions = [...]ExecAllocatorOption{
NoFirstRun,
NoDefaultBrowserCheck,
Headless,

// After Puppeteer's default behavior.
Flag("disable-background-networking", true),
Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
Flag("disable-background-timer-throttling", true),
Flag("disable-backgrounding-occluded-windows", true),
Flag("disable-breakpad", true),
Flag("disable-client-side-phishing-detection", true),
Flag("disable-default-apps", true),
Flag("disable-dev-shm-usage", true),
Flag("disable-extensions", true),
Flag("disable-features", "site-per-process,TranslateUI,BlinkGenPropertyTrees"),
Flag("disable-hang-monitor", true),
Flag("disable-ipc-flooding-protection", true),
Flag("disable-popup-blocking", true),
Flag("disable-prompt-on-repost", true),
Flag("disable-renderer-backgrounding", true),
Flag("disable-sync", true),
Flag("force-color-profile", "srgb"),
Flag("metrics-recording-only", true),
Flag("safebrowsing-disable-auto-update", true),
Flag("enable-automation", true),
Flag("password-store", "basic"),
Flag("use-mock-keychain", true),
}

还有一些可能需要用到的

  • –no-first-run 第一次不运行
  • –default-browser-check 不检查默认浏览器
  • –headless 不开启图像界面
  • –disable-gpu 关闭gpu,服务器一般没有显卡
  • –remote-debugging-port chrome-debug工具的端口(golang chromepd 默认端口是9222,建议不要修改)
  • –no-sandbox 不开启沙盒模式可以减少对服务器的资源消耗,但是服务器安全性降低,配和参数 - –remote-debugging-address=127.0.0.1 一起使用
  • –disable-plugins 关闭chrome插件
  • –remote-debugging-address 远程调试地址 0.0.0.0 可以外网调用但是安全性低,建议使用默认值 127.0.0.1
  • –window-size 窗口尺寸
    更多参数说明详解headless-chrome官方文档 https://developers.google.com/web/updates/2017/04/headless-chrome
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"context"
"github.com/chromedp/chromedp"
"log"
)

func main() {

var ua string

ctx := context.Background()
options := []chromedp.ExecAllocatorOption{
chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
}
options = append(options, chromedp.DefaultExecAllocatorOptions[:]...)

c, cc := chromedp.NewExecAllocator(ctx, options...)
defer cc()
// create context
ctx, cancel := chromedp.NewContext(c)
defer cancel()

err := chromedp.Run(ctx,
chromedp.Navigate(`https://www.whatsmyua.info/?a`),
chromedp.WaitVisible(`#custom-ua-string`),
chromedp.Text(`#custom-ua-string`, &ua),
)
if err != nil {
log.Fatal(err)
}
log.Printf("user agent: %s", ua)
}

输出

1
2019/07/14 17:24:49 user agent: Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36

开启GUI来debug

然后还是遇到了hang住,不知道为啥,询问了别人说可以关闭headless来开启gui,这样可以看到chrome具体在干啥了

虽然默认选项里是开启了headless,但是我们可以利用切片在尾部追加,来覆盖掉前面的选项,例如

1
2
3
4
5
$ seq 5 | head -n 1
1
$ seq 5 | head -n 1 -n 2
1
2

而headless的函数内容为

1
2
3
4
5
6
func Headless(a *ExecAllocator) {
Flag("headless", true)(a)
// Like in Puppeteer.
Flag("hide-scrollbars", true)(a)
Flag("mute-audio", true)(a)
}

所以开启gui这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"context"
"github.com/chromedp/chromedp"
"log"
"time"
)

func main() {

var ua string

ctx := context.Background()
options := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless", false),
chromedp.Flag("hide-scrollbars", false),
chromedp.Flag("mute-audio", false),
chromedp.UserAgent(`Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36`),
}

options = append(chromedp.DefaultExecAllocatorOptions[:], options...)

c, cc := chromedp.NewExecAllocator(ctx, options...)
defer cc()
// create context
ctx, cancel := chromedp.NewContext(c)
defer cancel()

err := chromedp.Run(ctx,
chromedp.Navigate(`https://www.whatsmyua.info/?a`),
chromedp.WaitVisible(`#custom-ua-string`),
chromedp.Text(`#custom-ua-string`, &ua),
chromedp.Sleep(10* time.Second),
)
if err != nil {
log.Fatal(err)
}
log.Printf("user agent: %s", ua)
}

运行会看到chrome被打开一个新窗口,写着被自动控制着,如果遇到问题我们可以实时的观察

设置chrome的execPath

实际上运行都是依赖于机器上有chrome浏览器,这是包里的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func ExecPath(path string) ExecAllocatorOption {
return func(a *ExecAllocator) {
// Convert to an absolute path if possible, to avoid
// repeated LookPath calls in each Allocate.
if fullPath, _ := exec.LookPath(path); fullPath != "" {
a.execPath = fullPath
} else {
a.execPath = path
}
}
}

// findExecPath tries to find the Chrome browser somewhere in the current
// system. It performs a rather agressive search, which is the same in all
// systems. That may make it a bit slow, but it will only be run when creating a
// new ExecAllocator.
func findExecPath() string {
for _, path := range [...]string{
// Unix-like
"headless_shell",
"headless-shell",
"chromium",
"chromium-browser",
"google-chrome",
"google-chrome-stable",
"google-chrome-beta",
"google-chrome-unstable",
"/usr/bin/google-chrome",

// Windows
"chrome",
"chrome.exe", // in case PATHEXT is misconfigured
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,

// Mac
`/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
} {
found, err := exec.LookPath(path)
if err == nil {
return found
}
}
// Fall back to something simple and sensible, to give a useful error
// message.
return "google-chrome"
}

如果我们的安装路径变了可以用ExecPath设置下

官方的demo

s2

执行js

有些函数不会返回,所以可以下面,或者[]byte

1
2
chromedp.Evaluate(`document.getElementById('iframe').contentWindow.document.querySelector('div[class^=ckplayer] video').click();aaa="111"`,
&res),

https://github.com/chromedp/chromedp/issues/381#issuecomment-500662646

一些坑

iframe

https://github.com/chromedp/chromedp/issues/72#issuecomment-642151827
https://github.com/chromedp/chromedp/blob/49daeb65bff2056e97c1eaee79c8e924566ae675/nav_test.go#L278-L286
https://github.com/chromedp/chromedp/issues/212#issuecomment-416462733

CATALOG
  1. 1. 什么是cdp
  2. 2. chromedp能做什么
    1. 2.1. 使用前提
    2. 2.2. 安装
    3. 2.3. 场景一
      1. 2.3.1. 常用选择器
  3. 3. 调试和其他
    1. 3.1. UA
    2. 3.2. 开启GUI来debug
    3. 3.3. 设置chrome的execPath
  4. 4. 官方的demo
  5. 5. 执行js
  6. 6. 一些坑