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 做网页截图程序
- 使用 chromedp 做刷点击量/刷赞/搜索引擎 SEO 训练….(click farming)
3. 使用 chromedp
使用 chromedp 之前你必须有一下基础
- 少量 linux(centos) 基础
- 少量 javascript selector/xpath 基础
- go 语言基础
- go 要熟悉 go 中使用函数作为参数 (闭包) 的写法.
- 少量函数是编程概念 (chromedp 有很多函数是编程写法)
3.1 安装 go 语言包
go getchromepd
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:Userszhouqing1AppDataLocalGoogleChromeApplicationchrome.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:Userszhouqing1AppDataLocalGoogleChromeApplicationchrome.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:Userszhouqing1AppDataLocalGoogleChromeApplicationchrome.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),
}
}
runner.Flag
chromedp 截图效果
3.3 chromedp 使用 chrome headless 模式 (不会弹出 GUI 界面)
3.3.1 Centos7(没有图像界面) 安装 chrome
docker pull chromedp/headless-shelldocker run -d -p 9222:9222 --rm --name headless-shell chromedp/headless-shell
官方这安装方法在我的服务器上安装失败.
在服务器 yum 安装 chronium-headless
[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)
[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
[ericzhou@mojotv ~]$ rpm -ql chromium-headless.x86_64
/usr/lib64/chromium-browser/headless_shell
[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’
[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 代码复制就会执行出错. 但是满足大部分需求是没有问题的.