1. 为什么我在Ubuntu上选择Playwright
很多做自动化的人应该都有过这种经历:在Windows上调试得好好的脚本,一迁到Linux服务器上就各种起不来,不是缺依赖就是浏览器版本对不上。我之前用Selenium跑某平台的自动化任务,光是维护ChromeDriver和浏览器版本匹配就花了不少时间,后来还遇到无头模式下偶发超时的问题,排查起来非常头疼。直到在某次重构里换成了Playwright,整个人都轻松了不少。
如果你也在Ubuntu上做Web自动化测试、爬虫采集或者RPA流程,这篇文章应该适合你。我会从环境配置开始讲,然后拆解几个实际场景的代码,最后把我踩过的坑和排查思路一并整理出来。这里不搞虚的,全部都是我在真实项目里跑过的方案。
先简单说说为什么推荐Playwright。它和Selenium最大的区别是:Playwright不依赖独立的WebDriver,而是直接通过浏览器厂商提供的调试协议(CDP)和浏览器通信,所以不存在“驱动版本不匹配”这种经典问题。它内置了WebKit、Chromium和Firefox三种内核,而且安装浏览器时会把对应版本一起拉下来,一致性和可控性都高很多。
另外,Playwright的自动等待机制也比Selenium默认的隐式等待和显式等待更智能。它会在执行点击、填表之前自动等待元素可交互,不需要你手动去加一堆sleep。这一点在动态渲染的页面上效果特别明显,脚本稳定性直接上一个台阶。
下面这张表是我个人使用后的直观对比,供你选型时参考:
| 对比项 | Playwright | Selenium | Puppeteer |
|---|---|---|---|
| 浏览器支持 | Chromium、Firefox、WebKit | 全系列,但需驱动 | 仅Chromium |
| 驱动管理 | 内置,无需额外步骤 | 需单独维护Driver | 内置,但生态单一 |
| 自动等待 | 强,自带可操作性检查 | 一般,需自己写等待 | 较弱,需封装 |
| 多标签页/多上下文 | 原生支持 | 较弱 | 支持 |
| 网络拦截与Mock | 内置API丰富 | 需要额外依赖 | 内置但功能少 |
| 语言支持 | Python、Node、Java、.NET | 多语言 | 仅Node |
如果你只有Chromium的需求,Puppeteer也能用,但多浏览器兼容和上下文隔离这两块,Playwright明显更省心。我在Ubuntu服务器上跑定时任务、批量采集,甚至本地起一个可视化调试面板,一套Playwright全都能覆盖。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Ubuntu下Playwright环境配置:从安装到跑通第一个脚本
2.1 准备Python虚拟环境,避免依赖冲突
Ubuntu系统自带的Python环境最好不要直接往里装包,尤其是你同时有其他项目在跑的时候,依赖版本冲突能把人搞疯。我习惯用venv给每个自动化项目单独建一个环境。
bash复制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),后续所有安装和运行都要在这个环境下执行。如果你在服务器上通过systemd或cron跑脚本,记得在启动命令里显式指定venv/bin/python,不然脚本会落到系统Python里,可能导致模块找不到。
2.2 安装Playwright与浏览器内核
虚拟环境激活后,直接用pip安装:
bash复制pip install --upgrade pip
pip install playwright
装完只是Python库,还需要下载浏览器二进制文件。这一步很多人会忽略,或者下载不完整,后面启动浏览器就报错。
bash复制playwright install chromium
playwright install会默认下载Chromium、Firefox和WebKit三个内核,体积比较大。如果只需要Chromium,用playwright install chromium就够了。下载的文件默认放在~/.cache/ms-playwright目录下,不需要root权限,但对磁盘空间有一定要求,建议预留至少1GB。
2.3 补齐系统依赖库
这是Ubuntu上最容易出问题的地方。如果你的服务器是最小化安装,或者缺少图形库相关依赖,启动Chromium时会看到类似这样的报错:
code复制error while loading shared libraries: libnss3.so: cannot open shared object file: No such file or directory
遇到这类报错,最简单的方式是用Playwright自带的依赖安装命令:
bash复制sudo playwright install-deps chromium
这条命令会自动安装Chromium运行所需的所有系统库,包括NSS、GTK、libxcb等。如果你是干净的Ubuntu Server,执行完这个基本就能跑。如果是云主机,可能还需要确认系统架构,比如ARM架构的服务器部分依赖包名称会不同,但install-deps会自动处理。
装完之后,可以先跑一个最小化脚本验证环境:
python复制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()
如果控制台输出Example Domain,说明环境已经通了。我记得第一次在另一台服务器上跑,前几次都会在libnss3上卡住,后来补了依赖就一路顺畅。
3. 核心概念与设计思路:如何让自动化脚本更稳定
3.1 浏览器实例、上下文与页面:三层隔离模型
Playwright里有两个容易混淆的概念:browser(浏览器实例)和context(上下文)。一个浏览器实例可以创建多个上下文,每个上下文之间是完全隔离的,包括Cookie、缓存、存储等。这一点在做多账号并行的时候特别有用。
我通常这样理解:浏览器实例相当于一个独立的浏览器窗口组,而上下文是里面的一次“隐身会话”。在不同上下文里打开同一个网站,互相之间看不到登录态。所以如果要跑多个账号,不需要反复启动浏览器,只要在一个实例里创建多个上下文就行,开销小很多。
python复制with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context1 = browser.new_context()
context2 = browser.new_context()
page1 = context1.new_page()
page2 = context2.new_page()
每个上下文里再创建页面(page),一个页面就相当于一个标签页。日常操作基本都发生在page对象上。
3.2 选择器与自动等待:告别盲目sleep
Playwright的选择器引擎非常强大,支持CSS、XPath、文本内容、角色定位等多种方式。我更推荐能用文本或角色定位就用它们,因为页面结构经常变,但按钮文字一般不会频繁改。
python复制page.click("text=登录")
page.fill("#username", "test_user")
page.click("button:has-text('提交')")
text=和:has-text()都是文本选择器的写法。text=登录是精确匹配,“:has-text”是包含匹配。实际使用中,我会优先用get_by_role来做可访问性定位,比如page.get_by_role("button", name="提交"),这样可以减少对CSS类名的依赖。
Playwright最省心的是自动等待:调用click、fill等操作时,它会自动等待元素出现、可见且可操作。如果超时了就会报超时错误,默认等待时间是30秒,需要修改可以传入timeout参数。
python复制page.click("text=登录", timeout=10000)
这里要注意:虽然自动等待能力强,但并不意味着可以完全不用思考等待。某些页面的跳转是异步任务完成后才触发路由变化的,你可能需要显式等待某个条件出现,这时候就用到expect:
python复制from playwright.sync_api import expect
expect(page.locator(".user-name")).to_be_visible()
这句的意思是:等待页面里出现.user-name这个元素,并且它可见。它是轮询机制,不会固定等待多少秒,也不会提前结束,比sleep高效得多。
3.3 页面操作与事件处理:监听响应、捕获请求
自动化脚本并不是只做“点击”和“输入”,很多时候我们需要知道页面到底发生了什么。Playwright提供了两个常用的钩子:监听网络请求和监听控制台日志。
python复制page.on("console", lambda msg: print("浏览器日志:", msg.text))
page.on("request", lambda req: print("请求:", req.url))
这对排查页面渲染异常特别有用。我在采集脚本里会监听response,如果某次请求返回了非200状态码,就记录下来,便于定位是反爬拦截还是临时故障。
拦截网络请求则可以用来屏蔽不必要的资源,比如图片和字体,提升加载速度:
python复制def block_resource(route):
if route.request.resource_type in ("image", "font", "media"):
route.abort()
else:
route.continue_()
page.route("**/*", block_resource)
这个思路在大规模采集时非常实用,尤其是批量抓列表页的时候,不加载图片能节省大量带宽,速度也能提升30%以上。
4. 自动化实战:从登录到采集一站式流程
4.1 设计一个完整的采集场景
我拿一个模拟的“用户后台系统”来举例,这套流程在生产环境跑过很多次,逻辑基本通用。场景是这样的:有一个内部分析平台,需要先登录,然后进入某个列表页,筛选条件后翻页,把每一页的关键字段保存到本地CSV。同时,为了直观核对,需要给每个页面截图存档。
整个流程拆成四步:登录、进入列表页并设置筛选、翻页采集、数据落地及截图。下面我会逐步把代码写出来,并解释每一步为什么这么写。
4.2 实现自动登录并保持会话
登录页有两个输入框:用户名、密码,一个登录按钮。虽然是模拟,但结构很典型。用Playwright写登录就是先填再点:
python复制from playwright.sync_api import sync_playwright
def login(page, username, password):
page.goto("https://example-internal.com/login")
page.fill("#username", username)
page.fill("#password", password)
page.click("#login-btn")
page.wait_for_load_state("networkidle") # 等待网络空闲
assert page.url.endswith("/dashboard"), "登录失败,未跳转到主页"
wait_for_load_state("networkidle")是我习惯用的一个等待策略,表示等待页面500毫秒内没有新网络请求发生。它比直接等待某个元素更保险,因为登录成功后往往会有多个并发请求。不过这个条件在某些长连接页面上可能会一直等不到,所以也可以换成page.wait_for_selector(".user-info"),等待具体元素出现更稳妥。
登录成功后的Cookie需要保存下来,下次运行脚本就不用重新登录了。Playwright提供了storage_state,可以把上下文的Cookie和LocalStorage保存到文件:
python复制context.storage_state(path="session.json")
下次直接加载:
python复制context = browser.new_context(storage_state="session.json")
这个做法在跑定时任务时非常香。我一般在第一次登录后存一份session.json,后续脚本直接加载,只有在发现接口返回未登录时才重新走一遍登录流程。
4.3 翻页采集与数据落地
列表页的结构是典型的表格加分页器,每页显示20行。我通过表格行数判断当前页数据,然后点击下一页按钮。这里最容易踩的坑是:下一页按钮在某些情况下会变成不可点击状态,比如到了最后一页。Playwright的自动点击虽然会等待元素可操作,但如果你已经点击了“禁用”状态的按钮,它其实并不会报错,但页面也不会翻页,于是可能导致死循环。
我的处理方法是在点击前判断按钮是否被禁用:
python复制while True:
rows = page.locator("table tbody tr")
count = rows.count()
for i in range(count):
cells = rows.nth(i).locator("td")
row_data = {
"id": cells.nth(0).inner_text(),
"name": cells.nth(1).inner_text(),
"status": cells.nth(2).inner_text(),
}
rows_data.append(row_data)
next_btn = page.locator("#pagination .next")
if next_btn.is_disabled():
break
next_btn.click()
page.wait_for_selector("table tbody tr") # 等待新数据渲染
这段代码里,关键点是每翻一页之后,要等待新的表格行渲染完成。如果不等,直接采集就会拿到上一页的数据,造成重复。更严谨的做法是等待URL或者表格内容变化,但用wait_for_selector配合较短超时已经足够。
采集到的数据最后写入CSV:
python复制import csv
with open("output.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["id", "name", "status"])
writer.writeheader()
writer.writerows(rows_data)
4.4 截图与异常监测
截图是排查问题最直观的手段。在无人值守的服务器上,如果脚本运行出错,没有截图几乎没法定位问题。我习惯在关键节点都留一张截图:
python复制page.screenshot(path=f"page-{page_num}.png", full_page=True)
full_page=True可以截取整个滚动页面,对于查看列表底部信息很有用。另外,在异常捕获时,我也会把当时页面的HTML保存下来,方便后续分析:
python复制try:
page.click("#submit")
except Exception as e:
with open("error_page.html", "w", encoding="utf-8") as f:
f.write(page.content())
page.screenshot(path="error_screenshot.png")
raise
这种“截图加HTML”的双保险,帮我定位过好多次动态渲染错误。
另外,如果脚本需要长时间运行,我建议定时发个心跳,比如每完成一个页面采集就写一行日志。我通常是直接打印到控制台,然后配合systemd的journalctl查看。如果哪天早上发现任务没跑完,一看日志就知道卡在哪一页。
5. Ubuntu上常见问题与排查技巧实录
5.1 浏览器启动失败:依赖缺失、沙箱限制和内存不足
Linux服务器上最常见的一类问题就是启动失败。除了前面提到的依赖库缺失,还有两个问题我需要特别提醒。
一个是Chromium在root用户下默认不允许启动,因为它需要沙箱功能,而容器环境里往往没有用户命名空间。解决办法是给启动参数加--no-sandbox,但这样会降低安全性,只建议在可控环境里使用。
python复制browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
另一个是服务器内存不足。无头Chromium每个实例大概占用300MB内存,如果你同时开多个上下文,很容易触发OOM。建议在启动参数里限制内存使用,或者用--disable-dev-shm-usage来避免/dev/shm空间不足的问题。这个参数在Docker容器里几乎是必加的。
5.2 元素定位超时与动态加载:如何精准等待
我遇到最多的就是元素定位不到,或者点击时报超时。通常原因有三个:页面元素在iframe里、元素被遮挡、或者数据是懒加载的。
如果是iframe内元素,需要先拿到frame对象:
python复制frame = page.frame_locator("#iframe-id")
frame.locator("button").click()
如果元素被遮挡,比如有弹窗盖住按钮,Playwright会一直等待直到超时。这时候可以先用page.locator(".modal-close").click()关掉弹窗,再点击目标。如果弹窗是延迟出现的,建议先监听并处理弹窗,而不是盲目等待。
懒加载场景则要搭配滚动:
python复制page.mouse.wheel(0, 3000)
page.wait_for_timeout(500)
但wait_for_timeout尽量少用,更好的做法是轮询某个条件成立。
5.3 并发任务与资源泄露:控制好浏览器实例数量
在Ubuntu服务器上跑定时采集,我一开始图省事,直接开了10个浏览器实例,结果内存直接爆掉。后来改成单实例多上下文,还是会有连接超时的问题。最终采用的方式是控制并发数量,比如用信号量限制同时执行的任务数为4:
python复制import asyncio
from playwright.async_api import async_playwright
semaphore = asyncio.Semaphore(4)
async def run_task(url):
async with semaphore:
async with async_playwright() as p:
browser = await p.chromium.launch(...)
# ...
另外,用完的page要关闭,context也要关闭,不然连接一直挂着。这个别指望垃圾回收,Playwright的浏览器进程是独立进程,不显式关闭就会一直占用资源。
5.4 无头模式与有头模式的差异
有头模式能看到页面实际渲染,但服务器上没有显示器,一般只能用xvfb-run模拟显示,麻烦而且慢。无头模式对绝大多数采集场景都够用,但有个差异要注意:部分网站会检测无头浏览器特征,然后弹出验证码或者返回空数据。
如果在无头模式下采集不到数据,可以先尝试在有头模式下调试一次,确认是检测问题还是脚本问题。另外,可以用Playwright提供的channel参数指定使用系统安装的Chrome,而不是自带的Chromium,这样指纹更接近普通用户:
python复制browser = p.chromium.launch(channel="chrome", headless=True)
当然,这要求你先在系统里安装Chrome。在Ubuntu上装Chrome也很简单,下载deb包后sudo dpkg -i安装即可。
6. 我的一些经验总结和实用建议
6.1 用Playwright的Codegen快速生成脚本
如果你刚开始写Playwright脚本,抓不准选择器,可以直接用它的代码生成器。在项目环境中运行:
bash复制playwright codegen https://example-internal.com/login
它会打开一个可视化浏览器窗口,你手动操作页面,所有动作会自动生成代码。代码生成器支持Python、Node等多种语言,生成后复制到项目中微调即可。
我经常用它来快速探一个陌生页面的结构,省掉反复看HTML的时间。不过要注意,生成的代码通常比较冗余,建议理清楚逻辑后精简一遍,重点优化等待和异常处理。
6.2 日志和异常的规范化设计
自动化脚本跑在无人值守的服务器上,日志设计直接决定你排障的效率。我现在的做法是:每个关键步骤都打印时间戳和动作描述,异常时打印完整的堆栈和当前URL,同时截图存档。
python复制import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("auto_task")
logger.info("开始登录 %s", url)
这样配合systemd或者cron的系统日志,基本能做到出问题五分钟内定位到根因。
6.3 把重复的登录态与页面对象封装成模块
运行一段时间后,你会发现很多代码是重复的,比如初始化浏览器、加载登录态、打开页面等。我建议把这些抽成独立函数或类,方便复用。
一个简单的初始化模块可以这样写:
python复制from playwright.sync_api import sync_playwright
def get_context(storage_state=None):
pw = sync_playwright().start()
browser = pw.chromium.launch(
headless=True,
args=["--no-sandbox", "--disable-dev-shm-usage"]
)
context = browser.new_context(storage_state=storage_state)
return pw, browser, context
这样每个任务脚本只需调用get_context,然后在页面逻辑里专注写业务,维护起来轻松很多。我甚至封装了一个playwright_runner类,统一管理启动、关闭、截图和日志,后续新增采集任务基本只写核心的逻辑部分。
6.4 关于浏览器更新与版本锁定的提醒
Playwright升级比较频繁,新的版本可能会强制要求匹配的浏览器版本。如果你在跑生产任务,不建议直接在线上随便升级Playwright库。正确做法是:升级前先在一台测试机上跑一遍全量回归,确认没有问题后,再更新线上环境的库和浏览器。
我习惯把Playwright版本和浏览器版本记录在项目说明里,比如“Playwright 1.40.0 + Chromium 118”。遇到问题的时候,这个信息能帮你快速判断是否是版本升级导致的环境差异。
另一个实践是,在CI或者定时任务里固定使用同一个虚拟环境和已下载的浏览器缓存,避免每次部署都重新下载浏览器。可以把~/.cache/ms-playwright目录打包进Docker镜像,这样新环境启动时不用再执行playwright install,能省不少时间。
最后想说的是,Playwright在Ubuntu上的体验,整体是挺顺滑的,但不要指望装上就能一路顺畅。依赖库、沙箱、资源管控这些问题都是Linux环境下的老面孔,理解它们背后的原因比记住某条命令更有用。上面这些内容,都是我在一次次凌晨排查任务失败的过程中攒下来的,希望能让你少走一点弯路。
