[ 生活需要仪式感 ]

0%

CentOS 定时截屏并推送企业微信

1 简析

1.1 需求与背景

有这个需求,是由于工作上,有一些监控视图,总是需要 VPN 连接才能浏览,对于非工作时间段,掌握服务状况不友好(艰辛的打工人)。

故有了,定时截图监控视图,并通过推送到企业微信,助力业务。

1.2 无头浏览器(Headless Browser)

无头浏览器指的是没有图形用户界面的浏览器。 无头浏览器在类似于流行网络浏览器的环境中提供对网页的自动控制,但是通过命令行界面或使用网络通信来执行。 (来自维基百科)

简单来说,就是可以使用程序模拟用户进行浏览器操作,能够做到别的测试方法没办法做到的 js、ajax 渲染。

1.3 效果图

数据会被定时截取并推送至企业微信。


2 实现原理

操作/模拟登陆/截图:googleChrome + chromeDriver + chromedp

推送:企业微信 WebHook


3 截图

以下操作基于 CentOS Linux release 8.3.2011 操作。 之前尝试通过 Ubuntu 进行测试,不过依赖包有兼容问题遂放弃。

3.1 安装必要的软件与环境

3.1.1 安装必要的软件

1
2
3
4
# 安装无头浏览器 google-chrome 
$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
$ yum install google-chrome-stable_current_x86_64.rpm
$ google-chrome –version

3.1.2 安装必要的驱动

1
2
3
4
# 安装 chromeDriver 驱动
$ wget https://chromedriver.storage.googleapis.com/2.32/chromedriver_linux64.zip
$ unzip chromedriver_linux64.zip -d /usr/bin/
$ chromedriver –version

3.1.3 安装必要的字体与图像驱动

1
2
3
4
5
6
7
# 安装字体与图像驱动解决乱码(英、中、日)
$ yum install xorg-x11-fonts* -y
$ yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 –y
$ yum install vlgothic-fonts.noarch vlgothic-p-fonts.noarch
$ yum install bitmap-fonts bitmap-fonts-cjk
$ yum install wqy-unibit-fonts.noarch wqy-zenhei-fonts.noarch
$ yum install Xvfb

3.1.4 测试安装正确性

如果可以顺利的截图,且截图中的中文字符显示正确,则表示已经安装了对应的组件了。

1
2
# 如果使用 root 权限,则需要附加 `--no-sandbox`
$ google-chrome-stable --headless --disable-gpu --screenshot https://www.baidu.com

3.2 开发 Go 插件

以下操作基于 Golang 操作,使用 Chrome 官方提供包 github.com/chromedp/chromedp

更多的例子,可以查询官方的示例 https://github.com/chromedp/examples

3.2.1 总览

chromedp 模拟用户操作分为 3 步:

  • 构建一个特殊的自有 ctx 开始
  • 然后构造一个 chromedp.Tasks{} 对象来模拟用户操作。
    • chromedp.Tasks{} 的参数,则是一系列的 []Action,即你希望浏览器模拟的动作(登陆、点击、截屏、等待)等。
  • 最后,完成了 Task 的构造,使用 chromedp.Run(ctx, chromedp.Task{...}) 即完成一次模拟了。

以下,则是一些常用的动作实现与讲解

3.2.2 Init

需要起一个特定的上下文

1
2
3
4
5
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithDebugf(log.Printf),
)
defer cancel()

3.2.3 全屏截屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// screenshot 截屏
// url-地址,name-文件保存名称
func screenshot(ctx context.Context, url, name string) {
// capture screenshot of an element
var buf []byte

// capture entire browser viewport, returning png with quality=90
if err := chromedp.Run(ctx, fullScreenshotHandle(url, 90, &buf)); err != nil {
log.Fatal(err)
}
if err := ioutil.WriteFile(name+"_fullScreenshot.png", buf, 0o644); err != nil {
log.Fatal(err)
}

log.Printf("wrote fullScreenshot.png")
}

// fullScreenshotHandle takes a screenshot of the entire browser viewport.
func fullScreenshotHandle(urlstr string, quality int, res *[]byte) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(urlstr),
chromedp.FullScreenshot(res, quality),
}
}

3.2.4 模拟登陆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func login(ctx context.Context, url, username, password string) {
var res string
err := chromedp.Run(ctx, submit(url, `//input[@name="account"]`, username, password, &res))
if err != nil {
log.Fatal(err)
}
log.Printf("got: `%s`", strings.TrimSpace(res))
}

func submit(urlstr, sel, username, password string, res *string) chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate(urlstr),
chromedp.WaitVisible(sel),

// 用于设定 select option 操作
chromedp.SetValue(`//select[@name="loginType"]`, "2001"),
// 用于设定 input 文本框
chromedp.SendKeys(`//input[@name="account"]`, username),
chromedp.SendKeys(`//input[@name="password"]`, password),
chromedp.Submit(`//input[@name="account"]`),
// 用于设定操作直到某元素不出现(用于判断登陆状态)
chromedp.WaitNotPresent(`//*[@id="remeber"]//label[contains(., '记住帐号')]`)
}
}

3.2.5 点击事件

1
2
3
4
5
6
7
8
9
action :=  chromedp.Tasks{
chromedp.Navigate(url),
chromedp.ActionFunc(func(ctx context.Context) error {...}
}
// 新增点击事件
action = append(action,
chromedp.Click(`#panel-58 .dashboard-row__title`, chromedp.NodeVisible),
chromedp.Click(`#panel-65 .dashboard-row__title`, chromedp.NodeVisible),
)

4 推送

在企业微信中,新建一个群聊,并创建一个机器人,获取对应的 WebHook 地址。

由于机器人,允许发送图片,格式为图片的 base64 + md5(base64)。

4.1 图片转码

1
2
3
4
5
6
7
8
9
10
11
12
//byte2base64 把图片转为 base64 格式并输出对应的 md5 校验码
func byte2base64(ctx context.Context, pngByte []byte) (base64Str, md5Str string) {

base64Str = base64.StdEncoding.EncodeToString(pngByte)
h := md5.New()
h.Write(pngByte)
md5Str = strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
// log.Println(base64Str)
// log.Println("===============")
// log.Println(md5Str)
return
}

4.2 构建一个微信推送结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type WxPush struct {
MsgType string `json:"msgtype,omitempty"`
Image WxPushImage `json:"image,omitempty"`
}

type WxPushImage struct {
Base64 string `json:"base64,omitempty"`
Md5 string `json:"md5,omitempty"`
}

func createWxPushStruct (base64Str, md5Str string) (pushStruct WxPush) {
pushStruct = WxPush{
MsgType: "image",
Image: WxPushImage{
Base64: string(base64Str),
Md5: md5Str,
},
}
return
}

4.3 发起请求

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
// httpPostJson 发起 POST JSON 请求
func httpPostJson(postJson []byte, url string) {
// return
log.Println(string(postJson))

req, err := http.NewRequest("POST", url, bytes.NewBuffer(postJson))
if err != nil {
log.Fatalf("post request err[%s]",err)
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("post do err[%s]",err)
}
defer resp.Body.Close()

statuscode := resp.StatusCode
hea := resp.Header
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
fmt.Println(statuscode)
fmt.Println(hea)
}