前言:

Selenium 一直是UI自动化框架的中流砥柱 而webdriver和对应版本的浏览器 也一直是使用Selenium这个库所绕不过去的门槛 本次文章中 我们将通过一个示例演示一下如何使用http请求+ws长链接 在不强制版本和不使用webdriver的情况下进行浏览器元素操作和页面的打开关闭

环境: golang 1.18

前置:

知其然知其所以然 首先我们需要知道 Selenium是如何跟浏览器进行交互的 他的原理是什么 以Python为例

go语言浏览器驱动 golang webdriver_chrome


在这里, selenium启动了一个服务 我们首先来看这个服务里做了什么事情

go语言浏览器驱动 golang webdriver_json_02


这里执行了一个cmd命令 用于启动一个服务,当服务无法启动或无法连接上时 会抛出一个异常

go语言浏览器驱动 golang webdriver_chrome_03


go语言浏览器驱动 golang webdriver_golang_04


这里Selenium会通过ws一直去连接一个服务当服务连接上时返回一个成功的标识

go语言浏览器驱动 golang webdriver_json_05

go语言浏览器驱动 golang webdriver_go语言浏览器驱动_06

那么这个服务是从哪里来的呢? 发送的这些请求有什么用处呢?接下来我们来认识一个协议: CDP(Chrome DevTools Protocol)

CDP(Chrome DevTools Protocol)

chrome devtools protocol允许第三方对基于chrome的web应用程序进行调试、分析等,它基于WebSocket,利用WebSocket建立连接DevTools和浏览器内核的快速数据通道。一句话,有了这个协议就可以自己开发工具获取chrome的数据

那到底怎么进行调试呢? 我们以chrome为例(我使用的是基于chrome内核二开的浏览器本质上与chrome一致):

首先找到浏览器在本地的安装地址:

go语言浏览器驱动 golang webdriver_chrome_07


接下来 我们在当前位置打开cmd命令行工具 执行以下命令:

chrome.exe --remote-debugging-port=9222

我们可以看到 打开了一个新的浏览器并在本地的9222端口上建立了一个浏览器服务

go语言浏览器驱动 golang webdriver_json_08


既然有服务 那么肯定就有外放的接口:

http://localhost:9222/json/listgo语言浏览器驱动 golang webdriver_golang_09

我们能看到 该接口返回了一个数组.并且数组内有json响应值 接下来我们来解释一下其中内容

devtoolsFrontendUrl:当前页面的地址, 通过该参数加上localhost:9222/ 可以对该tab页进行操作

id: 服务返回的tab页所属id

type: 页面的类型 page为当前页面 iframe为嵌套页面

url: 内部访问的地址

webSocketDebuggerUrl: ws连接的地址 而看到这个地址 相比大家也就明白了Selenium为什么会去创建ws连接了

服务启动了,ws地址有了 那么参数怎么设计呢? 怎么就可以去操作浏览器的元素了呢? 接下来我们来认识一下ws的信令内容 共分为三部分:

id: 发送的指令唯一标识 后续CDP程序会根据该id返回内容

method: 调用的CDP方法

params: 本次请求中需要用到的参数

举个栗子:

{
			ID:     111, // 自己定义
			Method: "Page.navigate", // 官方定义好的 可以通过Chrome DevTools Protocol官方地址查看
			Params: map[string]interface{}{  // 消息体
				"url": "https://xxxxx.xxxxx.com/drelease/#/project/",
			},
		},

这里我们简单列举三个method 而这三个方法也是UI页面操作中最常用的三个:
page.navigate: 访问params参数中的url地址
expression: 执行javaScript方法
Browser.close 关闭浏览器

接下来 我们开始与浏览器进行交互

如果打算个人应用 可以通过os.exec先去将浏览器的服务启动起来, 在通过http请求获取到api接口的响应值 解析出ws的地址 
本次只是为了演示 所以直接从地址中复制出来
import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
	"log"
	"net/url"
	"os"
	"os/signal"
	"time"
)

func Ws() {

	interrupt := make(chan os.Signal, 1) // 创建一个通道 用于监听信号
	signal.Notify(interrupt, os.Interrupt)
	// 拼接url地址
	u := url.URL{Scheme: "ws", Host: "localhost:9222", Path: "/devtools/page/D6E1A14E3A3FB5E1757ABDAC6F692DB1"}
	// 创建一个连接
	c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		log.Fatal("dial:", err)
	}
	defer c.Close()
	// 关闭信号的通道
	done := make(chan struct{})
	// 启动一个后台协程监听服务返回的数据并输出
	go func() {
		defer close(done)
		for {
			_, message, err := c.ReadMessage()
			if err != nil {
				log.Println("read:", err)
				return
			}
			log.Printf("recv: %s", message)
		}
	}()
	// 用一个临时结构体 进行数据拼接用于发送信令
	type DataMap struct {
		ID     int                    `json:"id"`
		Method string                 `json:"method"`
		Params map[string]interface{} `json:"params"`
	}
	dataMap := []DataMap{
		{
			ID:     111,
			Method: "Page.navigate",
			Params: map[string]interface{}{
				"url": "https://XXXXx/drelease/#/project/", // 第一步 打开一个地址
			},
		},
		{
			ID:     112,
			Method: "Runtime.evaluate",
			Params: map[string]interface{}{
				"expression":    "document.getElementById('ui_username').value = ''",// 第二步输入账号
				"returnByValue": true,
			},
		},
		{
			ID:     115,
			Method: "Runtime.evaluate",
			Params: map[string]interface{}{
				"expression":    "document.getElementById('ui_password').value = ''", //第三步输入密码
				"returnByValue": true,
			},
		},
		{
			ID:     117,
			Method: "Runtime.evaluate",
			Params: map[string]interface{}{
				"expression":    "document.getElementById('submit_login').click()", //第四步点击按钮
				"returnByValue": true,
			},
		},
		{
			ID:     119,
			Method: "Browser.close", // 关闭浏览器
			Params: nil,
		},
	}

	for _, v := range dataMap {
		// 遍历出结构体进行数据发送
		// ws协议发送的是二进制 所以通过json进行序列化
		data, _ := json.Marshal(v)
		if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
			fmt.Println("消息发送失败")
			continue
		}
		// 延时5秒后进行下一次数据发送
		fmt.Printf("正在等待5秒结束当前执行的是%#v", v)
		time.Sleep(time.Second * 5)
	}
	// 当操作完成以后 关闭ws通道
	if err = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""));err!=nil {
		log.Println("write close:", err)
		return
	}

}

效果:

go语言浏览器驱动 golang webdriver_golang_10


当然市面上已经很多的大佬基于这套协议封装好了框架 功能更加丰富可用 大家感兴趣的话可以在github.com上进行搜索自行查看