ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Playwright多浏览器初始化实战:双实例并行与隔离策略

Playwright多浏览器初始化实战:双实例并行与隔离策略 1. 为什么需要同时初始化两个浏览器先从业务场景说起很多人第一次听到“Playwright初始化两个浏览器”这个需求时第一反应是“一个浏览器不够用吗”。说实话我在刚开始接触这套自动化框架时也这么想过。直到我自己在项目里遇到几个非常现实的问题才意识到同时拉起两个浏览器实例不是炫技而是实打实的业务需要。先说一个最常见的场景多账号并行操作。比如你有两个店铺后台、两个公众号、两个不同角色的管理账号需要同时登录操作。如果只开一个浏览器实例第一个登录态的 Cookie 会直接污染第二个账号的操作环境轻则出现“登录状态异常”的报错重则触发站点风控。因为同一个浏览器进程里的上下文默认是共享存储数据的两个账号在同一浏览器里跳来跳去迟早出事。另一个典型场景是数据采集与页面调试并行。采集脚本用无头浏览器在后台抓数据但页面结构变了、选择器失效了这时候你必须开一个带界面的浏览器去手动看页面结构。如果采集脚本和调试页面共用同一个浏览器实例操作上很容易互相干扰——你手动刷新页面采集脚本正好跑到一半直接报错。两个浏览器各自独立一个跑采集任务一个做人工调试互不干扰。还有一类场景来自跨浏览器验证。举个例子你们团队开发了一个新页面后端同学在 Chrome 里调试没问题但需求方用的是 Firefox结果按钮错位、下拉框点不开。你需要同时在 Chromium 和 Firefox 里跑一遍自动化用例而不是先跑完一个再跑另一个因为很多偶发性 BUG 需要双引擎同时执行才能复现。这也是 Playwright 这类框架的核心卖点之一。最后一个场景适合有一定自动化经验的人环境对比测试。生产环境和预发布环境在同一个浏览器里来回切换Cookie、LocalStorage 全部混在一起根本无法判断是环境差异导致的还是缓存污染导致的。初始化两个浏览器一个指向生产环境一个指向预发布环境并排跑对比用例问题定位瞬间清晰。如果你现在正在做自动化测试、爬虫采集脚本或者任何需要浏览器多实例并行的项目同时初始化两个浏览器的思路你应该尽快掌握。这篇文章不绕弯子直接讲清楚原理、给出完整可运行的代码并把我在实际运行中踩过的坑一并交代清楚。2. 动手前必须想清楚的几个概念浏览器实例与浏览器上下文的区别很多初学者一上来就写两个launch结果发现代码能跑但想要隔离的东西没有真正隔离。问题出在没搞懂 Playwright 里 Browser、BrowserContext、Page 三层对象的关系。2.1 browser、context、page 三者的层级关系你可以把Browser理解成一个独立的浏览器进程它启动后占用真实的内存和 CPU就像你在电脑上手动打开了一个 Chrome 程序。BrowserContext是浏览器进程内部的一个“独立会话空间”不同 Context 之间 Cookie、LocalStorage、缓存完全隔离——就像 Chrome 里的“无痕模式窗口”或“不同用户配置文件”。Page则是一个具体的标签页它必须依附在某个 Context 之上。关键在于在一个Browser进程里可以创建多个BrowserContext这些 Context 之间的隔离性已经足够应对绝大多数“两个账号”“两套登录态”的需求。所以在思考“初始化两个浏览器”时你首先要判断我要的是两个真正的浏览器进程还是同一个浏览器进程里的两个 Context判断标准很简单你需不需要为两个任务配置不同的启动参数。比如给其中一个浏览器实例设置独立的数据目录userDataDir、不同的代理、限制某种浏览器内核或者干脆一个是 Chromium 一个是 Firefox那必须初始化两个真正的Browser实例。如果只是登录态、Cookie、缓存需要隔离用同一个浏览器实例下的两个 Context 就够了资源占用小速度也快得多。2.2 两个浏览器的两种正确打开方式用代码表达会更直观。如果你用的是同步 API在一个浏览器进程里创建两个 Context 大概是这样from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context_a browser.new_context() context_b browser.new_context() page_a context_a.new_page() page_b context_b.new_page() page_a.goto(https://example.com) page_b.goto(https://example.com)这样的context_a和context_b在 Cookie 层面是完全独立的。context_a登录的账号不会影响context_b。但如果你需要给两个任务配置不同的代理或者不同的浏览器引擎那就得启动两个真正的浏览器实例from playwright.sync_api import sync_playwright with sync_playwright() as p: browser_1 p.chromium.launch(headlessFalse) browser_2 p.chromium.launch(headlessTrue, proxy{server: http://localhost:8080}) page_1 browser_1.new_page() page_2 browser_2.new_page()说白了大部分情况下“单浏览器多 Context”更划算。只有确实需要进程级隔离时才去启动第二个Browser。2.3 不同代码形态下的初始化差异Playwright 提供了同步和异步两套 API。同步 API 适合普通脚本、爬虫任务写起来直来直去异步 API 适合跑在asyncio事件循环里并发场景更好用。但要注意不要在一个 Python 进程里同时混用两套 API尤其不要在一个同步脚本里尝试async_playwright创建的东西否则event loop is closed这类报错会让人抓狂。还有一个容易踩坑的细节launch方法默认启动的是一个全新的、干净状态的浏览器进程。如果你传了user_data_dir参数它会把指定目录当作浏览器配置目录加载。这种模式不适合并发多实例操作多个进程同时读写同一个user_data_dir会产生锁冲突。要么让每个实例用独立的目录要么干脆不用这个参数。3. 双浏览器初始化的完整实操同步、异步与双引擎混跑概念理清之后直接上可运行的完整代码。我尽量把细节写全方便你复制后能直接跑通。3.1 同步 API 初始化两个 Chromium 实例假设你需要两个浏览器实例分别完成两种不同任务一个负责登录 A 站点后采集商品列表另一个负责登录 B 站点后截图存档。两者启动参数不需要完全一致因此用两个launch是合理的from playwright.sync_api import sync_playwright def task_for_browser_one(): print(browser 1: 开始采集A站商品列表) # 这里就写你自己的页面操作逻辑 def task_for_browser_two(): print(browser 2: 开始对B站页面执行截图) # 这里就写你自己的页面操作逻辑 with sync_playwright() as p: browser_1 p.chromium.launch( headlessTrue, args[--disable-gpu, --no-sandbox] ) browser_2 p.chromium.launch( headlessFalse, args[--start-maximized] ) page_1 browser_1.new_page() page_2 browser_2.new_page() task_for_browser_one() task_for_browser_two() browser_1.close() browser_2.close()这段代码里有几个细节值得注意。--no-sandbox在 Linux 服务器上几乎是必须的否则浏览器启动时经常因为沙箱权限问题报错。--disable-gpu在无头模式下可以减少 GPU 相关报错。而第二个浏览器用headlessFalse加上--start-maximized适合需要人工肉眼确认运行过程的场景。如果你希望两个任务真的“同时”执行而非依次执行就不能像上面这样顺序调用。合理做法是把两个任务分别放进两个线程里每个线程持有自己的 Playwright 入口对象。这里有一个非常重要的坑Playwright 的同步 API 在线程中使用时必须保证每个线程中sync_playwright().start()是独立调用的不能共用同一个入口。简单说你要把启动浏览器的逻辑封装成一个函数然后在两个线程里分别执行from playwright.sync_api import sync_playwright import threading def worker(worker_name: str, task_func): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() print(f{worker_name}: 浏览器已启动) task_func(page) browser.close() def task_a(page): page.goto(https://example.com) print(task A done) def task_b(page): page.goto(https://example.org) print(task B done) t1 threading.Thread(targetworker, args(worker-1, task_a)) t2 threading.Thread(targetworker, args(worker-2, task_b)) t1.start() t2.start() t1.join() t2.join()这种方案的好处是每个线程拥有独立的浏览器进程任务之间完全隔离非常适合把多个猴子任务并行化。代价是内存占用会成倍增长你自己心里要有数。3.2 异步 API 初始化两个浏览器的写法如果你的整个项目已经基于asyncio那就用异步 API。异步方案最大的优势是可以非阻塞地并发管理多个浏览器实例不用手动开线程import asyncio from playwright.async_api import async_playwright async def run_browser_1(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(https://example.com) print(browser 1 标题:, await page.title()) await browser.close() async def run_browser_2(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) page await browser.new_page() await page.goto(https://example.org) print(browser 2 标题:, await page.title()) await browser.close() async def main(): await asyncio.gather(run_browser_1(), run_browser_2()) asyncio.run(main())asyncio.gather会让两个浏览器的启动和页面访问几乎同时发生整体耗时远小于串行执行。在跑大量用例的自动化项目中这个方案能显著压缩总时间。但要记住了异步代码里千万不要混用time.sleep()那会阻塞整个事件循环。统一用await page.wait_for_timeout(1000)或asyncio.sleep()。3.3 双引擎混跑Chromium 与 Firefox 同时初始化“两个浏览器”不一定非得是两种 Chromium 实例。跨浏览器兼容性验证就是典型的双引擎混跑场景。Playwright 官方同时支持 Chromium、Firefox 和 WebKit。如果你的系统里已经执行过npx playwright install firefox可以直接这样做from playwright.sync_api import sync_playwright with sync_playwright() as p: chromium_browser p.chromium.launch(headlessTrue) firefox_browser p.firefox.launch(headlessTrue) chromium_page chromium_browser.new_page() firefox_page firefox_browser.new_page() chromium_page.goto(https://example.com) firefox_page.goto(https://example.com) chromium_title chromium_page.title() firefox_title firefox_page.title() print(Chromium 标题:, chromium_title) print(Firefox 标题:, firefox_title) chromium_browser.close() firefox_browser.close()同样地真正的并发还是要通过线程或异步来实现。双引擎混跑的另一个常见用途是脚本兼容性回归同一个测试脚本在两个浏览器里跑一遍哪个挂掉哪个没挂一目了然。我自己在维护一套内部自动化巡检脚本时就固定用 Chromium 和 Firefox 各跑一遍关键流程整体可靠性提升非常明显。3.4 初始化后的任务分配与隔离验证初始化完两个浏览器怎么确认隔离真的生效一个最简单的验证方法在两个浏览器里分别访问一个能够显示你当前 IP、User-Agent 或已登录账号的页面看输出是否不同。更直接的做法是设置不同的 User-Agent。很多站点会根据 UA 做不同的页面渲染你在浏览器 1 里模拟移动端 UA在浏览器 2 里保持桌面端 UA两个浏览器打开同一个 URL很可能拿到完全不同的响应from playwright.sync_api import sync_playwright with sync_playwright() as p: browser_pc p.chromium.launch(headlessTrue) browser_mobile p.chromium.launch(headlessTrue) mobile_ua Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1 context_pc browser_pc.new_context() context_mobile browser_mobile.new_context(user_agentmobile_ua) page_pc context_pc.new_page() page_mobile context_mobile.new_page() page_pc.goto(https://example.com) page_mobile.goto(https://example.com) browser_pc.close() browser_mobile.close()这种任务分配方式在响应式站点自动化测试和移动端页面专项巡检中非常实用。4. 初始化过程中最容易踩的坑环境安装、超时与资源泄漏能写出上面代码不代表万事大吉。双浏览器初始化的本质是“同时管理多个浏览器进程”而多进程本身就是各种奇怪问题的温床。我把过去一年迭代中踩得最深的几个坑列出来。4.1 npx playwright install 失败的常见原因与处理新手最容易卡死的一步就是安装浏览器内核。pip install playwright只是装了 Python 包真正的浏览器内核还需要单独下载。当你执行npx playwright install或python -m playwright install时脚本会从 Playwright 官方 CDN 下载体积很大的浏览器压缩包。国内网络环境下下载到一半连接断开是常态最终报Hosted at: https://playwright.download.prss.microsoft.com/...这类错误。这不是什么高深问题解决办法就是走国内镜像。设置环境变量后重新执行export PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright/ python -m playwright install chromium如果你只需要 Chromium就别装全家桶install chromium比install整个快得多占用也小得多。另外在 Linux 服务器上装完浏览器之后还经常遇到缺少系统动态库的问题默认浏览器起不来。这种情况直接执行python -m playwright install-deps chromium它会用 apt 帮你补齐所有运行时依赖。这几步做完环境层面基本就通畅了。4.2 双浏览器启动慢、启动超时问题同时启动两个浏览器进程比启动一个慢得多尤其是在服务器配置一般、磁盘 IO 繁忙的情况下。如果你的代码里没有给launch设置超时参数它会使用默认的 30 秒。一旦系统负载高30 秒可能真的不够用然后直接抛TimeoutError。我的习惯是显式把超时调大同时加上slow_mo方便调试browser p.chromium.launch( headlessTrue, timeout60000, slow_mo50 )slow_mo50的意思是每个操作后停留 50 毫秒。它不是为了生产提速而是在调试脚本时让你肉眼看清每一步做了什么事。生产环境建议去掉这个参数。启动过慢的另一个原因是你给每个浏览器实例都创建了很多默认 Context 或者安装了过多扩展。无头模式下能不加载扩展就不要加载每多一个扩展启动时间都肉眼可见地增加。另外如果你用wait_for_selector去等某个页面元素而这个元素本身是异步加载的也要适当提高超时不然两个浏览器同事跑起来后本来 3 秒能加载完的元素硬生生等到超时。4.3 资源释放用完关闭浏览器、上下文与页面这一点在双浏览器场景里重要程度排第一。单个浏览器忘记关闭顶多就是残留一个进程两个浏览器同时忘记关闭内存占用会非常吓人。尤其在跑长任务时如果代码在中途抛异常后续的browser.close()没有执行两个浏览器进程就变成僵尸进程留在系统里。最稳妥的做法是用上下文管理器或者try-finally结构。同步 API 里的with sync_playwright()能保证入口对象关闭但它不会自动关闭你手动launch出来的浏览器实例。要确保关闭你应该这样写from playwright.sync_api import sync_playwright browser_1 None browser_2 None try: with sync_playwright() as p: browser_1 p.chromium.launch(headlessTrue) browser_2 p.chromium.launch(headlessTrue) # 业务逻辑 finally: if browser_1: browser_1.close() if browser_2: browser_2.close()还有一个容易忽略的点context.close()和browser.close()是两回事。只关闭 Context 不会关闭浏览器进程。如果你在一个浏览器实例里创建了多个上下文用完一个关一个最后再统一关闭浏览器这是最优的资源管理方式。4.4 网络与系统层面上的干扰因素如果你在脚本里给两个浏览器配置了不同的代理或不同的网络出口初始化时很容易出现“一个浏览器正常、另一个完全打不开网页”的现象。这种时候先别怀疑 Playwright先确认代理地址是否可达、代理协议是否写对格式。Playwright 的代理配置要求是browser p.chromium.launch( proxy{server: http://ip:port, username: , password: } )格式写错比如漏了http://前缀浏览器启动时不会立刻报错但访问任何网站都会失败。另外如果你在后端任务里设置代理后忘记清理系统级代理环境变量第二个浏览器也会受影响。建议在脚本开始和结束时打印或记录当前的环境变量里与代理相关的内容排查问题会快很多。要注意的是在采集公开数据时遵守目标站点的 robots 声明、控制请求频率是基本功。不要去尝试绕过站点的风控或验证码机制这既不合法也容易把账号和 IP 搭进去。我说的双浏览器方案是为了合理合规地提升效率和隔离数据不是为破解任何限制服务的。5. 进阶玩法把“两个浏览器”变成“多浏览器并发矩阵”既然能从 1 个浏览器扩展到 2 个那就自然能扩展到 3 个、5 个甚至更多。核心思想完全一样只是代码结构需要稍加设计。5.1 循环创建多个浏览器实例的队列模式如果你的任务列表是动态的比如有 10 个账号需要登录每个账号要抓取不同站点你可以用线程池配合独立浏览器启动函数来批量初始化from concurrent.futures import ThreadPoolExecutor from playwright.sync_api import sync_playwright def run_account_task(account_name: str, task_config: dict): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context() page context.new_page() # 登录、采集、断言等逻辑 browser.close() return f{account_name} 执行完毕 tasks [ {account: account_1, url: https://example.com}, {account: account_2, url: https://example.org}, {account: account_3, url: https://example.net}, ] with ThreadPoolExecutor(max_workers3) as executor: results list(executor.map(lambda config: run_account_task(config[account], config), tasks)) for r in results: print(r)这里必须强调max_workers不要盲目设大。每个 worker 对应一个完整的浏览器进程普通服务器内存 4G 的情况下同时跑 5 个 Chromium 实例就很容易把内存打满。合理做法是先尝试 2-3 个 worker观察内存占用后逐步增加。5.2 连接已启动的浏览器connect_over_cdp 的妙用有时候你不需要自己初始化浏览器而是想“接管”一个已经打开的浏览器。Playwright 提供了 CDPChrome DevTools Protocol连接方式。你可以手动启动一个带调试端口的浏览器google-chrome --headless --remote-debugging-port9222 --user-data-dir/tmp/chrome-profile-a然后在脚本里连接它from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.connect_over_cdp(http://localhost:9222) context browser.contexts[0] page context.new_page() page.goto(https://example.com) browser.close()这个方案特别适合以下场景手动登好了某些账号想继续用 Playwright 接管后续操作或者某个浏览器窗口已经打开了大量页面你不想重新初始化一个全新进程把上下文丢掉。不过要注意的是通过connect_over_cdp连接的浏览器页面数量、网络状态都受原始进程约束和全新launch出来的行为会有细微差别。5.3 我的初始化策略经验总结经过一段时间的实操我对“初始化多个浏览器”这件事形成了自己的判断标准。最简单的一句话能用多 Context 就绝不用多 Browser。多 Browser 是在“进程级隔离”和“不同内核混跑”这两个明确需求下才出手的最终方案。初始化顺序上也有一套固定习惯。先检查目标浏览器内核是否安装再清理系统残留的浏览器进程避免脏进程影响本次初始化随后按“轻量配置优先、必需参数才加”的原则启动第一个实例跑通最简单的页面访问最后才叠加第二个实例和完整业务逻辑。这套顺序能帮你把变量降到最少谁出了问题一眼就能锁定。另外一个经常被人忽略的小技巧将浏览器初始化逻辑统一封装成工厂函数。项目跑得越久你会遇到越来越多“既要 Chromium 又要 Firefox”“既要国内网络出口又要国外出口”之类的组合需求。一个统一的初始化函数接收一个配置字典返回准备好了的浏览器实例整个项目的可维护性会高非常多。基于配置驱动的初始化方式也是后续做大规模跨浏览器矩阵测试的基础。
返回列表