Selenium迁移Playwright实战:从脆弱脚本到稳定自动化测试
花了好几年把项目的Selenium脚本一点一点调到稳定换到Playwright之后之前最脆弱的那几十条用例几乎没再闹过脾气。这话不是说Selenium不行了而是当你的回归脚本每天都要和隐性等待、元素漂移、弹窗时机较劲确实是时候看看另外一套东西了。这篇文章想做的就是把从Selenium迁移到Playwright这件事的关键点一次讲清楚不是罗列文档而是结合我在实际迁移中遇到什么、怎么改、踩了哪些坑给你一份可以直接照着操作的入门指南。无论你是刚接触自动化测试的新人还是已经用Selenium写了好几年脚本的老手这篇文章都适合先花十分钟读一遍再动手。1. 迁移之前想清楚的事1.1 不是Selenium的错而是设计思路的区别很多人在吐槽Selenium不稳定的时候其实没有意识到问题出在哪儿。Selenium的设计根源是模拟用户在浏览器里的每一个动作它的API很长一段时间里都围绕WebDriver协议展开先找元素再对元素做操作中间插一堆等待。这种模型不是不能用而是在今天这种单页应用遍地、异步加载满天飞的环境里它的容错成本越来越高。Playwright走的是另一条路。它底层不再依赖WebDriver协议而是基于浏览器调试协议直接和设备上的浏览器进程通信。这么说可能有点抽象你可以理解成一个自动化测试框架从“远程指挥浏览器办事”变成了“直接在浏览器内部派驻了一个执行者”。好处很明显操作更快、状态感知更准确、等待机制更接近用户真实体验。这也决定了迁移时不能只改API写法思路层面也要跟着换。我在迁移初期犯过一个大错把Selenium那一套“先显式等待元素出现再点一下”的代码逻辑原封不动复制到Playwright里。结果是代码变得比以前更啰嗦完全没有体会到新框架带来的便利。后来才意识到Playwright内置的自动等待已经覆盖了绝大多数场景你要做的只是声明目标它自己会等元素可见、可交互、稳定之后再执行后续动作。这个认知一旦转变代码量立刻下来一大截。1.2 迁移的收益需要先量化评估如果只是图新潮就换框架那项目的测试运维成本反而会上升。我在动手之前先把现有Selenium脚本按几个维度归了个类失败频率、维护频率、运行时长、涉及浏览器类型的复杂度。这样能直观看到哪一类脚本投入产出比最低。值得优先迁移的往往是这三类长期受元素定位不稳定影响的脚本比如动态ID、位置经常变化的元素。需要严格处理弹窗、多标签页、iframe的复杂场景。对执行速度有要求的回归集因为Playwright在协议层面的开销更小并发能力更强。反过来如果项目里的Selenium脚本大部分已经稳定运行且场景简单、改动频率低那么短期内保持现状完全可以接受。迁移不是KPI迁移是为了减少总维护成本。这个评估我建议写在项目文档里方便以后复盘。2. 环境准备与基础用法差异2.1 安装与初始化流程Playwright的安装比Selenium要省心不少。以前用Selenium你需要单独下载浏览器驱动还要处理驱动版本和浏览器版本不匹配的问题。Playwright的思路是把浏览器管理这件事一起接管了npm install playwright/test或者对应语言的库之后执行一次浏览器安装命令它会自动下载对应版本的浏览器内核版本和工具库严格对齐。以常用技术栈为例安装命令大概是npm init -y npm install -D playwright/test npx playwright install如果只想用Playwright作为普通库而不使用测试运行器可以安装核心库npm install playwright执行npx playwright install时会下载Chromium、Firefox和WebKit。实际项目如果只需要Chromium系也可以指定只装一种减少CI上的存储压力。这一步有个细节值得注意在CI环境里别忘了一些系统依赖库Playwright很贴心地提供了npx playwright install-deps命令会自动把Linux环境里缺失的库补齐。2.2 编写第一个脚本时最容易适应的变化从Selenium切换到Playwright第一感觉是选择器语法变了第二感觉是你极少再需要自己写等待。我用一个最简单的登录场景对比一下。Selenium版本常见的写法是from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() driver.get(https://example.com/login) wait WebDriverWait(driver, 10) username wait.until(EC.presence_of_element_located((By.ID, username))) username.send_keys(test) driver.find_element(By.ID, login-btn).click()同样的场景用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/login) page.fill(#username, test) page.click(#login-btn) browser.close()注意这里我没有写任何等待语句。fill和click内部已经包含了等待元素出现且可交互的逻辑。这种设计对新手特别友好因为少了一个最容易出错的思考维度到底该等什么、等多久、等到什么状态才算稳。当然自动等待不是万能的后面我会专门讲到它做不到的事。3. 核心机制差异等待、选择器与上下文3.1 自动等待的底层逻辑与适用范围Playwright的自动等待为什么敢替你做决定因为它对每个操作都定义了一套默认的“可行动状态”。比如click之前框架会检查元素是否附着在DOM上、是否可见、是否稳定不处于动画中、是否接收事件。全部满足才执行点击。换句话说它把Selenium里你手动拼出来的那些visibility_of_element_located和element_to_be_clickable组合判断内置成了标准动作的一部分。但这套机制在两种场景下有局限纯后端异步逻辑导致页面短暂失去交互状态此时元素虽然在但业务状态不对。需要精确控制时间点的测试比如某些倒计时逻辑。这种时候我一般用显式断言配合轮询来处理。Playwright提供了expect风格的可重试断言能让你用一段声明式代码完成状态等待。例如要等待某个文本出现在指定区域from playwright.sync_api import expect expect(page.locator(.status)).to_have_text(处理完成)这个断言会自动重试直到超时。它比time.sleep高级得多也远比Selenium时代自己封装等待轮询要省代码。3.2 locator API比find_element强在哪里Selenium长期以来的元素定位思路是“去全局DOM里找某个特征”比如find_element(By.XPATH, //div[classa]//span)。这种思路最大的问题是一旦页面结构变化选择器失效脚本就报错。而Playwright推广的locator更贴近用户感知它不是先找到元素对象而是描述一个元素的查找规则和行为配合后续动作一起执行。举一个具体的例子。假设页面里有一个列表每个列表项里都有一个“删除”按钮。在Selenium里要定位第二个列表项的删除按钮你会写一串万能的路径看起来绕又难维护。在Playwright里可以这么写page.locator(li).nth(1).locator(.delete-btn).click()更妙的是locator支持链式调用和相对定位代码读起来像在实际操作页面。你不需要关心目标在DOM里从头到尾走过的路径只需要描述“在某个区域内找某个东西”。我一直觉得这正是Playwright在用设计逼迫开发者写出更接近用户视角的测试而不是写脆弱的DOM结构耦合代码。还有一点容易被忽略Playwright内置了选择器引擎默认同时支持CSS和XPath你不用特意在前端加什么标记。但如果想让代码结构更稳我建议在关键交互元素上加上业务含义明确的>context browser.new_context() page context.new_page() page.goto(https://example.com)上下文还能模拟移动设备、修改User-Agent、授予地理位置权限。以前在Selenium里要借助第三方库或手动改配置才能完成的事现在成了官方API的基本操作。我在迁移一个多端适配项目时靠这个特型就直接省掉了一整套设备仿真脚本测试覆盖范围反而扩大了。4. 逐步迁移实例一个表单流程的改造对比4.1 原始Selenium脚本剖析为了更直观地说明迁移方式看一个稍微复杂点的例子提交一个包含名字、邮箱、手机号注册表单还要处理弹窗确认和页面跳转。Selenium的原始脚本大概是这个样子driver.get(https://example.com/register) driver.find_element(By.ID, name).send_keys(张三) driver.find_element(By.ID, email).send_keys(zhangsanexample.com) driver.find_element(By.ID, phone).send_keys(13800138000) driver.find_element(By.NAME, agree).click() driver.find_element(By.CSS_SELECTOR, button[typesubmit]).click() WebDriverWait(driver, 10).until(EC.alert_is_present()) alert driver.switch_to.alert alert.accept() WebDriverWait(driver, 10).until(EC.url_contains(welcome)) assert welcome in driver.current_url这段代码问题不少每一步在元素加载慢时都可能抛异常切换alert要单独处理URL断言还要再等待一次。它的逻辑意图是清楚的但执行保障完全依赖那一堆WebDriverWait。4.2 迁移为Playwright的过程与关键改动Playwright版本我分三步来改。第一步简化页面交互部分page.goto(https://example.com/register) page.fill(#name, 张三) page.fill(#email, zhangsanexample.com) page.fill(#phone, 13800138000) page.check([nameagree]) page.click(button[typesubmit])第二步处理弹窗。Playwright对dialog也做了自动等待不再需要手动切换上下文page.on(dialog, lambda dialog: dialog.accept())第三步断言跳转结果。用expect替代手动轮询expect(page).to_have_url(https://example.com/welcome)整体改完代码量几乎减少一半。更重要的一点是原来的脚本在执行顺序上依赖各个等待的先后改动任何一处都要重新评估时序现在的版本把时序交给了框架去协调逻辑只保留了业务的“做什么”而没有掺杂太多“怎么等”。4.3 迁移时容易遗漏的隐藏点迁移不是简单的API替换。有几个隐藏点我当时反复踩必须单独拎出来提醒文件上传Selenium里常用send_keys把文件路径传给input[typefile]。Playwright里使用set_input_files但如果你对不可见的上传控件做操作必须先调用locator.set_input_files()不要直接去找可见手动交互的路径。下载文件Selenium时代经常要自己配置下载目录和文件类型白名单。Playwright的下载处理更规范可以通过page.expect_download()上下文管理器捕获下载事件然后决定保存还是丢弃。iframe与阴影DOM这是很多Selenium脚本迁不过去的真正原因。Playwright对frame的定位方式比Selenium直接示例frame page.frame_locator(#modal-frame) frame.locator(.confirm-button).click()如果页面里用了shadow DOMPlaywright同样允许CSS选择器直接穿透这让以前方案里困扰了很久的组件内元素定位问题一下子找到了出口。这一点在从Selenium迁移时可以说价值最大因为很多业务系统的前端框架早就把组件封装得一层套一层了。5. 测试能力升级并发、追踪与调试体验5.1 并行执行不再依赖外部工具Selenium的项目跑并发常规思路是引入多线程或者借助测试框架的并发模式再配Selenium Grid。但管理节点、维护远程浏览器环境本身又是一大摊事。Playwright把并行执行直接做进了测试运行器默认每个worker进程跑一个独立环境用例之间天然隔离。我用一个简单配置说明// playwright.config.js export default { testDir: ./tests, fullyParallel: true, workers: process.env.CI ? 4 : undefined, };这个配置在本地跑的时候会自动根据CPU核心数分配合适的worker数量在CI里则指定为4。你不需要额外准备集群也不用担心浏览器实例串数据。对比下来Selenium Grid方案在新项目里确实没有太多继续选它的理由除非你的团队运维能力已经很强且有跨环境兼容的硬性需求。5.2 trace和追踪功能让调试效率大幅提升我以前用Selenium在CI上看到一条失败用例最痛苦的事就是不知道失败时页面到底长什么样。虽然有截图功能但截图只能看静态画面看不到网络请求、控制台报错和元素定位的具体时序。Playwright的trace功能解决的就是这个问题。执行用例时开启追踪跑完之后会生成一个zip文件里面记录了页面发生的所有关键事件每次点击的位置、网络请求、console日志、截图、DOM快照。用Playwright自带的trace查看器打开你可以看到失败前几步到底发生了什么甚至能看到哪一个元素在等待超时。await page.tracing.start({ screenshots: true, snapshots: true }); // 你的测试步骤 await page.tracing.stop({ path: trace.zip });这条能力我实际用下来觉得尤其适合交给新人去排查问题不用一上来就去翻源码先看trace回放很多问题就水落石出了。5.3 断言、Mock与拦截请求的生态整合做前端自动化测试经常会遇到“我不想等后端接口返回只想验证前端交互逻辑”的情况。以前在Selenium里做这件事要借助代理工具配置各种证书、修改请求转发非常繁琐。Playwright提供了原生网络拦截能力可以在发送请求前修改或者直接返回模拟数据。await page.route(**/api/user/**, route { route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ id: 1, name: 测试用户 }) }); });这个能力让测试环境不再依赖一个完整可用的后端服务很多场景下能单独在前端层面完成集成验证。配合内置断言库一个测试文件里就能覆盖交互、数据展示和错误提示不需要再拆成多套工具组合。对团队而言工具链越少维护成本就越低新人上手成本也越低。6. 常见问题与排查技巧实录6.1 定位超时和严格模式冲突Playwright的locator有一个特点定位结果如果匹配到多个元素默认会报错提示你使用了“严格模式”。这其实是帮你拦截了隐藏的问题。Selenium里多个元素匹配时通常只取第一个很多错误就这样被悄悄掩盖了。遇到严格模式报错时最快的处理办法是先明确业务意图。如果你确实要操作第一个或特定的某个就使用.first、.last或.nth(index)。如果页面结构本身导致选择器写得过宽应该收紧选择器。千万别为了过测试就写.first糊弄过去否则以后页面顺序一变就又是一颗雷。定位超时是另一类高频报错。解决办法不是加超时时间而是先确认选择器是否唯一。我在实际操作中习惯用Playwright自带的代码生成器跑一遍目标操作看它生成的选择器是什么。这个生成器会优先选择相对稳定的属性比自己手写简单粗暴的选择器靠谱得多。6.2 网络请求变化导致的用例偶然失败有一类用例失败最让人头疼本地跑的时候明明能过上了CI就偶发失败。我排查过很多次后发现很大概率是后端接口返回不够稳定而前端界面又恰好依赖该返回状态。Selenium时代对这种问题没有太好的内置手段你只能加等待或者重试。Playwright里有两个思路可以根治对依赖的外部请求做Mock减少对真实环境的依赖。用wait_for_response对关键接口做显式同步确保页面拿到数据后再做断言。with page.expect_response(**/api/register) as response_info: page.click(button[typesubmit]) response response_info.value assert response.ok这个模式把“操作”和“对结果的同步等待”绑定在一起逻辑上很顺也比随机sleep可靠得多。6.3 浏览器启动失败与系统依赖问题换了新框架原有CI环境上第一次跑Playwright很常见到浏览器启动失败。这通常不是代码问题而是缺少系统级依赖库。在Linux环境执行npx playwright install-deps如果是在Docker里面跑测试建议基于官方镜像而非自建基础镜像否则容易漏掉字体库和图形环境的依赖。我还遇到过一种情况系统本身装了Chrome但Playwright下载的是自己的Chromium内核两边版本不一致导致行为略有差异。解决方法是统一使用Playwright自己管理的浏览器版本不要混用。6.4 代码生成器是开始走迁移的最短路径很多人在迁移初期不知道从哪下手我的建议是不要纯手工重写脚本用Playwright的命令行工具快速生成一段基准脚本再在此基础上做简化。npx playwright codegen https://example.com执行后会打开一个带调试器的浏览器窗口它实时记录你的操作并在右侧生成对应代码。你可以直接对照Selenium里的操作步骤一步一步在可视化环境里走一遍流程生成代码后复制到测试文件里再补充断言和业务逻辑。这样至少能保证定位器和操作顺序是正确的减少了从Selenium翻译选择器时的误差。代码生成器不是写测试代码的最终答案但它是降低迁移启动成本非常有效的拐杖用完之后你再手动优化代码结构效率会高很多。收尾的一点个人体会如果非要说这次迁移给我最大的改变是什么我会说是测试代码的信任度变高了。以前Selenium脚本能过我偶尔会怀疑是不是等待时间凑得好现在Playwright的用例能过我更愿意相信是业务行为真的符合预期。这个心态变化往往比节省了多少执行时间更重要因为自动化测试的价值从来不只是“能跑”而是“跑出的结果值得被相信”。最后再说一个小技巧迁移完成之后别急着删掉Selenium的老代码保留一到两个版本作为参考尤其是那些包含复杂业务逻辑的用例。因为很多历史用例里面的前置条件、数据组织方式、断言逻辑都是宝贵的业务资产在Playwright里重写时完全可以用作需求清单避免遗漏。等新脚本稳定运行几周再彻底清理旧代码这个过渡期会让你安心很多。

相关新闻

高温热湿流动耦合模型解析:从物理机制到COMSOL多物理场仿真

高温热湿流动耦合模型解析:从物理机制到COMSOL多物理场仿真

高温热湿流动耦合模型,乍一听像是只会在论文摘要里出现的词,但只要做过热风干燥、高温除湿或者湿热环境控制,就会知道它其实每天都在工程现场刷存在感。热空气为什么越吹越“干”、湿物料为什么在某个温度段反而降温、冷凝水为什么会出现在你…

2026/10/13 19:27:55 阅读更多 →
象棋复盘编号法:从112323看防守转换与绝杀

象棋复盘编号法:从112323看防守转换与绝杀

我平时复盘有个习惯,每一局印象深的棋都会顺手记一个编号。112323 就是这么来的,当时只是随手写下的谱号,回头一拆才发现有意思——它刚好对应三个节点:第 11 回合的一次防守,第 23 回合的一次转换,以及第 …

2026/10/13 19:27:55 阅读更多 →
C++学生成绩管理系统:从文件读写到STL算法的工程实践

C++学生成绩管理系统:从文件读写到STL算法的工程实践

简介:这是一套完整的C学生成绩管理系统毕业设计项目,面向计算机专业本科生及C初学者,解决课程设计、毕业设计中常见的数据库交互与桌面应用开发需求。资源包含26个文件,以5个核心CPP源码文件和4个H头文件构成主程序逻辑&#xff0…

2026/10/13 19:26:49 阅读更多 →

最新新闻

Python爬虫实战:手机APP数据抓取与解析全流程

Python爬虫实战:手机APP数据抓取与解析全流程

简介:这份PDF资料面向具备Python基础、对网络请求与爬虫开发感兴趣的技术爱好者与研发工程师,围绕手机APP数据传输的抓取与解析展开,重点讲解如何借助Fiddler抓包定位登录与话题接口,再通过urllib2构造HTTP请求、携带Header与表单…

2026/10/14 3:04:31 阅读更多 →
C++ 对接 S3 对象存储实战:SDK 选型、分片上传与避坑指南

C++ 对接 S3 对象存储实战:SDK 选型、分片上传与避坑指南

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

2026/10/14 3:04:31 阅读更多 →
SQLite安装全指南:嵌入式数据库跨平台配置与常见坑

SQLite安装全指南:嵌入式数据库跨平台配置与常见坑

第一次折腾SQLite的时候,我找了一圈“Setup.exe”,愣是没找到。下载页面全是压缩包,网上一堆教程各写各的,有人说不用装,有人说得编译源码,看得人发懵。后来我才搞明白:SQLite的“安装”逻辑跟M…

2026/10/14 3:04:31 阅读更多 →
C++手写数据结构实战:动态数组、哈希表、AVL树与图算法

C++手写数据结构实战:动态数组、哈希表、AVL树与图算法

简介:《Implementing Useful Algorithms in C》由Dmytro Kedyk撰写,面向具备一定C基础、希望深入掌握算法设计与工程实现的开发者与计算机专业学生。全书从算法正确性证明、渐进记法与机器模型等基本原则切入,系统讲解数据类型选择、内存管理…

2026/10/14 3:04:31 阅读更多 →
本地27B模型配Function Calling:工具不触发的原因与实战

本地27B模型配Function Calling:工具不触发的原因与实战

给本地跑的27B模型配了三个专业工具之后,我做的第一件事就是拿它试聊天。你猜怎么着——工具列表明明挂在接口定义里,模型却像一个看见工具箱、却只肯用手拧瓶盖的人,简单问题全凭语言能力直接答,一个工具都不碰。我一度觉得这套配…

2026/10/14 3:04:31 阅读更多 →
容器内nvidia-smi报Unknown Error的排查与根治

容器内nvidia-smi报Unknown Error的排查与根治

凌晨两点半,工作群里弹出一条消息:某个长期跑的容器,前一秒还好好的,后一秒容器里的nvidia-smi直接报Failed to initialize NVML: Unknown Error。宿主机上执行nvidia-smi一切正常,新建一个临时容器也能正常看到卡&…

2026/10/14 3:03:31 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →