You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Selenium与WebSocket的连续序列录制器开发问题

序列录制器Selenium持续录制问题解决方案

背景与核心需求

  • 开发序列录制器:客户端提交URL,服务器端通过Selenium启动浏览器打开该URL,通过WebSocket同步页面PNG到客户端供用户交互,最终生成符合规范的.side格式录制文件
  • 必须满足:页面刷新、跳转(如触发登录表单加载)时,录制不能中断,需持续捕获所有用户操作

目标.side文件格式示例

{  
  "id": "38b09777-d8d1-4e07-a985-5a4a300004bf",
  "version": "2.0",
  "name": "test",
  "url": "http://testphp.vulnweb.com",
  "tests": [
    {
      "id": "3d6ea84f-d64f-4109-92f2-c0c96bafda65",
      "name": "stop",
      "commands": [
        {
          "id": "8f5c8c81-3628-4505-b767-856de504999a",
          "comment": "",
          "command": "open",
          "target": "/",
          "targets": [],
          "value": ""
        },
        {
          "id": "75978eab-8205-4fe1-84a4-e4030f96afe2",
          "comment": "",
          "command": "setWindowSize",
          "target": "1760x1055",
          "targets": [],
          "value": ""
        },
        {
          "id": "2b218310-7214-45b5-addf-6b137ad04b5e",
          "comment": "",
          "command": "click",
          "target": "linkText=Your profile",
          "targets": [
            ["linkText=Your profile", "linkText"],
            ["css=li:nth-child(5) > a", "css:finder"],
            ["xpath=//a[contains(text(),'Your profile')]", "xpath:link"],
            ["xpath=//div[@id='sectionLinks']/ul/li[5]/a", "xpath:idRelative"],
            ["xpath=//a[contains(@href, 'userinfo.php')]", "xpath:href"],
            ["xpath=//li[5]/a", "xpath:position"],
            ["xpath=//a[contains(.,'Your profile')]", "xpath:innerText"]
          ],
          "value": ""
        }
      ]
    }
  ]
}

可行的Playwright实现参考

Playwright通过事件委托绑定到body,能自动捕获动态元素点击,且页面刷新/跳转后可重新注入脚本维持录制:

document.body.addEventListener('click', function (event) {
    const clickedElement = event.target;
    const eventDetails = {
        id: generateUUID(),
        command: 'click',
        target: clickedElement.tagName.toLowerCase() + (clickedElement.id ? '#' + clickedElement.id : ''),
        value: ''
    };
    console.log(eventDetails);
    window.localStorage.setItem('recordedEvents', JSON.stringify(window.localStorage.getItem('recordedEvents') ? JSON.parse(window.localStorage.getItem('recordedEvents')).concat(eventDetails) : [eventDetails]));
});

原Selenium代码的问题

原代码给页面每个元素单独绑定click事件,页面刷新/跳转后,旧DOM元素被销毁,绑定的事件监听也随之失效,导致录制中断:

const clickableElements = await driver.findElements(By.css("body *")); // 选择所有元素
for (const element of clickableElements) {
    const tagName = await element.getTagName();

    await driver.executeScript(`
        const element = arguments[0];
        element.addEventListener('click', function() {
            const eventDetails = {
                id: "${generateUUID()}",
                command: "click",
                target: "${tagName.toLowerCase()}" + (element.id ? "#" + element.id : ""),
                value: ""
            };
            window.localStorage.setItem('recordedEvents', JSON.stringify(JSON.parse(window.localStorage.getItem('recordedEvents') || '[]').concat(eventDetails)));
        });
    `, element);
}

解决步骤与代码实现

1. 改用事件委托捕获所有点击事件

将事件监听绑定到document,利用事件冒泡机制捕获所有元素的点击,包括动态加载的元素:

await driver.executeScript(`
    // 页面内定义UUID生成函数
    window.generateUUID = function() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    };

    // 绑定事件委托到document
    document.addEventListener('click', function(event) {
        const clickedElement = event.target;
        const eventDetails = {
            id: window.generateUUID(),
            command: "click",
            target: clickedElement.tagName.toLowerCase() + (clickedElement.id ? "#" + clickedElement.id : ""),
            value: ""
        };
        // 存储事件到localStorage
        const existingEvents = JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
        existingEvents.push(eventDetails);
        window.localStorage.setItem('recordedEvents', JSON.stringify(existingEvents));
    });
`);

2. 监听页面导航,自动重新注入脚本

页面刷新或跳转后,之前注入的脚本会被清空,通过Selenium监听navigation事件,在每次页面加载完成后重新注入录制脚本:

const { until } = require('selenium-webdriver');

// 注册导航完成监听
driver.on('navigation', async () => {
    await driver.wait(until.documentIsReady());
    // 重新注入事件委托脚本
    await driver.executeScript(`
        window.generateUUID = function() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        };

        document.addEventListener('click', function(event) {
            const clickedElement = event.target;
            const eventDetails = {
                id: window.generateUUID(),
                command: "click",
                target: clickedElement.tagName.toLowerCase() + (clickedElement.id ? "#" + clickedElement.id : ""),
                value: ""
            };
            const existingEvents = JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
            existingEvents.push(eventDetails);
            window.localStorage.setItem('recordedEvents', JSON.stringify(existingEvents));
        });
    `);
});

// 初始页面加载后注入脚本
await driver.wait(until.documentIsReady());
await driver.executeScript(`
    window.generateUUID = function() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    };

    document.addEventListener('click', function(event) {
        const clickedElement = event.target;
        const eventDetails = {
            id: window.generateUUID(),
            command: "click",
            target: clickedElement.tagName.toLowerCase() + (clickedElement.id ? "#" + clickedElement.id : ""),
            value: ""
        };
        const existingEvents = JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
        existingEvents.push(eventDetails);
        window.localStorage.setItem('recordedEvents', JSON.stringify(existingEvents));
    });
`);

3. 扩展录制范围(可选)

若需要捕获输入、表单提交等更多操作,添加对应事件监听:

// 注入扩展事件监听的脚本
await driver.executeScript(`
    // 监听输入事件
    document.addEventListener('input', function(event) {
        const target = event.target;
        if (['input', 'textarea'].includes(target.tagName.toLowerCase())) {
            const eventDetails = {
                id: window.generateUUID(),
                command: "type",
                target: target.tagName.toLowerCase() + (target.id ? "#" + target.id : ""),
                value: target.value
            };
            const existingEvents = JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
            existingEvents.push(eventDetails);
            window.localStorage.setItem('recordedEvents', JSON.stringify(existingEvents));
        }
    });

    // 监听表单提交事件
    document.addEventListener('submit', function(event) {
        const target = event.target;
        if (target.tagName.toLowerCase() === 'form') {
            const eventDetails = {
                id: window.generateUUID(),
                command: "submit",
                target: target.tagName.toLowerCase() + (target.id ? "#" + target.id : ""),
                value: ""
            };
            const existingEvents = JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
            existingEvents.push(eventDetails);
            window.localStorage.setItem('recordedEvents', JSON.stringify(existingEvents));
        }
    });
`);

4. 导出为.side格式文件

录制完成后,从页面localStorage读取事件,整理成.side规范的结构并导出:

const fs = require('fs');

// 服务器端UUID生成函数
function generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

// 获取页面录制的事件
const recordedEvents = await driver.executeScript(`
    return JSON.parse(window.localStorage.getItem('recordedEvents') || '[]');
`);

// 构建.side格式数据
const sideFile = {
    id: generateUUID(),
    version: "2.0",
    name: "recording",
    url: await driver.getCurrentUrl(),
    tests: [
        {
            id: generateUUID(),
            name: "test_session",
            commands: recordedEvents
        }
    ]
};

// 写入文件(树莓派本地存储)
fs.writeFileSync('./recording.side', JSON.stringify(sideFile, null, 2));

树莓派环境注意事项

  • 确保安装适配ARM架构的浏览器驱动(如ChromiumDriver for ARM)
  • 若使用无头模式,需配置浏览器参数支持WebSocket和页面截图功能

内容的提问来源于stack exchange,提问作者Nomadyb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 06:47:02