golang headless browser包chromedp初探

BrookeRiva 发布于1月前
0 条问题

前天晚上像写个网站自动投稿,但是chrome F12抓的包里请求的几个参数里的值不知道js咋生成的,看不懂js。询问了下网友,网友看我截图请求蛮多的,说有空帮我看看。并且他说到了模拟过程虽然能成功但是可能反爬措施强会导致封号,建议我用无头浏览器整。

搜了下相关概念,无头浏览器的话python里就是 selenium 驱动的,广泛使用的headless browser解决方案PhantomJS已经宣布不再继续维护,转而推荐使用headless chrome。Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的前提下,使用所有 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

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

场景一

https://cn.bing.com/?mkt=zh-CN
zhangguanzhang

代码

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)
}

运行结果

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

Process finished with exit code 0

截图图片为:

golang headless browser包chromedp初探

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

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来筛选操作的标签去执行

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

第二个参数是QueryOption,缺省是 chromedp.BySearch ,允许使用CSS或XPath选择器查询元素,包装DOM.performSearch,其他的有ByID,ByQuery,ByQueryAll,ByJSPath

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

调试和其他

讲解简单调和一些场景

UA

实际动手的时候发现一只hang住一样,才醒悟到网站组织了ua

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)
}

输出

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数组,记住是数组

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
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)
}

输出

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,但是我们可以利用切片在尾部追加,来覆盖掉前面的选项,例如

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

而headless的函数内容为

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

所以开启gui这样写

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浏览器,这是包里的代码

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设置下

查看原文: golang headless browser包chromedp初探

  • ticklishrabbit
  • silverfish
  • smallcat
  • lazyfish
  • lazydog
  • redgorilla
  • greenpeacock
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。