前言:
Selenium 一直是UI自动化框架的中流砥柱 而webdriver和对应版本的浏览器 也一直是使用Selenium这个库所绕不过去的门槛 本次文章中 我们将通过一个示例演示一下如何使用http请求+ws长链接 在不强制版本和不使用webdriver的情况下进行浏览器元素操作和页面的打开关闭
环境: golang 1.18
前置:
知其然知其所以然 首先我们需要知道 Selenium是如何跟浏览器进行交互的 他的原理是什么 以Python为例
在这里, selenium启动了一个服务 我们首先来看这个服务里做了什么事情
这里执行了一个cmd命令 用于启动一个服务,当服务无法启动或无法连接上时 会抛出一个异常
这里Selenium会通过ws一直去连接一个服务当服务连接上时返回一个成功的标识
那么这个服务是从哪里来的呢? 发送的这些请求有什么用处呢?接下来我们来认识一个协议: CDP(Chrome DevTools Protocol)
CDP(Chrome DevTools Protocol):
chrome devtools protocol允许第三方对基于chrome的web应用程序进行调试、分析等,它基于WebSocket,利用WebSocket建立连接DevTools和浏览器内核的快速数据通道。一句话,有了这个协议就可以自己开发工具获取chrome的数据
那到底怎么进行调试呢? 我们以chrome为例(我使用的是基于chrome内核二开的浏览器本质上与chrome一致):
首先找到浏览器在本地的安装地址:
接下来 我们在当前位置打开cmd命令行工具 执行以下命令:
chrome.exe --remote-debugging-port=9222
我们可以看到 打开了一个新的浏览器并在本地的9222端口上建立了一个浏览器服务
既然有服务 那么肯定就有外放的接口:
http://localhost:9222/json/list
我们能看到 该接口返回了一个数组.并且数组内有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
}
}
效果:
当然市面上已经很多的大佬基于这套协议封装好了框架 功能更加丰富可用 大家感兴趣的话可以在github.com上进行搜索自行查看