go语言chromedp使用教程
1. chromedp 是什么?
而最近广泛使用的headless browser解决方案PhantomJS已经宣布不再继续维护,转而推荐使用headless chrome.
那么headless chrome究竟是什么呢,Headless Chrome 是 Chrome 浏览器的无界面形态,可以在不打开浏览器的前提下,使用所有 Chrome 支持的特性运行你的程序.
简而言之,除了没有图形界面,headless chrome具有所有现代浏览器的特性,可以像在其他现代浏览器里一样渲染目标网页,并能进行网页截图,获取cookie,获取html等操作.
想要在golang程序里使用headless chrome,需要借助一些开源库,实现和headless chrome交互的库有很多,这里选择chromedp,接口和Selenium类似,易上手.
chromedp提供一种更快,更简单的方式来驱动浏览器 (Chrome, Edge, Safari, Android等)在 Go中使用Chrome Debugging Protocol 并且没有外部依赖 (如Selenium, PhantomJS等).
2. chromedp 能够做什么?
- 使用chromedp解决反爬虫JS问题
- 使用chromedp做网站的自动化测试
- 使用chromedp服务器代码渲染(主要是解决VueJS等SPA应用)
- 使用chromedp做网页截图程序
- 使用chromedp做刷点击量/刷赞/搜索引擎SEO训练….(click farming)
3. 使用 chromedp
使用chromedp 之前你必须有一下基础
- 少量linux(centos)基础
- 少量javascript selector/xpath 基础
- go 语言基础
- go 要熟悉go 中使用函数作为参数(闭包)的写法.
- 少量函数是编程概念(chromedp 有很多函数是编程写法)
3.1 安装go语言包
go get
命令安装chromedp chromepd
包
go get -u github.com/chromedp/chromedp
3.2 chromedp使用chrome 普通模式
普通模式会在电脑上弹出浏览器窗口.调用完成之后需要关闭掉浏览器,
当然在电脑上也可以使用chrome headless 模式, 缺点就是你多次go run main.go 的时候, go 代码运行中断导致后台chrome headless不能退出,导致第二次本地调试失败, 解决方案就是自己手动结束chrome进程.
建议在不提调试go代码的时候不要使用 chrome headless 模式. 使用普通模式可以在浏览器中看到代码执行的效果.
在我本机(windows10)上测试的时候chromedp 提示找不到chrome.exe
所以需要制定一下chrome.exe的执行程序地址
runner.Path(`C:\Users\zhouqing1\AppData\Local\Google\Chrome\Application\chrome.exe`),
main.go 代码
package main
import (
"context"
"github.com/chromedp/chromedp/runner"
"io/ioutil"
"log"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
)
func main() {
var err error
// create context
ctxt, cancel := context.WithCancel(context.Background())
defer cancel()
// 本期启动chrome的一些参数相当于执行了 shell 命令
// C:\Users\zhouqing1\AppData\Local\Google\Chrome\Application\chrome.exe --no-default-browser-check=true --no-sandbox=true --window-size=1280,900
// 如果需要更多参数详解chrome浏览器参数的文档
runnerOps := chromedp.WithRunnerOptions(
//我的windows10电脑使用chromedp默认配置导致找不到chrome.exe
//这行代码可以注释掉,如果找不到自己的chrome.exe 请像我一样制定chrome.exe路径
//一下配置都不是必选的
//更多参数详解文档 https://blog.csdn.net/wanwuguicang/article/details/79751571
runner.Path(`C:\Users\zhouqing1\AppData\Local\Google\Chrome\Application\chrome.exe`),
//启动chrome的时候不检查默认浏览器
runner.Flag("no-default-browser-check", true),
//启动chrome 不适用沙盒, 性能优先
runner.Flag("no-sandbox", true),
//设置浏览器窗口尺寸,
runner.WindowSize(1280, 1024),
//设置浏览器的userage
runner.UserAgent(`Mozilla/5.0 (iPhone; CPU OS 11_0 like Mac OS X) AppleWebKit/604.1.25 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1`),
)
//在普通模式的情况下启动chrome程序,并且建立共代码和chrome程序的之间的连接(https://127.0.0.1:9222)
c, err := chromedp.New(ctxt, chromedp.WithLog(log.Printf), runnerOps)
if err != nil {
log.Fatal(err)
}
var siteHref, title, iFrameCode string
err = c.Run(ctxt, visitMojoTvDotCn("https://mojotv.cn/2018/12/10/how-to-create-a-https-proxy-serice-in-100-lines-of-code.html", &siteHref, &title, &iFrameCode))
if err != nil {
log.Fatal(err)
}
log.Printf("`%s` (%s),html:::%s", title, siteHref, iFrameCode)
// shutdown chrome
err = c.Shutdown(ctxt)
if err != nil {
log.Fatal(err)
}
// wait for chrome to finish
err = c.Wait()
if err != nil {
log.Fatal(err)
}
}
func visitMojoTvDotCn(url string, elementHref, pageTitle, iFrameHtml *string) chromedp.Tasks {
//临时放图片buf
var buf []byte
return chromedp.Tasks{
//跳转到页面
chromedp.Navigate(url),
//chromedp.Sleep(2 * time.Second),
//等待博客正文显示
chromedp.WaitVisible(`#post`, chromedp.ByQuery),
//滑动页面到google adsense 广告
chromedp.ScrollIntoView(`ins`, chromedp.ByQuery),
chromedp.Screenshot(`#post`, &buf, chromedp.ByQuery, chromedp.NodeVisible),
//等待2s
chromedp.Sleep(2 * time.Second),
//截图到文件
chromedp.ActionFunc(func(context.Context, cdp.Executor) error {
//保存图片到mojotv_local.png
return ioutil.WriteFile("mojotv_local.png", buf, 0644)
}),
//滑动页面到#copyright
chromedp.ScrollIntoView(`#copyright`, chromedp.ByID),
//等待mojotv google广告展示出来
chromedp.WaitVisible(`#post__title`, chromedp.ByID),
chromedp.Sleep(2 * time.Second),
//获取我的google adsense 广告代码
chromedp.InnerHTML(`#post__title`, iFrameHtml, chromedp.ByID),
//跳转到我的bilibili网站
chromedp.Sleep(5 * time.Second),
chromedp.Click("#copyright > a:nth-child(3)", chromedp.NodeVisible),
//等待则个页面显现出来
chromedp.WaitVisible(`#page`, chromedp.ByQuery),
//在chrome浏览器页面里执行javascript
chromedp.Evaluate(`document.title`, pageTitle),
chromedp.Screenshot(`#page`, &buf, chromedp.ByQuery, chromedp.NodeVisible),
chromedp.Sleep(5 * time.Second),
//截取bili网页图片
chromedp.ActionFunc(func(context.Context, cdp.Executor) error {
return ioutil.WriteFile("bili_local.png", buf, 0644)
}),
//获取bilibili网页的标题
chromedp.JavascriptAttribute(`a`, "href", elementHref, chromedp.ByQuery),
}
}
chromedp普通模式你可以通过 runner.Flag
函数来定义 chrome 启动的参数
chromedp截图效果
3.3 chromedp使用chrome headless模式(不会弹出GUI界面)
3.3.1 Centos7(没有图像界面) 安装chrome
使用官方Docker安装
- 下载docker image :
docker pull chromedp/headless-shell
- 运行docker :
docker run -d -p 9222:9222 --rm --name headless-shell chromedp/headless-shell
官方这安装方法在我的服务器上安装失败.
在服务器yum安装chronium-headless
- 搜索chrome 的yum源
[ericzhou@mojotv ~]$ yum search chromium Loaded plugins: fastestmirror, langpacks Loading mirror speeds from cached hostfile ================================================================================== N/S matched: chromium =================================================================================== chromium-common.x86_64 : Files needed for both the headless_shell and full Chromium chromium-headless.x86_64 : A minimal headless shell built from Chromium chromium-libs.x86_64 : Shared libraries used by chromium (and chrome-remote-desktop)
-
选择
chromium-headless.x86_64
, 执行sudo yum install chromium-headless.x86_64
,我的服务器上已经安装好了[ericzhou@mojotv ~]$ sudo yum install chromium-headless.x86_64 Loaded plugins: fastestmirror, langpacks ADDOPS-base | 2.9 kB 00:00:00 base | 3.6 kB 00:00:00 centosplus | 3.4 kB 00:00:00 docker-ce-stable | 3.5 kB 00:00:00 epel | 4.7 kB 00:00:00 extras | 3.4 kB 00:00:00 google-chrome | 1.3 kB 00:00:00 updates | 3.4 kB 00:00:00 google-chrome/primary | 1.7 kB 00:00:00 Loading mirror speeds from cached hostfile
-
寻找chrome 二进制文件位置
rpm -ql chromium-headless.x86_64
[ericzhou@mojotv ~]$ rpm -ql chromium-headless.x86_64 /usr/lib64/chromium-browser/headless_shell
我的安装可执行文件路径在
/usr/lib64/chromium-browser/headless_shell
-
使用非root用户运行
chrome
[ericzhou@mojotv chromium-browser]$ nohup /usr/lib64/chromium-browser/headless_shell --no-first-run --no-default-browser-check --headless --disable-gpu --remote-debugging-port=9222 --no-sandbox --disable-plugins --remote-debugging-address=0.0.0.0 --window-size=1920,1080 & [1] 21747 [ericzhou@mojotv chromium-browser]$ nohup: ignoring input and appending output to ‘/home/zhouqing1/nohup.out’
headless_shell(chrome) Flag 参数说明
--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
窗口尺寸
-
查看端口服务器端口是否开启是否开启
[ericzhou@mojotv chromium-browser]$ netstat -lntp (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:139 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN - tcp 0 0 0.0.0.0:9222 0.0.0.0:* LISTEN 21747/headless_shel
chrome-headless 9222端口效果
3.3.2 golang代码实现chromedp 调用远程chrome-headless程序
一下代码实例包含多个chromedp/example多个项目的功能
- chromedp 屏幕截图
- chromedp 和chrome浏览器分离, 远程调用
- chromedp 提取页面元素
- chromedp 执行javascript 代码
- chromedp 点击页面跳转
package main
import (
"context"
//"fmt"
"io/ioutil"
"log"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/chromedp"
"github.com/chromedp/chromedp/client"
)
func main() {
var err error
// create context
ctxt, cancel := context.WithCancel(context.Background())
defer cancel()
// 连接我远程服务器上启动和chrome-headless 服务器
// 因为我的代码不是在我的笔记本上运行,所以不能使用client.New默认配置
// 所以使用client.URL来自定义自己服务器地址
c, err := chromedp.New(ctxt, chromedp.WithTargets(client.New(client.URL("http://pan.mojotv.cn:9222/json")).WatchPageTargets(ctxt)), chromedp.WithLog(log.Printf))
if err != nil {
log.Fatal(err)
}
// run task list
var siteHref, title, iFrameCode string
err = c.Run(ctxt, visitMojoTvDotCn("https://mojotv.cn/2018/12/10/how-to-create-a-https-proxy-serice-in-100-lines-of-code.html", &siteHref, &title, &iFrameCode))
if err != nil {
log.Fatal(err)
}
log.Printf("`%s` (%s),html:::%s", title, siteHref, iFrameCode)
}
func visitMojoTvDotCn(url string, elementHref, pageTitle, iFrameHtml *string) chromedp.Tasks {
//临时放图片buf
var buf []byte
return chromedp.Tasks{
//跳转到页面
chromedp.Navigate(url),
//chromedp.Sleep(2 * time.Second),
//等待博客正文显示
chromedp.WaitVisible(`#post`, chromedp.ByQuery),
//滑动页面到google adsense 广告
chromedp.ScrollIntoView(`ins`, chromedp.ByQuery),
chromedp.Screenshot(`#post`, &buf, chromedp.ByQuery, chromedp.NodeVisible),
//截图到文件
chromedp.ActionFunc(func(context.Context, cdp.Executor) error {
return ioutil.WriteFile("mojotv.png", buf, 0644)
}),
//等待mojotv google广告展示出来
chromedp.WaitVisible(`ins`, chromedp.ByQuery),
//获取我的google adsense 广告代码
chromedp.InnerHTML(`ins`, iFrameHtml, chromedp.ByQuery),
//跳转到我的bilibili网站
chromedp.Click("#copyright > a:nth-child(3)", chromedp.NodeVisible),
//等待则个页面显现出来
chromedp.WaitVisible(`#page-index`, chromedp.ByQuery),
//在chrome浏览器页面里执行javascript
chromedp.Evaluate(`document.title`, pageTitle),
chromedp.Screenshot(`#page-index`, &buf, chromedp.ByQuery, chromedp.NodeVisible),
//截取bili网页图片
chromedp.ActionFunc(func(context.Context, cdp.Executor) error {
return ioutil.WriteFile("bili.png", buf, 0644)
}),
//获取bilibili网页的标题
chromedp.JavascriptAttribute(`a`, "href", elementHref, chromedp.ByQuery),
}
}
chromedp代码和chrome-headless分离优缺点
- 优点: chrome只需要一个实例deamon运行,节省资源
- 缺点:不能在golang中创建chrome-headless 服务导致,不能控制chrome-headless的参数 浏览器的尺寸,useragent
- 缺点:服务端chrome-headless一般都缺少中文字体,需要到服务器安装字体
截图效果
不能显示字体,因为我的centos7服务器没有安装中文字体导致, centos 安装中文字体教程
4. 总结
对与不习惯函数式编程的同学来说,chromedp的代码还是比较奇怪不是容易看懂, 但是如果你有耐心多点击cmd+鼠标左键还是可以看懂的,需要有耐心. chromedp在使用selector 和执行js代码的时,如果表达式复杂就会找不到元素或者,js代码复制就会执行出错. 但是满足大部分需求是没有问题的.