playwright 2026 实操教程
· 1,967 词 · 10 分钟 读完 playwright基础 原创 video
2026年初,浏览器自动化基本上都是AI工具帮我们代劳了。
Selenium 系方案在面对现代网站的指纹检测、行为分析、WAF 时越来越吃力,而 Playwright 凭借原生多内核支持、极致 API 友好度、社区活跃,已经稳坐头把交椅。
今天我们就反其道而行之,从零开始,手写一套针对真实应用的自动化流程。
我们的测试对象是开源自托管笔记服务 Memos(https://github.com/usememos/memos)。
为什么选 Memos 做 demo?
- 到2026年1月,它已经有 56k+ stars,社区非常活跃(最新稳定版 v0.25.3,2025年11月发布)
- 极简、隐私优先、自托管、无遥测、无广告、MIT 开源
- 部署简单(Docker 一行搞定),界面现代、响应式、支持手机
- 有完备的 REST API,但我们这次主要用 Playwright 走浏览器端操作,模拟真人用户行为(后续可对比 API 方式)
- 功能覆盖典型笔记场景:登录、写 memo、加标签、@提及、搜索、公开/私有切换、附件上传等,非常适合拿来练手和展示 Playwright 的各种能力
Memos 核心功能快速过一遍(我们将自动化其中的一些操作)
- 原子化短笔记(memo):每条像数字便签,支持 Markdown(代码、表格、任务列表、链接预览)
- 标签(#work #idea)、@提及、内部链接,方便知识联网
- 可见性控制:公开 / 仅自己 / 保护(密码)
- 搜索 + 标签过滤 + 日历视图
- 附件上传(图片、文件)
- 归档、置顶、删除、恢复
- 暗黑模式 + 移动端友好
- 自托管后数据完全本地掌控(SQLite/MySQL/PostgreSQL)
安装memo
快速自建一个 Memos 测试环境(推荐 Docker,5 分钟内搞定):
docker run -d \
--name memos-demo \
-p 5230:5230 \
-v ~/memos-data:/var/opt/memos \
--restart unless-stopped \
neosmemo/memos:stable
启动后访问 http://localhost:5230,注册第一个账号,就能得到一个干净的实例。我们后续所有自动化代码都针对这个本地 Memos 来写。
安装 Playwright(Python 版,2026年推荐方式)
# 1. 升级 pip(规避很多隐蔽问题)
pip install --upgrade pip
# 2. 安装核心 + pytest 插件(写测试用很方便)
pip install playwright pytest-playwright
# 3. 下载浏览器(约 300–500MB,建议开代理/科学网络)
playwright install
# 省空间只装 chromium:
# playwright install chromium
快速验证安装成功:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("http://localhost:5230") # 改成你的 Memos 地址
print(page.title()) # 应该看到 "Memos" 或类似标题
browser.close()
浏览器弹出并能访问本地 Memos → 环境就绪。
登录与注册
注册和登录用到了几乎相同的元素,所以我们直接用Page Object模式来实现。
界面

html

实现代码
import re
from playwright.sync_api import Playwright, sync_playwright, expect
import time
class AuthPage():
def __init__(self, page):
self.page = page
self.username = page.get_by_role("textbox", name="Username")
self.password = page.get_by_role("textbox", name="Password")
self.sign_up_btn = page.get_by_role("button", name="Sign up")
self.sign_in_btn = page.get_by_role("button", name="Sign in")
def login(self, username, password):
self.page.goto("http://localhost:5230/auth/")
self.username.wait_for(state="visible", timeout=2000)
self.username.fill(username)
self.password.fill(password)
time.sleep(1)
self.sign_in_btn.wait_for(state="visible", timeout=2000)
self.sign_in_btn.click()
time.sleep(1)
def signup(self, username, password):
self.page.goto("http://localhost:5230/auth/signup")
self.username.wait_for(state="visible", timeout=2000)
self.username.fill(username)
self.password.fill(password)
self.sign_up_btn.click()
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
username = "sample"
password = "demo"
auth_page = AuthPage(page)
auth_page.signup(username, password)
# auth_page.login(username, password)
input("Press Enter...")
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
免登录
免登录的原理是在浏览器里设置一个cookie,下面是示例代码,依然是po模式。
def login_with_session(self, session):
print("login....")
self.page.goto("http://localhost:5230/auth", wait_until="domcontentloaded")
time.sleep(1)
self.page.context.add_cookies([
{
"name": "user_session",
"value": session,
"url": "http://localhost:5230" # 必须与页面同源
}
])
self.page.goto("http://localhost:5230", wait_until="domcontentloaded")
调用方式如下。
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
username = "sample"
password = "demo"
auth_page = AuthPage(page)
auth_page.login_with_session('在浏览器的Application -> Cookies里面能找到')
input("Press Enter...")
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
创建memo
界面

代码如下。
def create_memo(page, content: str):
# create memo
page.goto("http://localhost:5230")
page.get_by_role("textbox", name="Any thoughts...").wait_for(state="visible", timeout=2000)
page.get_by_role("textbox", name="Any thoughts...").fill(content)
time.sleep(1)
page.get_by_role("button", name="Save").click()
这里要注意的是我们需要等待输入框出现之后再进行操作。
pin 和 unpin 第1条 memo
界面

代码如下
def pin_and_unpin_memo(page):
time.sleep(1)
# pin
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Pin").click()
# unpin
time.sleep(1)
page.locator(".lucide.lucide-bookmark").first.click()
这里如何定位3个小竖点比较困难,我是用的css selector。
也就是这行代码page.locator('.lucide-ellipsis-vertical').first.click()。
另外弹出的menu也要等待,毕竟代码运行的速度比浏览器动画的速度要快。
增加 reaction
界面

代码如下。
def add_reaction(page, reaction: str):
time.sleep(1)
page.locator("relative-time").first.hover()
page.locator(".lucide.lucide-smile-plus").first.wait_for(state="visible", timeout=2000)
page.locator(".lucide.lucide-smile-plus").first.click()
time.sleep(1)
page.get_by_role("dialog").wait_for(state="visible", timeout=2000)
page.get_by_role("dialog").get_by_text(reaction).click()
使用的时候是这样的add_reaction(page, "❤️"),可以点击任意的表情。
这里的难点是先要hover到一个元素上,然后再等待弹出框。
编辑第1条 memo
界面

代码如下。
def edit_memo(page, content: str):
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Edit").click()
page.get_by_role("textbox", name="Any thoughts...").nth(1).fill(content)
time.sleep(1)
page.get_by_role("button", name="Save").nth(1).click()
page.get_by_role("textbox", name="Any thoughts...").nth(1)这行代码是定位页面上第2个文本框,也就是编辑的那个多行文本框。
删除
界面

删除是会弹出对话框的,要等待这个对话框出现才能继续操作。
def delete_memo(page):
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Delete").click()
page.get_by_role("dialog", name="Are you sure you want to").wait_for(state="visible", timeout=2000)
page.get_by_role("button", name="Delete").click()
上传文件
这里的原理是找到页面上隐藏的input[type="file"]的控件,然后直接设置文件的绝对路径。
上传文件的时候会遇到系统对话框,这个处理起来比较麻烦,所以不要跟界面纠缠,直接操作隐藏控件就好。
page.locator('input[type="file"]').first.set_input_files("./pic.jpg")
page.get_by_role("button", name="Save").click()
完整代码
import re
from playwright.sync_api import Playwright, sync_playwright, expect
import time
class AuthPage():
def __init__(self, page):
self.page = page
self.username = page.get_by_role("textbox", name="Username")
self.password = page.get_by_role("textbox", name="Password")
self.sign_up_btn = page.get_by_role("button", name="Sign up")
self.sign_in_btn = page.get_by_role("button", name="Sign in")
def login(self, username, password):
self.page.goto("http://localhost:5230/auth/")
self.username.wait_for(state="visible", timeout=2000)
self.username.fill(username)
self.password.fill(password)
time.sleep(1)
self.sign_in_btn.wait_for(state="visible", timeout=2000)
self.sign_in_btn.click()
time.sleep(1)
def signup(self, username, password):
self.page.goto("http://localhost:5230/auth/signup")
self.username.wait_for(state="visible", timeout=2000)
self.username.fill(username)
self.password.fill(password)
self.sign_up_btn.click()
def login_with_session(self, session):
print("login....")
self.page.goto("http://localhost:5230/auth", wait_until="domcontentloaded")
time.sleep(1)
self.page.context.add_cookies([
{
"name": "user_session",
"value": session,
"url": "http://localhost:5230" # 必须与页面同源
}
])
self.page.goto("http://localhost:5230", wait_until="domcontentloaded")
def create_memo(page, content: str):
# create memo
page.goto("http://localhost:5230")
page.get_by_role("textbox", name="Any thoughts...").wait_for(state="visible", timeout=2000)
page.get_by_role("textbox", name="Any thoughts...").fill(content)
time.sleep(1)
page.get_by_role("button", name="Save").click()
def pin_and_unpin_memo(page):
time.sleep(1)
# pin
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Pin").click()
# unpin
time.sleep(1)
page.locator(".lucide.lucide-bookmark").first.click()
def add_reaction(page, reaction: str):
time.sleep(1)
page.locator("relative-time").first.hover()
page.locator(".lucide.lucide-smile-plus").first.wait_for(state="visible", timeout=2000)
page.locator(".lucide.lucide-smile-plus").first.click()
time.sleep(1)
page.get_by_role("dialog").wait_for(state="visible", timeout=2000)
page.get_by_role("dialog").get_by_text(reaction).click()
def edit_memo(page, content: str):
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Edit").click()
page.get_by_role("textbox", name="Any thoughts...").nth(1).fill(content)
time.sleep(1)
page.get_by_role("button", name="Save").nth(1).click()
def delete_memo(page):
page.locator('.lucide-ellipsis-vertical').first.click()
page.get_by_role("menu").wait_for(state="visible", timeout=2000)
page.get_by_role("menuitem", name="Delete").click()
page.get_by_role("dialog", name="Are you sure you want to").wait_for(state="visible", timeout=2000)
page.get_by_role("button", name="Delete").click()
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
username = "sample"
password = "demo"
auth_page = AuthPage(page)
# auth_page.signup(username, password)
# auth_page.login(username, password)
auth_page.login_with_session("7-0baac506-9071-4d9b-ac72-158f83081c43")
# create_memo(page, "my sample")
# edit_memo(page, "✍️")
# edit_memo(page)
# add_reaction(page, "❤️")
page.locator('input[type="file"]').first.set_input_files("./pic.jpg")
page.get_by_role("button", name="Save").click()
input("Press Enter...")
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)