news 2026/9/30 1:16:28

Playwright+Pytest实战:打造稳定高效的Web UI自动化测试方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright+Pytest实战:打造稳定高效的Web UI自动化测试方案

大约一年前,我接手了团队的 Web UI 自动化测试资产。300 多个 Selenium 用例,全量跑一遍要 45 分钟,失败率稳定在 35% 左右,光看失败日志就能耗掉半天。团队已经不止一次讨论过要不要把整套东西删掉重写。

我那时候没有急着表态,而是花了三天时间做了一件事:把所有用例按"是否真正在回归业务价值"过了一遍筛子。结果发现真正有价值的用例不到 40%,其余的都卡在定位不稳定、等待时间乱写、浏览器窗口一换就挂这类问题上。这些问题不是 Selenium 独有的,但 Selenium 确实给不了像样的解药。也就是在那段时间,我把 Playwright 拉进了技术选型的对比池,配合 Pytest 的测试组织能力,搭出了一套让团队愿意维护的 UI 自动化方案。

这篇文章就围绕 Playwright + Pytest 这组黄金搭档展开,我把从环境搭建、核心 API、Fixture 管理、真实业务场景到 CI 集成的完整路径都记录下来,包括我踩过的一些坑。如果你正在为 Web UI 自动化测试的选型和落地发愁,这篇文章应该能帮你省掉不少弯路。

1. 为什么是 Playwright + Pytest:这次选型背后的实际考量

1.1 与 Selenium、Cypress 的横向对比,谁更适合 UI 自动化

先声明一点,我没有任何贬低 Selenium 的意思。它的生态成熟度、社区规模、语言支持广度都是其他工具短期追不上的。但 Selenium 的定位是一个 WebDriver 协议的标准实现,它把底层的浏览器控制能力暴露给上层,至于怎么等待元素、怎么重试操作、怎么排查失败,它不管。

这意味着你用 Selenium 写用例,必须自己解决两个高频问题:一是元素定位的实时有效性,二是等待策略的合理性。大多数团队的做法是到处塞time.sleep或者WebDriverWait,一旦页面响应变慢或者接口超时,脚本就变得极度脆弱。

Cypress 是另一条技术路线。它直接跑在浏览器内,安装简单,调试体验也不错。但 Cypress 有两点限制让我在项目里推进不下去:第一,它只支持 JavaScript/TypeScript,这对我们这种以 Python 技术栈为主的测试团队不友好;第二,多标签页、跨域域名切换这类场景处理起来很别扭。

Playwright 刚好解决了上面两类痛点。它由微软团队维护,底层用的是 CDP(Chrome DevTools Protocol),可以同时驱动 Chromium、Firefox 和 WebKit。定位元素时,系统会自动等待元素可操作,不需要写显式的等待逻辑。它还内置了 trace 录制、视频回放、网络请求监听、移动端模拟等能力,这些在传统方案里都要靠额外引入依赖才能实现。

下面这张表是我当时做选型对比时整理的,至今仍觉得可以直接拿来当参考:

维度SeleniumCypressPlaywright
语言支持Python、Java、JS、C# 等仅 JavaScript/TypeScriptPython、Java、JS、.NET 等
等待机制需要手写显式等待自动等待,但仅限应用内内置自动等待,覆盖面广
跨域/多标签页支持,但代码繁琐支持有限原生支持,API 简洁
失败排查能力截图 + 日志,需另配工具视频 + 截图,浏览器内调试截图 + 视频 + Trace,最完整
执行模型按 WebDriver 协议交互浏览器内直接执行CDP 协议直连
学习曲线中,但坑多低中低,API 设计现代

1.2 Pytest 在自动化测试生态里的独特地位

在 Python 测试生态里,Pytest 几乎是事实标准,不只是因为它的断言写法简洁,更因为它提供了三个 UI 自动化必需的扩展点:Fixture、参数化、插件体系。

Fixture 可以让我们把浏览器实例、用户登录态、测试数据的准备和回收逻辑做成可复用的装置,而不是每个用例都自己初始化一遍。参数化则让同一套用例跑在不同浏览器、不同用户角色、不同输入数据上,代码不膨胀。这两个特性结合起来,UI 自动化的维护成本会断崖式下降。

更关键的是,业界已经有现成的pytest-playwright插件,它把 Playwright 的page、browser、context等核心对象直接暴露为 Pytest 的原生 Fixture。你在用例函数里写一个参数page,它就自动给你一个干净的页面实例,测试结束自动关闭。这种体验,早期 Selenium + pytest 时代需要写大量样板代码才能实现。

所以结论很简单:Playwright 负责解决"怎么稳定地操作浏览器"的问题,Pytest 负责解决"怎么组织、运行、报告这些操作"的问题。两者不是替代关系,而是互补关系。这也是我这篇文章想强调的核心思路——框架是载体,测试设计才是灵魂,但载体选对了,灵魂才能落地。

2. 环境搭建与工程骨架:把第一个用例跑起来

2.1 安装与版本锁定

安装过程这里不再赘述基本的 pip install 流程,但有几个细节值得特别注意。

第一步是创建独立的虚拟环境。千万别图省事直接往系统 Python 里装 Playwright,因为 Playwright 的版本迭代速度较快,不同项目可能需要锁定不同版本,共用一个环境迟早出问题。

python -m venv venv source venv/bin/activate pip install playwright pytest-playwright

第二步是安装浏览器内核。pip install playwright只装了 Python 包,浏览器内核需要额外下载:

playwright install chromium

如果你要测的是全浏览器矩阵,就执行playwright install --with-deps,它会一次性装好 Chromium、Firefox 和 WebKit,并且自动安装系统依赖。建议在 Linux CI 环境里用--with-deps,省得一个个补系统库。

第三步也是我吃过亏之后养成的习惯:把版本锁定到requirements.txt里。

playwright==1.44.0 pytest-playwright==0.4.3 pytest==8.2.0

为什么强调版本锁定?因为 Playwright 每个版本都会更新浏览器驱动协议,如果团队成员有人升了版有人没升,就会出现"本地全过、CI 上全是奇奇怪怪的报错"的情况。

2.2 基础用例结构:从打开页面到第一个断言

装好环境之后,创建一个最简单的用例文件,验证整体链路是否通畅:

# test_demo.py from playwright.sync_api import expect def test_page_title(page): page.goto("https://example.com") expect(page).to_have_title("Example Domain")

在终端执行pytest test_demo.py。如果环境正常,你会看到用例通过。

这里要解释一下page这个参数是哪儿来的。它来自pytest-playwright插件。当你安装了插件后,它自动注册了page、context、browser这些 fixture。测试用例只要声明需要page,插件就会创建一个全新的浏览器上下文,测试结束自动关闭。这样每个用例天然隔离,不同用例之间不会因为浏览器缓存或登录态而互相干扰。

推荐的工程目录结构可以这样组织:

auto_test/ ├── conftest.py # 存放全局 fixture,配置 ├── requirements.txt ├── pages/ # 页面对象层 │ ├── login_page.py │ └── home_page.py ├── testcases/ # 测试用例层 │ ├── test_login.py │ └── test_order.py ├── data/ # 测试数据 │ └── users.json └── reports/ # 报告输出目录

页面对象模式(Page Object Model)在 UI 自动化里的重要性怎么强调都不为过。简单说就是把一个页面的定位器、操作封装成一个类,用例只关心业务动作,不关心具体 CSS 选择器。这样的好处是,前端一改 DOM 结构,你只需要改pages/目录下的一个类,而不是满仓库地找选择器。

3. 核心 API 实战:定位、等待、断言与追踪

3.1 locator 定位体系,比"选择器"更进一步

很多从 Selenium 转过来的人,会习惯性地找find_element_by_id/find_element_by_xpath这类方法。Playwright 的做法不太一样,它把定位能力集中在locator上:

from playwright.sync_api import expect page.goto("https://example.com/login") # 文本定位 page.locator("text=", "登录").click() # CSS 定位 + 子元素过滤 page.locator(".form-item").filter(has_text="用户名").locator("input").fill("admin") # ARIA role 定位,模拟辅助功能读屏器的视角 page.get_by_role("button", name="提交").click() # 正则匹配,适合动态文本 page.get_by_text(re.compile(r"订单号.*?已生成")).is_visible()

get_by_role是我现在最推荐的方式。它以可访问性语义定位元素,比如"名称为提交的按钮",而不是"class 是 btn-submit-red 的元素"。前端样式频繁变动的情况下,这种定位方式的稳定性要远高于 CSS 或 XPath。

如果确实需要 XPath,Playwright 也支持,但我的建议是把 XPath 当作最后手段。XPath 在维护性上很差,大部分复杂的 XPath 表达式只有写的那个人能看懂。

3.2 自动等待机制:不再写 time.sleep

Playwright 最大的卖点之一就是自动等待。当你调用click()、fill()这些操作时,Playwright 会持续检查元素是否可操作,默认超时时间是 30 秒,只有超过超时时间仍未满足条件才会报错。

但这种自动等待不等于"什么都不用管"。有几种场景你仍然需要显式等待:

第一,页面跳转或路由变化时。点击登录按钮后,页面可能先去请求接口,再跳转新页面。这时候可以用page.wait_for_load_state()等待某个特定的事件。

page.get_by_role("button", name="登录").click() page.wait_for_load_state("networkidle")

需要注意,networkidle在指标较重的页面(持续轮询、长连接)上会一直等。用它时要谨慎,更推荐用wait_for_url或断言关键元素。

第二,元素已经存在但数据尚未填充到位。比如表格已经渲染了行,但行内的某个字段还在异步加载。这时候最好用expect做轮询式断言:

expect(page.get_by_text("加载完成")).to_be_visible(timeout=10000)

这个expect不是普通断言,它会持续轮询,等条件满足或超时,比手动写for循环重试要优雅得多。

3.3 断言库、截图、视频与 Trace:把失败现场"留影"

Playwright 同源的expect断言库替代了 pytest 里的assert一部分场景。如果你只是判断文本是否出现,直接expect就好。但如果你要做数据级的断言,比如比较接口返回值,那还是用 Python 原生assert更顺手。

在失败排查层面,我几乎把所有项目都默认配置成"失败截图 + 视频 + Trace"三件套。这样一来,用例失败后,我不但能看到最后的界面长什么样,还能回放浏览器整个操作过程。

pytest-playwright提供现成的配置项:

# pytest.ini [pytest] addopts = --trace on --video on --screenshot only-on-failure

Trace 文件生成后,可以用playwright show-trace trace.zip在本地打开回看。这是一个 HTML 文件,会展示每个操作时的页面快照、网络请求、控制台日志,排查问题像看监控回放一样直接。

4. 用 Pytest Fixture 管理浏览器和测试数据

4.1 fixture 作用域的选择:复用浏览器还是复用上下文?

在pytest-playwright里,fixture 有四个关键层级:browser、context、page和request。browser是整个测试进程共用的浏览器实例,context是浏览器内的一个独立上下文(类似隐身窗口),page是上下文中的一个标签页。

我推荐绝大多数情况下用默认的function级 fixture,也就是每个用例都从新的page开始。因为 UI 自动化最怕用例间产生状态耦合,比如上一个用例登录了,下一个用例默认就有登录态。这种耦合在最开始会跑得很爽,一旦失败,排查问题的成本会翻倍。

如果你有部分用例确实需要复用登录态来节省时间,可以自己写一个 fixture,把登录操作放在context级:

import pytest @pytest.fixture(scope="session") def browser_context(browser): context = browser.new_context() # 这里可以注入 localStorage、cookie 或走一次 UI 登录 page = context.new_page() page.goto("https://example.com/login") page.get_by_role("textbox", name="用户名").fill("test_user") page.get_by_role("textbox", name="密码").fill("123456") page.get_by_role("button", name="登录").click() page.wait_for_url("https://example.com/dashboard") page.close() yield context context.close()

这里的核心思路是:把耗时的登录操作提前,让后面的用例从已认证的上下文里直接new_page(),从而大幅缩短用例执行时间。

4.2 参数化与数据驱动:一套代码跑多套场景

Pytest 的@pytest.mark.parametrize在 UI 测试里有两个非常自然的用法。

用法一是按浏览器维度跑矩阵测试。你可以在配置里指定多个浏览器,或者通过参数组合手动控制:

import pytest from playwright.sync_api import expect @pytest.mark.parametrize("browser_name", ["chromium", "firefox"]) def test_login_with_different_browsers(browser_name, browser): pass

用法二是按测试数据维度跑用户场景。比如登录场景的异常输入集:

import pytest @pytest.mark.parametrize( "username,password,expected_message", [ ("", "123456", "用户名不能为空"), ("admin", "", "密码不能为空"), ("wrong", "wrongpass", "用户名或密码错误"), ], ) def test_login_validation(page, username, password, expected_message): page.goto("https://example.com/login") page.get_by_role("textbox", name="用户名").fill(username) page.get_by_role("textbox", name="密码").fill(password) page.get_by_role("button", name="登录").click() expect(page.get_by_text(expected_message)).to_be_visible()

数据驱动到这个程度后,增删一条测试用例,就是增删参数列表里的一行元组,完全不用复制粘贴代码。这也是 UI 用例数量变多时仍能保持低维护成本的关键操作。

4.3 失败重跑与用例级截图

UI 自动化最难防的就是偶发失败——网络抖动、某个接口延迟,哪怕定位器再稳也会翻车。Pytest 生态里有一个成熟插件pytest-rerunfailures:

pip install pytest-rerunfailures

配置:

addopts = --reruns 2 --reruns-delay 1

意思是失败后重新尝试 2 次,每次间隔 1 秒。重跑适合用来过滤偶发问题,但我得提醒一句:重跑使用要克制。如果一个用例连续重跑仍失败,说明它是真失败,要把注意力放在修复上,而不是用重跑掩盖问题。

5. 真实业务场景中的三个硬骨头

5.1 动态 iframe 内容处理

很多后台系统还在用旧式 iframe 嵌套架构,登录后切换到主模块,往往要先切换到对应 frame。以前用 Selenium 写switch_to.frame会写出一串索引切换,极难维护。

Playwright 针对 iframe 提供了frame_locator,逻辑上更接近"在某个 frame 内找元素"的自然描述:

import pytest from playwright.sync_api import expect def test_iframe_content(page): page.goto("https://example.com/legacy_admin") # 直接基于 iframe 元素定位,不依赖索引 iframe = page.frame_locator("#main_frame") iframe.get_by_role("textbox", name="搜索").fill("订单") iframe.get_by_role("button", name="查询").click() expect(iframe.get_by_text("订单列表")).to_be_visible()

frame_locator可以嵌套使用,如果 iframe 里还套 iframe,可以继续调用frame_locator一路点到目标层级。这套 API 设计最大的价值是,开发人员的 DOM 结构调整时,你只需要关注框架元素的 id/name,不用去跟踪索引变化。

5.2 监听页面请求与接口级断言

UI 自动化测试最容易忽略的一环是:界面操作成功不等于后端接口返回了正确数据。界面上一个小弹窗很容易掩盖接口报错,或者页面渲染了数据,但接口实际多查了一遍。Playwright 原生支持路由监听,让 UI 测试也能做接口断言。

def test_order_list_request(page): page.goto("https://example.com/orders") with page.expect_response("**/api/orders?*") as response_info: page.get_by_role("button", name="刷新").click() response = response_info.value assert response.status == 200 # 做更细粒度的响应体校验 data = response.json() assert data.get("code") == 0 assert len(data.get("list", [])) > 0

这种"UI 触发 + 接口校验"的组合,能让我们在功能测试阶段就感知到前后端联调的潜在问题。特别是在遇到列表页"数据加载完了但展示为空"这类 bug 时,接口断言能快速划分责任:到底是前端渲染问题,还是后端返回就有问题。

5.3 弹窗、下载与多标签页处理

弹窗在自动化里曾经是个老大难,alert、confirm 需要单独处理。Playwright 的做法是用 dialog 事件回调:

page.on("dialog", lambda dialog: dialog.accept()) page.get_by_role("button", name="删除").click()

多标签页则用 context 的等待事件,点击打开新页面的按钮后,获取新的 page 对象:

with page.context.expect_page() as new_page_info: page.get_by_role("link", name="在新窗口打开").click() new_page = new_page_info.value new_page.wait_for_load_state() expect(new_page.get_by_text("新页面内容")).to_be_visible()

下载文件也是同样的思路,with page.expect_download() as download_info:包裹触发下载的操作,然后通过download_info.value获取下载对象,再决定保存到本地还是直接读取内容。这套事件驱动模式,把 Selenium 时代需要写大量监听逻辑的事情,简化到四五行代码。

6. 踩坑实录:我在这套方案上踩过的核心问题

6.1sync_api与async_api混用报错

刚开始在项目里引入 Playwright 时,我写过一个最简单的脚本:

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

这段脚本在本地跑得很好。但我把它接到 pytest 里时,遇到过一个经典报错:

playwright._impl._errors.Error: It looks like you are using Playwright Sync API inside the asyncio loop.

问题出在我的 conftest.py 里同时引入了异步逻辑,导致事件循环冲突。Playwright 的同步 API 与asyncio不能在同一线程内混用。解决起来也不难:要么全程用 sync_api,要么全程用 async_api,别混着来。在 Pytest 环境下,默认走 sync_api 就行,pytest-playwright插件内部已经帮你处理好了同步接口的接入。如果你确实需要用异步模式写用例,需要改用pytest-asyncio和playwright.async_api,但除非项目本身是异步服务,否则没必要。

6.2 浏览器缓存与上下文隔离问题

在开发 phase 后期,我遇到过一种诡异的场景:测试在本地跑十次有九次通过,但在 CI 上第一次跑必失败。排查了很久,发现是本地开发时浏览器上下文里残留了登录态,用例里假设"打开登录页就能看到登录表单",但实际上浏览器自动跳过了登录页。

解决方案就是前面讲过的:确保每个用例都从独立的context开始。pytest-playwright默认的pagefixture 已经是这个行为,但如果你自己写了复用上下文的 fixture,一定要想清楚哪些数据该复用,哪些不该。比如 localStorage、sessionStorage、cookie 这类状态,除非是测试数据准备阶段主动注入,否则不要让它跨用例残留。

6.3 CI 无头模式下定位失败,本地却通过

跑到 CI 那一步,我们又在无头浏览器上翻车了。同样的代码,本地headed=True时稳定,一进 CI 的 headless 模式就报"元素不可见"或者"点击被拦截"。

后来定位到两个主要因素。

第一个是窗口尺寸。无头模式下默认窗口视口是固定值,某些响应式布局的元素可能因为视口太窄被折叠到汉堡菜单里。解决方式是在 fixture 里显式设置视口尺寸:

@pytest.fixture def page(page): page.set_viewport_size({"width": 1920, "height": 1080}) return page

第二个是CSS 动画和骨架屏。开发环境本地运行网络快,动画很快结束。CI 里 CPU 资源和网络带宽有限,元素一直处于动画中,Playwright 判断它是"稳定"状态(位置持续不变)的时间窗口一直在延后。这类问题不能靠加sleep,要用 Playwright 的expect轮询等待来完成:

expect(page.get_by_role("button", name="提交")).to_be_enabled(timeout=15000)

6.4 默认 30 秒超时被误解

很多新手拿到 Playwright,发现默认超时是 30 秒,以为可以高枕无忧。实际上page.goto()的超时和定位器操作的超时是两套体系。goto()超时只管页面导航,如果页面导航成功但某个异步资源加载慢,元素定位会继续走定位器自己的超时。如果什么都不配置,定位器默认也是 30 秒,但如果你在代码里手动写过.click(timeout=5000),那你得知道这 5 秒是从点击那一瞬间开始算的,不是从页面导航开始算的。

调超时参数时,别盲目改全局配置,要根据具体场景理解哪个环节卡住了。networkidle一直不触发,经常不是超时问题,而是页面里有持续的 WebSocket 或轮询请求,这时候等待"某个关键元素可见"比等"整个网络空闲"更靠谱。

7. 报告、CI 集成与后续演进

7.1 生成能让团队看懂的测试报告

一套自动化测试要真正用起来,报告必须让人一眼看懂。pytest 生态里最经典的是pytest-html和allure-pytest。我给团队接入的是pytest-html,因为它零配置、生成快,适合日常回归快速看结果。

pip install pytest-html pytest --html=reports/report.html --self-contained-html

--self-contained-html会把样式、截图都打进一个 HTML 文件,方便发到群里或者挂在 CI 的 artifact 上看。截图会自动嵌入报告,前提是你开启了--screenshot on或only-on-failure。

Allure 适合需要长期沉淀测试资产、想按功能模块分组展示通过率的团队,但它要额外维护 allure 命令行工具,配置成本更高。我的建议是:先跑通pytest-html,等报告内容成为团队晨会的常规输入后,再考虑是否引入 Allure。

7.2 放进 CI 流水线时的最佳实践

我在 CI 里执行的命令通常是:

pytest -n 4 --maxfail=5 --reruns=1 --reruns-delay=2 \ --html=reports/report.html --self-contained-html \ --trace on --video on --screenshot on

-n 4来自pytest-xdist,让 4 个 worker 并行跑用例。UI 测试并行时要特别注意两点:一是每个 worker 都会启动独立的浏览器实例,内存开销要做好预算,8G 内存的 runner 跑 4 个 worker 比较稳;二是用例之间绝对不能有共享文件或数据库状态,否则并行等于自找麻烦。

另外,CI 执行机上安装浏览器内核的步骤别忘掉:

playwright install --with-deps chromium

如果 CI 用的是 Docker,直接基于官方镜像mcr.microsoft.com/playwright打包是最省事的,里面已经预装好了所有系统依赖。

7.3 从单机脚本到可维护测试资产

项目跑到第四个月时,我的用例数量从最初的 40 条涨到了 180 条,但单轮执行时间反而从 35 分钟降到了 20 分钟。实现这个结果的几个关键动作值得记录一下。

第一,把用例按业务模块和稳定性分了两个等级。核心回归用例每次提交代码都跑,其余用例放到每天晚上跑。第二,给所有 fixture 和页面对象方法写了简短注释,标注了对应业务场景,方便后来人快速定位。第三,把定位器收敛到页面对象中,测试用例层看不到任何 CSS 选择器。第四,接口监听和 UI 断言结合,让失败用例能立刻判断是前端问题还是后端问题。

我还做了一件当时觉得麻烦、事后觉得回报最高的事:把 trace 文件都拉回到本地留档。每次 CI 失败,直接下载失败用例对应的 trace 文件回放,几乎不需要跑一遍本地环境就能定位问题。对我来说,这就是 Playwright 相较于其他工具体验断层式领先的地方。

从我这一年多的维护体会来看,自动化测试真正值钱的地方不是脚本量,也不是覆盖率数字,而是每一次失败都能用最短时间定位到真实缺陷。Playwright 的 trace、自动等待、iframe 处理和接口监听,加上 Pytest 的 fixture 和参数化,基本上把"稳定 + 可排查 + 易维护"这三个诉求都照顾到了。如果你也准备在团队里推这套组合,我建议先把 trace 开起来,把报告做明白,再谈覆盖率。这些看起来不起眼的细节,往往比选型时纠结的功能清单重要得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 1:16:24

微信H5调用支付宝支付的合规实现方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:16:05

Win10重装不是一键的事:UEFI/BIOS/PE底层原理与实战排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:15:09

多源Transformer信贷评分:融合流水与文本的多源风控方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:14:51

I2C多主机仲裁与时钟延展:开漏输出、线与逻辑及工程避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:14:21

手语机器人实战:从MediaPipe姿态识别到ROS关节指令映射

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:12:42

V1项目封装与总结:芯片级、PCB封装库与软件接口封装实战复盘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华