在自动化测试或数据采集场景中,我们经常需要同时打开多个浏览器窗口,并且希望每一个窗口里的鼠标移动、点击都是相互独立的,不会彼此干扰。传统的做法是启动多个独立的浏览器进程,各自绑定一个驱动,但这种方式资源消耗大,且进程间调度复杂。现代浏览器自动化工具如Playwright提供了浏览器上下文(browser context)的概念,可以从根本上解决输入隔离的问题。

为什么多个浏览器会共享鼠标状态
早期基于Selenium的脚本通常每个浏览器实例对应一个WebDriver进程,从操作系统层面看,这些浏览器确实是不同的进程。但如果在同一个脚本主线程里交替调用不同driver的action.move_to或action.click,由于代码执行是串行的,很容易出现“上一个浏览器的鼠标还没复位,下一个浏览器又发了新坐标”的错觉。实际上并不是操作系统级共享,而是脚本逻辑没有隔离各个会话的输入队列。
另外,有些工具在旧版本中使用了全局的单例输入调度器,比如通过中间件拦截系统级鼠标事件再分发给不同窗口,这就真的会造成串台。理解这一点很重要:我们要么从架构上让每个浏览器拥有独立的输入上下文,要么在代码层严格隔离调用时序。Playwright选择的是前者,它把输入设备状态绑定在context而不是browser进程上。
Playwright的上下文隔离原理
Playwright中的browser.new_context()会创建一个全新的存储、缓存和输入状态空间。每个context里的page都拥有自己的鼠标、键盘对象,彼此之间没有任何引用关系。当你在context A的page上调用mouse.move(100,100),context B的page完全感知不到这次移动,因为它们的坐标系统和事件循环是分离的。
这种机制底层依赖于浏览器提供的多配置文件(profile)隔离能力,以及CDP(Chrome DevTools Protocol)中独立的输入域。由于不需要启动多个浏览器二进制文件,仅仅是多开上下文,内存占用远低于多进程方案。下面的代码展示了如何并行创建三个独立上下文并各自操作鼠标:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
contexts = []
for i in range(3):
# 每个上下文都是完全独立的浏览器会话
ctx = browser.new_context()
page = ctx.new_page()
page.goto('https://ipipp.com')
contexts.append((ctx, page))
# 各自独立移动鼠标并点击
for idx, (ctx, page) in enumerate(contexts):
# 不同上下文的鼠标坐标互不影响
page.mouse.move(50 * idx, 80)
page.mouse.click(50 * idx, 80)
print(f'context {idx} clicked at independent position')
for ctx, page in contexts:
ctx.close()
browser.close()
使用Selenium实现类似隔离的局限
Selenium官方并没有直接提供“上下文”这样的轻量隔离单元。通常只能用Thread或者asyncio给每个driver包一层,确保调用不交叉。但这只是逻辑隔离,如果用了ActionChains,必须保证每个线程持有自己的实例,否则会抛出状态混乱的异常。
下面是一个用Python线程隔离多个独立浏览器鼠标操作的示例,注意这里启动的是多个浏览器进程,资源开销明显:
import threading
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
def run_browser(index):
# 每个线程启动独立driver,实现物理隔离
driver = webdriver.Chrome()
driver.get('https://ipipp.com')
chain = ActionChains(driver)
# 独立移动与点击,不会与其他线程冲突
chain.move_by_offset(10 * index, 20).click().perform()
driver.quit()
threads = []
for i in range(3):
t = threading.Thread(target=run_browser, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
这种方案的缺点是每开一个浏览器就要占用一份运行时内存,且在CI环境中容易因为资源不足被杀掉。如果只是在单机做轻量并行模拟,Playwright的context方案更合适。
坐标系统与无头模式的注意事项
当使用headless=True时,部分浏览器对鼠标坐标的映射可能和实际有头模式不同,尤其是页面有缩放或设备像素比设置时。建议在关键操作前用page.evaluate读取目标元素的getBoundingClientRect,再换算出准确坐标,而不是写死像素值。
此外,移动端上下文(如iPhone profile)使用的是触摸事件,Playwright里对应的是touchscreen.tap,而不是mouse.click。如果在错误上下文调用了桌面鼠标API,会直接报错。因此初始化context时应明确视口与设备参数:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
# 桌面上下文使用鼠标
desktop_ctx = browser.new_context(viewport={'width':1280,'height':800})
d_page = desktop_ctx.new_page()
d_page.mouse.move(200, 200)
# 移动上下文使用触摸
mobile_ctx = browser.new_context(viewport={'width':375,'height':667}, is_mobile=True)
m_page = mobile_ctx.new_page()
m_page.touchscreen.tap(100, 100)
desktop_ctx.close()
mobile_ctx.close()
browser.close()
总结与实践建议
要在并行运行的多个独立浏览器中模拟独立鼠标操作,核心就是让每个会话拥有自己的输入状态域。Playwright通过browser context以极低开销做到了这一点,适合大多数单机并行场景;Selenium则更依赖多进程或线程隔离,适合已经基于Selenium构建的老项目。
实际编写时,建议封装一个工厂函数,接收上下文参数并返回绑定好独立鼠标的page对象,这样业务代码就不会意外跨上下文调用。同时配合显式等待,避免因为页面未加载完导致鼠标落在错误节点上。只要隔离边界清晰,并行模拟独立操作并不复杂。
SeleniumPlaywrightparallel_browser_automation修改时间:2026-08-10 09:27:39