在开发RPA工作流时,如何获取到网页元素的定位符时很关键的一步,如果能够实现自动生成web元素的选择器,那么将会大大提高工作流的开发效率,本文介绍如何通过浏览器插件的方式实现web元素选择器的自动生成技术。
1. 浏览器插件如何开发
开发浏览器最重要的几个概念
1. manifest.json
这是一个Chrome插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_version
、name
、version
3个是必不可少的,description
和icons
是推荐的。
2. content-scripts
所谓content-scripts,其实就是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还可以包括css的),借助content-scripts
我们可以实现通过配置的方式轻松向指定页面注入JS和CSS(如果需要动态注入,可以参考下文),最常见的比如:广告屏蔽、页面CSS定制,等等。
3. background
后台是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。
background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS
。
4. popup
popup
是点击browser_action
或者page_action
图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。
比较常用的是以上的几个概念。
设计思路
要实现web元素选择器的自动生成,则需要我们的RPA软件能够跟浏览器插件通信,如何与浏览器插件进行通信呢?这里就需要用到浏览器插件的native-messaging(原生消息)能力。 扩展程序可以使用与其他消息传递 API 类似的 API 与原生应用交换消息。支持此功能的原生应用必须注册可与扩展程序进行通信的_原生消息传递主机_。Chrome 会在单独的进程中启动主机,并使用标准输入和标准输出流与其进行通信。
整个通信机制如上图所示,注册原生消息传递主机之后,浏览器在打开时会自动启动原生消息传递主机,即上图中的native host,该程序会启动一个websocket服务,同时通过标准输入输出跟浏览器的background.js通信。
当我们在RPA程序想要拾取web元素时,通过websocket协议向nativehost发送消息,并由nativehost转发给浏览器,告诉浏览器我们要进行元素拾取,浏览器收到消息后,通知当前激活的Tab页,监听鼠标的mouse over 和mouse click事件,当鼠标停在网页元素上时,元素会添加hover效果,告诉用户当前激活的元素,当用户点击某个元素时,content.js分析网页结果,计算出当前点击元素的xpath,通过上面的通信链路返回给RPA程序。
下面为核心代码:
原生消息传递主机配置的文件
{
"allowed_origins": [
"chrome-extension://knldjmfmopnpolahpmmgbagdohdnhkik/"
],
"description": "My Application",
"name": "com.my_company.my_application",
"path": "C:\\Program Files\\My Application\\chrome_native_messaging_host.exe",
"type": "stdio"
}
register.reg 注册表
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Google\Chrome\NativeMessagingHosts\com.my_company.my_application]
@="C:\\path\\to\\nmh-manifest.json"
background.js 跟 native host 通信
var port = chrome.runtime.connectNative('com.my_company.my_application');
port.onMessage.addListener(function (msg) {
console.log('Received' + msg);
});
port.onDisconnect.addListener(function () {
console.log('Disconnected');
});
port.postMessage({text: 'Hello, my_application'});
native host 跟 浏览器通信
func pumpStdout() {
count := 0
for {
buf := make([]byte, 4)
length, err := os.Stdin.Read(buf)
if err == nil && length == 4 {
dataLength := binary.LittleEndian.Uint32(buf)
data := make([]byte, dataLength)
if _, err = os.Stdin.Read(data); err == nil {
log.Logger.Info().Msg("receive from browser:" + string(data))
if wsConn != nil {
if err = wsConn.WriteMessage(1, data); err != nil {
wsConn = nil
}
}
} else {
log.Logger.Error().AnErr("break", err)
break
}
count = 0
} else if length == 0 {
time.Sleep(time.Millisecond * 200)
count += 1
if count > 10 {
log.Logger.Info().Msg("浏览器关闭")
os.Exit(0)
}
}
}
}
func pumpStdin(conn *websocket.Conn) {
for {
if conn != nil {
_, message, err := conn.ReadMessage()
log.Logger.Info().Msg("read from control:" + string(message))
if err != nil {
conn.Close()
break
}
length := len(message)
buf := make([]byte, 4)
binary.LittleEndian.PutUint16(buf, uint16(length))
if _, err := os.Stdout.Write(buf); err != nil {
conn.Close()
break
}
if _, err := os.Stdout.Write(message); err != nil {
conn.Close()
break
}
} else {
break
}
}
}
RPA和native host 通信
func StartPick(ctx context.Context) (string, error) {
if wsConn == nil {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/ws", nil)
if err == nil {
wsConn = conn
} else {
return "", err
}
}
messageId := uuid.New().String()
sendMessage := make(map[string]string)
sendMessage["message"] = "start"
sendMessage["id"] = messageId
request, err := json.Marshal(sendMessage)
if err != nil {
return "", err
}
if err := wsConn.WriteMessage(1, request); err != nil {
wsConn.Close()
wsConn = nil
return "", err
}
runtime.WindowMinimise(ctx)
for {
_, message, err := wsConn.ReadMessage()
if err != nil {
return "", nil
}
resp := make(map[string]interface{})
if err = json.Unmarshal(message, &resp); err != nil {
return "", nil
}
if resp["id"] == messageId {
runtime.WindowMaximise(ctx)
return string(message), nil
}
}
}
func StartCheck(ctx context.Context, frame, selector string) (string, error) {
if wsConn == nil {
conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8080/ws", nil)
if err == nil {
wsConn = conn
} else {
return "", err
}
}
messageId := uuid.New().String()
sendMessage := make(map[string]string)
sendMessage["message"] = "highlight"
sendMessage["frame"] = frame
sendMessage["xpath"] = selector
sendMessage["id"] = messageId
request, err := json.Marshal(sendMessage)
if err != nil {
return "", err
}
if err := wsConn.WriteMessage(1, request); err != nil {
return "", err
}
runtime.WindowMinimise(ctx)
for {
_, message, err := wsConn.ReadMessage()
if err != nil {
return "", nil
}
resp := make(map[string]interface{})
if err = json.Unmarshal(message, &resp); err != nil {
return "", nil
}
if resp["id"] == messageId {
runtime.WindowMaximise(ctx)
return string(message), nil
}
}
}