ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ubuntu服务器上Playwright自动化:从环境搭建到无人值守实战

Ubuntu服务器上Playwright自动化:从环境搭建到无人值守实战

早在三年前,我第一次在Ubuntu服务器上跑定时采集任务时,就被浏览器自动化折腾得够呛。那时候还是用老一套的Selenium方案,一个页面等得不耐烦就超时,换元素就报NoSuchElement,更别提系统里装的乱七八糟的浏览器驱动版本对不上这类破事。后面换到Playwright,整个体验完全不一样——安装完直接一条命令把浏览器内核一起拉下来,自带的自动等待机制让脚本稳定得像条老狗,跑了两周都没崩过一次。这也让我下定决心,以后凡是Linux环境下的自动化任务,首选就是它。

这篇内容就是围绕Ubuntu上从零开始搞定Playwright来写的,覆盖环境安装、API核心用法、几个高频实战场景,以及我在实际运行中踩过并且完整排查过的坑。适合谁看呢?一类是刚接触浏览器自动化、想在Linux服务器上跑脚本的新手,另一类是已经写过一些脚本、但对稳定性、并发和无人值守没什么头绪的朋友。看完之后,你至少能独立搭出一套可以长期运行的自动化服务,而不是停留在“能跑通Demo”的阶段。

1. 为什么偏偏是Ubuntu加Playwright

1.1 Ubuntu当自动化宿主机的底气

很多人会纠结:Windows上跑得好好的脚本,干嘛非要搬到Linux上?我的理由很简单,就一个字,稳。

服务器场景下,Ubuntu没有桌面环境拖后腿,资源能全部让给业务进程。同样是跑Playwright脚本,Windows光桌面、DWM进程就吃掉不少内存,在配置不高的云主机上很容易卡顿。而一台干净的Ubuntu 22.04 LTS,默认图形界面都不装,启动完内存占用才几百兆,多出来的资源完全可以分配给浏览器进程和并发任务。

另外就是运维习惯。绝大多数云服务器厂商的默认系统镜像都是Ubuntu Server,包管理用的是apt,生态资料多到随便搜。更关键的是,后期做成systemd服务、用cron做定时任务,这些都算Linux基本功,资料丰富、方案成熟。你在Windows上做同类事情,要么装守护工具,要么挂着任务计划程序,远不如Linux体系来得顺手。

1.2 Playwright和其他浏览器自动化方案的本质区别

先说结论:Playwright最让我放心的,是它把“等待、重试、重连、拦截”这些底层脏活都封装好了,你只需要写业务逻辑。

拿老方案做对比,过去写自动化脚本,最痛苦的是处理各种竞态条件。页面还在加载,脚本就去点按钮,结果元素根本不存在;AJAX数据还在路上,脚本就去读表格内容,读出来是空的。这类问题在动态渲染面前极其普遍,你只能靠time.sleep硬等,或者写一堆显式等待类,代码越写越枯燥。

Playwright则内置了自动等待机制,定位器在触发操作时会自动等待元素可操作。具体来说,一旦你调用了click、fill这类动作,框架会持续轮询直到元素满足“可见、稳定、可操作”这三个条件,超时才报错。这套机制直接消灭了我以前代码里大半的sleep,脚本跑起来不但更快,还更不容易出幺蛾子。

再说驱动管理。Ubuntu系统目录下装过老式驱动的人应该都懂,Chrome升级一个版本,驱动就失灵,然后你满世界找对应版本号。Playwright没有这种烦恼,它使用的是内置的浏览器内核下载机制,工具本身还维护了一份与浏览器版本精确对应的快照。安装的时候执行一行命令,Chromium内核和依赖库就全部到位,升级时也只需要重跑同样的命令,不必手动维护任何驱动文件。

1.3 这台自动化机器能接哪些活儿

把话题拉回到实际场景。基于Ubuntu加Playwright这套组合,我实际跑过的东西大致有这几类:

  • 数据采集:登录后台系统抓取报表、读取页面上下拉加载的列表数据,这类动态内容正好是Playwright的强项。
  • 端到端回归测试:定时把关键业务流程完整走一遍,发现页面报错或者关键元素缺失就告警。
  • 监控巡检:对自建服务的管理后台做健康检查,页面能打开、核心接口返回正常,就认为服务活着。
  • 自动化报表生成:自动化登录、筛选条件、截图、导出Excel,早晨到公司就能看到整理好的数据。

所以,只要你需要在Linux环境里“像人一样操作浏览器”,这套组合基本都能覆盖。

2. Ubuntu环境准备与安装:最容易失控的一步

2.1 Python虚拟环境:别把系统环境搞成垃圾场

Ubuntu自带的Python版本通常足够用,但我不建议直接拿系统的pip装包。原因很简单,apt本身依赖Python,你一个pip install把site-packages弄乱了,轻则依赖冲突,重则系统管理工具瘫痪。老老实实用venv,这个步骤一分钟都不要省。

实际操作如下:

sudo apt update sudo apt install -y python3-venv python3-pip mkdir -p ~/playwright-project cd ~/playwright-project python3 -m venv venv source venv/bin/activate

到这里,你的命令行提示符前面会出现一个(venv),说明已经进入虚拟环境。后面所有安装和运行都在这个环境里,跟系统Python彻底隔离。

2.2 安装Playwright库本体

进入虚拟环境后,安装库本体,一步到位:

pip install --upgrade pip pip install playwright

装完之后验证一下:

playwright --version

能看到版本号就说明库本体装好了。这时候还不能直接跑,因为浏览器内核还没下载。

2.3 浏览器内核与系统依赖:一条命令还是两条命令

这是安装过程里最容易出岔子的地方。Playwright并不使用你系统里已有的浏览器,而是会下载一份它自己匹配的Chromium等内核到用户目录下。这一步跟库本身是分开的,必须单独执行。

playwright install --with-deps chromium

注意我加了这个--with-deps参数。它的作用是让Playwright顺便安装Chromium运行所需的全部系统动态库。很多新手只跑playwright install chromium,结果后面启动浏览器时疯狂报错,缺libnss3、libatk、libcups这些共享库,然后一个一个手动装,非常折磨。

--with-deps这个参数本质上是帮你调用了apt-get install -y来补齐依赖,省去手工处理的痛苦。第一次执行时输出会很长,看到类似Fixing permissions、Installing dependencies这类信息是正常的。安装完成后,内核文件会放到~/.cache/ms-playwright/目录下。

如果你的生产环境不联网,或者装包速度太慢,也可以在有网的机器上装好之后,把整个~/.cache/ms-playwright目录打包带到目标机上解压,最后通过设置PLAYWRIGHT_BROWSERS_PATH环境变量指向自己解压的位置来手动接管。这个思路我后面会细说。

2.4 第一个能跑通的最小脚本

安装完成后,虚拟环境里写一个最小脚本,验证整个链路是否通畅:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() page.goto("https://example.com") print(page.title()) browser.close()

运行命令:

python test.py

乌班图终端能打出一个页面的标题,说明你的环境已经全部就绪,内核能启动、页面能加载。这一步是后续一切玩法的基础,千万别跳过。

3. 核心API使用逻辑:从"能跑"到"会写"

3.1 同步API还是异步API:先看你的场景

Playwright同时提供sync和async两套API。我的经验是,写常规脚本和定时任务,无脑选同步API,简单直接,出了问题也好调试。异步API的好处是可以在一个进程内并发调度多个浏览器操作,但代价是代码复杂度直线上升。

类型适用场景复杂度并发能力
sync_playwright脚本、采集、回归测试低单任务为主,也可多进程
async_playwright高并发IO密集场景高单进程内大量page并存

我个人的节奏是:脚本阶段用sync把业务逻辑跑通,真到了必须上并发的程度,再考虑用多进程跑sync脚本,而不是急着改用async。进程隔离比协程隔离更彻底,一个进程崩了不影响别的任务。

3.2 页面导航与等待:别再写time.sleep了

导航的底层逻辑,是页面加载过程充满了不确定性。老脚本的写法是time.sleep(5)赌它加载完,这种写法在网络上稍有波动就翻车。Playwright里page.goto()本身就可以设置等待条件:

page.goto("https://example.com", wait_until="networkidle", timeout=30000)

wait_until参数常见值有四个,它们的行为差别很大:

  • load:window.load事件触发(默认值),图片、脚本都已经开始加载,但不保证加载完。
  • domcontentloaded:DOM解析完成,速度最快,适合页面本身就慢的场景。
  • networkidle:500毫秒内网络没有新请求,适合数据量大的页面,但不适合轮询式页面。
  • commit:导航请求刚刚被接受,主要用于非常早期的跳转。

我实际用下来,默认load在绝大多数情况下够用。只有遇到那种加载完DOM后还要疯狂拉接口的页面,才会把条件改成networkidle。

等页面加载完之后,操作元素时已经不需要额外等待。page.fill()、page.click()这些动作会自动等待目标出现。如果数据刷新比较慢,用的则是显式等待:

page.wait_for_selector("text=加载完成", timeout=10000) page.wait_for_function("document.querySelectorAll('tr').length > 20", timeout=10000)

这两行用的频率最高。前者等某个元素出现,后者等某个JS条件成立。跟time.sleep的区别在于,页面很快就绪时脚本马上接着跑,不浪费时间;页面迟迟没反应时则精确报错,不会像僵尸一样干等半天。

3.3 定位元素:取色器、标题、结构三招走天下

写自动化脚本,一半的精力都花在解决“怎么找到要操作的元素”上。Playwright的定位器体系,是我用过最顺手的一套。

先记住最常用的三种定位姿势:

  1. 结构定位:page.locator("form input[name='username']"),适合有明确class和id的页面,直接抄控制台的CSS选择器。
  2. 角色定位:page.get_by_role("button", name="登录"),适合按钮、链接这类语义化元素,页面改样式不影响。
  3. 文本定位:page.get_by_text("欢迎回来"),适合动态文案和菜单项。

实际场景里,我通常第一个尝试get_by_role。它依赖的是无障碍语义,网页改CSS样式、加图标,都不影响定位结果。只有遇到那些没有语义的裸DIV时,才会退回到CSS选择器。

联动操作的经典写法如下:

page.get_by_role("textbox", name="用户名").fill("admin") page.get_by_role("textbox", name="密码").fill("123456") page.get_by_role("button", name="登录").click() page.wait_for_selector("text=控制台首页", timeout=15000)

这套写法可读性非常高,哪怕是维护别人写的脚本,一眼也能看明白它在干什么。

3.4 浏览器上下文:多标签与隔离的钥匙

新手最容易忽略的概念是browser_context(浏览器上下文)。它相当于一个独立的会话环境,cookie、localStorage、缓存全部隔离。来自不同context的页面在同一个浏览器进程里互不干扰。

常规的用法是,每个任务都创建一个新的context,任务结束就关闭,绝不复用:

context = browser.new_context() page = context.new_page() # ...任务逻辑... context.close()

这样做的好处是,如果某个任务把登录状态搞坏了,只影响它自己,别的任务依然干净。而且context的创建销毁成本很低,不用重启浏览器进程。后面讲的登录态复用、并发任务隔离,都是建立在这个概念之上的。

4. 典型实战场景的完整代码拆解

4.1 场景一:登录态的保存与复用

很多自动化任务需要登录,但你不可能让每次脚本运行都重新输账号密码,那样又慢又容易被风控拦。Playwright提供了一个非常优雅的解决方案——把登录后的storage_state保存到文件,下次直接加载。

第一次运行时手动登录:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") page.get_by_role("textbox", name="用户名").fill("myuser") page.get_by_role("textbox", name="密码").fill("mypass") page.get_by_role("button", name="登录").click() page.wait_for_selector("text=个人中心", timeout=15000) context.storage_state(path="state.json") browser.close()

之后所有脚本都加载这个状态文件,直接进入已登录状态:

with sync_playwright() as p: browser = p.chromium.launch(headless=True) context = browser.new_context(storage_state="state.json") page = context.new_page() page.goto("https://example.com/dashboard") # 直接操作业务页面

这里的storage_state包含了cookie和localStorage,相当于一个“免登录凭证包”。需要注意的是,它有有效期。如果用了几天后突然发现又跳回登录页了,说明凭证过期,重新跑一遍登录脚本刷一下即可。

4.2 场景二:多标签页与iframe的处理

自动化里最让人头疼的两件事,一是页面跳了新标签页,二是目标藏在iframe里。先说新标签页。

传统写法里,点击链接后弹出一个新窗口,这时候必须切换到新窗口的句柄。Playwright的做法则更直接,使用context.expect_page()或page.expect_popup()来捕获新页面:

with page.expect_popup() as popup_info: page.get_by_role("link", name="去下单").click() new_page = popup_info.value new_page.wait_for_selector("text=下单详情", timeout=15000) # 新页面里随便操作

这个做法的好处是,即使新页面弹出的时间不定,框架也会在弹出来的第一瞬间就拿到新页面对象,不用轮询句柄列表。

再说iframe。iframe本质是嵌套的独立文档,直接在外层页面里定位元素是找不到的。Playwright的解决方案是先用frame_locator进入iframe内部,之后的定位依然延续同一套定位器语法:

frame = page.frame_locator("#payment-iframe") frame.get_by_role("textbox", name="卡号").fill("6222000011112222") frame.get_by_role("button", name="确认支付").click()

注意frame_locator返回的是“定位器工厂”,你可以在它下面继续调用任意定位方法。嵌套iframe时也可以再继续.frame_locator(),逐层深入,逻辑非常清晰。

4.3 场景三:文件下载到指定目录

下载文件是自动化任务里绕不开的需求,不管是导出报表还是抓PDF。Playwright里使用expect_download()上下文管理器捕获下载事件:

with page.expect_download() as download_info: page.get_by_role("button", name="导出Excel").click() download = download_info.value # 等待下载完成 download.path() # 指定保存位置 download.save_as("/data/reports/monthly.xlsx")

这里有个小细节:download.path()返回的是临时目录里的路径,文件下载完成后随时可能被清理。想持久保存,必须立刻调用download.save_as(),不要犹豫。

另外,如果系统下载的是zip包,保存之后立刻用Python的zipfile模块解压,可以省去之后手动处理。我在实际自动化里通常会把下载、重命名、解压、归档写成一个函数,一次搞定。

4.4 场景四:全页面截图与元素截图

截图不只是用来交付成果,更多时候是我用来排障的利器。脚本出错时,一张现场截图往往比日志更直白。Playwright的截图功能很简单:

# 全页面截图,包括滚动区域外的部分 page.screenshot(path="full.png", full_page=True) # 元素截图,只截某个卡片或按钮区域 page.locator("div.product-card").screenshot(path="card.png")

这里我建议把full_page=True设为默认,尤其是页面结构不熟悉的场景。只截可视区域容易漏掉关键信息,一张全页长截图存下来,复盘时什么位置出了问题一目了然。

在headless模式下需要注意,截图不会主动等图片加载。稳妥的做法是截图前先用page.wait_for_selector("img[src]")之类的条件等关键图片出现,或者直接调用page.wait_for_load_state("networkidle"),免得截出来一片灰。

4.5 采集过程中的反自动化痕迹

提到数据采集,很多人会问“会不会被识别成机器人”。我的经验是,与其花大力气伪装指纹,不如老老实实控制行为节奏。频繁、快速、机械性的操作才是最大的暴露点。

实际采集脚本里我会加入随机的人类化操作:

import random import time page.goto("https://example.com/list", wait_until="domcontentloaded") page.wait_for_timeout(random.randint(1500, 3000)) for i in range(1, 31): # 模拟滚动 page.mouse.wheel(0, random.randint(400, 700)) page.wait_for_timeout(random.randint(800, 2000)) # 提取当前可见的数据 items = page.locator("div.item").all_inner_texts() # ...存储逻辑...

这种写法让每次操作的时间间隔都有随机波动,看起来更像真人。但千万别本末倒置,最大的防护还是请求频率本身。采集重要数据前,先评估一下目标站点能承受的合理压力,比任何伪装都重要。

5. Ubuntu上踩过的坑,和完整排查链路

5.1 坑一:goto超时,页面死活加载不完

现象是脚本在page.goto()这一行报了TimeoutError,页面标题都出来了,导航就是不算完。

当时我的排查思路是这样的,先看页面返回的HTTP状态码,再把wait_until改成commit,发现导航请求本身没问题,说明是后续资源阻塞导致load事件迟迟不触发。接着我用浏览器打开DevTools看Network面板,发现页面里有一堆长轮询接口,整个会话根本不会有真正的空闲期。

解决方案:把等待条件从load改为domcontentloaded,页面DOM就绪就立刻开始操作。操作的时候,具体数据再用wait_for_selector或wait_for_function精确等待。这样既避免了卡在导航阶段,又没有放弃对数据的把控。

这类页面上PNG图片、样式表加载慢,也可以考虑用page.route()把没用的静态资源直接abort掉,能大幅提速。规则示例:

def block_resource(route): if route.request.resource_type in ("image", "font"): route.abort() else: route.continue_() page.route("**/*", block_resource)

这个方法特别适合采集页面上有很多横幅广告图的场景,省的流量和时间都很可观。

5.2 坑二:元素定位不到,报Timeout

另一个高频问题,wait_for_selector超时。当时排障时,我先是确认元素确实在页面上,因为切换无头模式用headless=False打开时肉眼能看见。于是怀疑是iframe包裹,通过page.frames遍历确认了目标其实嵌在HTML的iframe里。

接下来的修复就是上面讲过的frame_locator,从iframe内部重新定位。这段经历其实是想说明:碰到定位超时,先不要急着盲目改选择器。按照“是否有iframe、是否是新标签页、元素是否在Shadow DOM容器里、页面是否真的加载完成”四个维度逐项排查,大部分问题都能迅速定位。

5.3 坑三:headless模式下黑屏、白屏、字体异常

无头模式省资源,但它不是一个等同于“没有显示器”的窗口,它实际上是没有GPU加速能力,也没有系统字体库支持的完整环境。

现象是截图出来字体发虚、甚至出现方框乱码。排查下来,是系统里缺少中文字体,装一下就好:

sudo apt install -y fonts-noto-cjk

另外,无头模式下GPU相关的WebGL功能是关闭的,如果目标页面严重依赖WebGL渲染,就需要考虑加启动参数--use-gl=swiftshader,使用软件渲染:

browser = p.chromium.launch( headless=True, args=["--use-gl=swiftshader", "--no-sandbox"] )

这个参数可以解决一部分白屏问题,优先级很高。注意,某些特殊环境下还需要关闭sandbox,比如容器里跑的时候。但--no-sandbox有安全隐患,非必要不加。

5.4 坑四:多任务一跑就内存爆炸

脚本同时起了好几个context,每个context里一堆页面,跑了不到半小时,服务器内存就见底,然后系统OOM把进程直接杀了。

排查时我先用free -h看内存,发现浏览器进程有20多个存活的渲染进程。当时我的写法是每个任务都新建一个context,任务结束后只是把页面关掉了,但没有立刻回收context。修复的关键是结束任务后调用context.close(),并立即清理引用。常规周期任务内存曲线,从持续上涨变成了锯齿状,稳定在可控范围。

还要合理设置浏览器实例数量,不要为每个任务都重新launch()一个浏览器。正确做法是全局共用一个浏览器实例,每个任务单独开context,一个浏览器内最多同时存在的context数据不要太大。

5.5 坑五:浏览器内核更新导致的诡异失败

某个周一早上,所有定时采集任务全线飘红,报错信息里提到浏览器可执行文件启动失败。排查到这一步,想到上周我执行过一次系统升级,顺手把Playwright也更新了。

问题核心就是库和浏览器内核的版本匹配关系。Playwright升级后,会自动要求对应的内核版本,旧内核不兼容。

解决方式有两种:

  1. 升级后立刻重跑playwright install --with-deps chromium,同步内核。
  2. 如果不想升级,可以用pip freeze > requirements.txt把当前版本固定住,每次部署时按版本恢复。

我的建议是,在定时任务脚本里加入版本检查,或者至少固定requirements版本号,不要让环境随着“顺手升级”飘走。定期全量升级是可以的,但升级后必须重新跑安装命令。

5.6 一次完整排查链路复盘

最后复盘一次最典型的报错排查过程,让大家感受一下完整链路是什么样的。

某天脚本一直报Target closed,关键词是目标浏览器上下文被关闭。我的排查顺序是这样的:

第一步,查看任务日志,确认出错在哪个页面、哪一步操作。发现是在一个弹出新标签页的任务里,操作新页面时报错。

第二步,怀疑新页面对象拿错了。检查代码后发现,expect_popup块结束之后,新页面长时间没有被使用,中间跑了一长段其他任务,等到再用时,context已经被前面的代码误关了。这是典型的生命周期管理问题。

第三步,修复策略:把新页面相关的所有操作全部缩进到expect_popup块内,保证它被使用期间context一定存活。改完之后,连续运行三天,没有再出错。

这个复盘想说明的是,Playwright报错信息虽然简洁,但背后往往指向资源生命周期问题。排查时先问自己三个问题:这个对象是哪个context创建的?context还在不在?我是不是提前把资源关了?

6. 从脚本升级成稳定的自动化服务

6.1 无人值守:nohup还是systemd

脚本调试阶段,你可以开着终端等它跑。但一个成熟的自动化任务,要的是无人值守。

我建议用systemd来做常驻服务,它自带守护、崩溃重启、开机自启、日志收集功能,比裸写进程管理靠谱得多。

[Unit] Description=Playwright data collection service After=network.target [Service] User=1xxx WorkingDirectory=/home/1xxx/playwright-project ExecStart=/home/1xxx/playwright-project/venv/bin/python main.py Restart=always RestartSec=10 Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target

把文件写到/etc/systemd/system/playwright-service.service,然后:

sudo systemctl daemon-reload sudo systemctl enable playwright-service.service sudo systemctl start playwright-service.service

日志查看直接用journalctl -u playwright-service -f,实时滚动输出,排障非常方便。

顺带强调的是,unit文件里写的ExecStart必须指向虚拟环境里的python绝对路径,而不是直接用python,否则systemd启动时不一定加载到venv环境。

6.2 定时任务:cron的经典用法

如果你要的是“每天凌晨两点跑一次”,而不是常驻进程,那cron就是最简单的方式:

0 2 * * * cd /home/1xxx/playwright-project && /home/1xxx/playwright-project/venv/bin/python main.py >> /home/1xxx/logs/run.log 2>&1

这里有一个便捷但容易踩坑的地方:cron环境里的PATH非常干净,很多命令找不到。所以脚本里的所有路径都建议用绝对路径,包括venv/bin/python和日志目录。还有一个坑是cron默认使用/bin/sh执行任务,部分语法和bash不同,稳妥起见在cron行首写上:

SHELL=/bin/bash

6.3 日志和重试机制

脚本跑久了,你会发现网络抖动是常态,一次超时不足以证明业务问题。给关键步骤加重试很有必要。

我的做法是封装一个带重试的函数:

import functools def retry(times=3, delay=5): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: if i == times - 1: raise print(f"第{i+1}次失败,{delay}秒后重试: {e}") time.sleep(delay) return wrapper return decorator @retry(times=3, delay=10) def collect_dashboard(): context = browser.new_context() page = context.new_page() page.goto("https://example.com", timeout=30000) # ...业务逻辑... context.close()

每次失败都打印日志,重试次数耗尽后再统一告警。同时,每个任务里也要做好异常兜底,确保context在异常时也能关闭:

try: collect_dashboard() finally: browser.close()

用try/finally或with语法,保证资源不泄漏。这一步看起来基础,但自动化运维里,资源泄漏往往是灾难的开端。

6.4 部署到新机器:一条龙脚本

当你要把这套东西部署到另一台新Ubuntu机器上时,手忙脚乱容易漏步骤。我习惯写一个部署脚本,一条龙走完,省心不容易错:

cat > setup.sh << 'EOF' #!/bin/bash set -e apt update apt install -y python3-venv python3-pip mkdir -p /opt/playwright-service cd /opt/playwright-service python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install playwright playwright install --with-deps chromium echo "部署完成" EOF chmod +x setup.sh ./setup.sh

把应用程序代码放进/opt/playwright-service之后,重复第6.1节的systemd配置,一台新服务器10分钟内就能上线。上线前建议先手动跑一次脚本跑通,再加服务,免得排障时被systemd日志干扰。

7. 个人体感与资源建议

聊了这么多,最后说几句体己话。

Playwright在Ubuntu上跑起来之后,我最大的感受是它把“自动化脚本”从易碎品变成了可维护的工具。你不再需要为一个wait去翻源码、为一个selector去F12半天,框架本身把高级浏览器封装的细节处理得足够优雅,让人把精力放在业务上。

如果你刚开始接触这套组合,不要一上来就追求并发、追求异步性能。先用同步API写一个最简单的采集任务,让它稳定跑一周,再逐渐加登录态、多页面、iframe、下载这些高级功能。稳定压倒一切,跑得久比跑得快重要得多。

在实际运行中还有一个经常被忽略的小技巧:在定时任务和systemd服务里,记得设置好PLAYWRIGHT_BROWSERS_PATH环境变量,指向你实际的浏览器缓存目录。这样即使系统盘空间紧张,也可以把浏览器内核挪到空间更大的数据盘,避免动不动就报磁盘满。我的部署脚本里永远有这样两行:

export PLAYWRIGHT_BROWSERS_PATH=/data/ms-playwright mkdir -p $PLAYWRIGHT_BROWSERS_PATH

最后,浏览器内核升级是个双刃剑,建议每次在升级环境后都跑一次完整的冒烟脚本,确认所有核心场景还都正常,再让它重新进入生产运行。自动化工具本身只是工具,真正让脚本稳定运行的,还是你愿意花时间做的这些保障工作。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进