×

golang headless browser包chromedp初探

Falcon 2020-06-24 views:
自动摘要

正在生成中……

什么是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
chromedp.BySearch // 如果不写,默认会使用这个选择器,类似devtools ctrl+f 搜索
chromedp.ByID // 只id来选择元素
chromedp.ByQuery // 根据document.querySelector的规则选择元素,返回单个节点
chromedp.ByQueryAll // 根据document.querySelectorAll返回所有匹配的节点
chromedp.ByNodeIP // 检索特定节点(必须先有分配的节点IP),这个暂时没用过也没看到过例子,如果有例子可以发给我看下

其他的自行去看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
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

原文作者:Zhangguanzhang