Shell多进程并发:从、xargs -P到GNU parallel的提速实战
这一章我们专门聊一个特别实际的话题Shell 多进程并发编程。写过批量脚本的人应该都有这种经历——一个for循环老老实实跑一百次任务CPU 明明是八核十六线程但整个脚本跑下来用了大半天。问题不在你写的逻辑上而在于默认串行执行把多核硬件完全浪费了。这一章要解决的就是这件事在不换语言、不引入复杂框架的前提下用 Shell 自身的机制把批量任务并发化把执行时间压下来。内容适合谁呢主要是两类读者一类是刚写完入门教程、正在尝试处理“批量下载”“批量压缩”“批量转码”这类场景的朋友另一类是已经在生产环境里用 Shell 脚本跑批处理任务但发现速度不理想、想找更稳妥方案的运维和开发。我尽量把原理、坑和可直接抄的模板都讲清楚。1. 串行for循环的瓶颈所在为什么任务越跑越慢1.1 一个最容易写出来的循环问题出在哪先看一段最典型的批量处理代码for id in $(cat ids.txt); do wget -q http://192.168.1.100/files/${id}.tar.gz -O /data/backup/${id}.tar.gz done这段代码逻辑没问题但它最大的问题是“一个一个来”。当前一个wget下载完才会启动下一个wget。假如每个文件平均下载需要 2 秒100 个文件就是 200 秒。但如果网络带宽、目标服务器并发能力都允许很多任务其实是可以同时下载的。串行执行意味着单核在干活其余核心在围观。我再拆一下时间构成。一次wget命令的耗时主要分三块DNS 解析和建立连接的时间数据真正在网络上传输的时间写入磁盘的时间。这三块里真正占用 CPU 的时间少得可怜绝大部分时间都在等网络、等磁盘、等外部服务。如果你只有一个进程在等那么这段时间里 CPU 基本上是在空转宝贵的多核资源就这么浪费掉了。1.2 Shell 任务的两大类耗时特征想优化并发先要对任务做分类。我在实际项目里基本把任务分成两类I/O 密集型任务比如下载文件、上传日志、数据库导入导出、视频转码中的磁盘读写。这类任务的特点是等待时间长、CPU 占用低非常适合多进程并发。并发数量可以开得比较大因为它们在等待的时候并不抢 CPU。CPU 密集型任务比如图片压缩、hash 计算、日志解析。这类任务本身把 CPU 跑满了并发数量不必超过物理核心数不然反而会互相抢资源、性能下降。Shell 本身的语法并不区分这两类但你心里要有数。后续配置并发线程数时I/O 密集型开大点、CPU 密集型开小点是个永恒的原则。举个例子视频转码用 FFmpeg每个进程都会把单个核心吃满你开 100 个并发进程结果是 100 个进程抢几个核速度没快多少系统负载却飙升。反过来如果任务是下载文件并发开 100 个、200 个都没问题瓶颈通常在网络带宽而不是 CPU。1.3 进程启动开销也不容忽视还有一个容易被忽略的点Shell 每执行一条外部命令都要fork一个子进程再通过exec加载目标程序。这个过程虽然快但也不是零成本。如果你在一个循环里跑上百次外部命令启动开销会累积成可观的耗时。并发不能治疗这个问题但可以让等待和启动阶段重叠起来。这部分我后面会反复用到time命令来验证优化效果。记住一句话先看任务的耗时构成再决定用什么并发方案不要盲目堆并发数。2. 三种主流 Shell 并发方案、xargs -P、GNU parallel2.1 最原始的后台符与waitShell 自身最简单的并发手段是后台符。命令后面加一个Shell 不会等它执行完而是直接返回并继续执行下一条命令。配合wait命令可以等待所有后台任务结束。直接看示例#!/bin/bash for id in $(cat ids.txt); do wget -q http://192.168.1.100/files/${id}.tar.gz -O /data/backup/${id}.tar.gz done wait这段代码会把ids.txt里所有任务一次性全部丢到后台然后wait等它们全部跑完。只改了一行整个脚本就从“串行”变成了“并发”。但别高兴太早。这种写法有个致命问题它没有并发上限。如果ids.txt里有 2000 个 id你就一次性创建了 2000 个wget子进程。目标服务器扛不住是一回事本机的文件描述符、进程表空间也会瞬间被打满。如果此时还开着很多 IO系统负载会非常难看甚至可能触发ulimit限制导致部分进程启动失败。所以裸用适合“任务量很小、不超过 10 个”的场景。任务量一大必须配合后面的并发控制手段。2.2xargs -P参数按行并发的利器xargs是每个 Linux 发行版都自带的标准工具-P参数用来指定同时运行的进程数。它会把输入按行拆分然后交给后面的命令并行处理。改造一下刚才的场景cat ids.txt | xargs -P 10 -I {} wget -q http://192.168.1.100/files/{}.tar.gz -O /data/backup/{}.tar.gz这里-I {}表示用输入行替换{}-P 10表示最多同时运行 10 个wget进程。xargs会自动维护一个进程池任务开始 10 个跑完一个就补充一个直到所有行都处理完。相比裸xargs -P的优点是简洁、自带并发上限、不需要自己写 wait。它的写法也更适合“从文件读取参数列表逐条执行外部命令”的批处理场景这也是为什么它在批量下载、批量格式转换脚本里如此常见。但是它也有自己的坑-I {}会改变xargs拼接参数的方式而且当命令本身需要多级 shell 调用时引号嵌套会比较费劲。例如cat urls.txt | xargs -P 8 -I {} sh -c curl -sL $1 /data/$(basename $1) _ {}注意sh -c后面那个_ {}_会变成$0{}变成$1。这种写法在复杂命令中很常见但初学者经常漏掉$0占位符导致参数错位。我建议不是特别复杂的场景能不用多级 shell 就不用尽可能让xargs直接拼接命令参数。2.3 GNU parallel功能最全的重型方案xargs -P已经能解决大部分需求但如果你需要更精细的控制比如“每个任务失败自动重试”“输出顺序保持与输入一致”“给任务加进度条”那就得请出 GNU parallel 了。下个例子cat urls.txt | parallel -j 8 --retries 3 --keep-order wget -q {}说明-j 8: 最多同时 8 个任务--retries 3: 任务失败自动重试 3 次--keep-order: 输出结果保持输入行顺序免得日志对不上号{}: 输入行的占位符。GNU parallel 还支持把任务分发到多台机器上执行这个特性在中小集群环境里特别好用。不过它是独立的第三方工具部分最小化安装的服务器上没有需要先安装yum install parallel或apt install parallel都行。对已经能跑xargs的环境来说GNU parallel 是增强选项不是必选项。2.4 把三种方案放在一起看下面这张表是我平时选型时参考的你可以直接存着方案并发控制保序输出失败重试学习成本适用场景wait需自己控制不支持不支持最低任务量小于等于 10 的脚本xargs -P支持-P不支持不支持中批量参数外部命令GNU parallel支持-j--keep-order--retries中高复杂批量任务、多机分发我的建议是日常两三行的批处理脚本用xargs -P就足够了脚本逻辑复杂、对输出顺序和失败重试有明确要求时直接上 GNU parallel省心很多。3. 给并发踩刹车并发数与进程池的精细控制3.1 不控制并发的后果一次把机器搞到卡死的真实经历前年我帮团队写过一个批量转码脚本最初版本就是裸并发。当时files.txt里一共 5000 个音频文件脚本一次性全部丢到后台FFmpeg 进程瞬间起了 5000 个。结果机器直接在几分钟内卡到 SSH 都连不上最后只有重启。重启完我第一件事就是翻/var/log/messages看到了大量fork failed: Resource temporarily unavailable的错误。这就是没控制并发上限的代价。后来我加了两个保险一是脚本开头用ulimit -u设置用户最大进程数防止单用户把系统进程表打爆二是在代码里用信号量方案限制并发数。ulimit只是兜底真正好用的是信号量。3.2 信号量控制并发数的经典写法Shell 里实现信号量最经典的做法是用命名管道FIFO加文件描述符。思路很简单先往管道里塞 N 行数据每个任务执行前读走一行拿走令牌执行完再还回去一行。谁拿到令牌谁才能执行从而限制同时运行的任务数量。看完整示例#!/bin/bash max_jobs6 fifo/tmp/sem_$$ mkfifo $fifo # 打开读写两端避免 read 到 EOF 退出 exec 9$fifo rm -f $fifo # 向管道放入 max_jobs 个令牌 for ((i0; imax_jobs; i)); do printf \n 9 done task() { local id$1 # 这里是真正的任务逻辑 sleep $((RANDOM % 3 1)) echo job $id done } for id in {1..20}; do read -r -u 9 # 读走一个令牌拿不到就等 { task $id printf \n 9 # 任务结束归还令牌 } done wait exec 9-运行效果是同一时刻最多 6 个任务在执行任务完成后新任务自动接上直到 20 个任务全部跑完。这里的核心在于exec 9$fifo它同时打开管道的读端和写端否则当没有写端引用时read会立刻遇到 EOF 退出信号量就失效了。我之前在内网分享这段代码时有同事问为什么不直接用xargs -P 6。确实这个场景xargs更简洁。但做大型脚本时信号量方案可以嵌进循环逻辑里、配合变量传参、动态调整并发数这是只靠xargs不好做到的。另外写轮子本身就是理解原理的过程理解了这个再去看xargs的-P实现原理你会更踏实。3.3 trap 与进程清理并发脚本有个隐蔽问题如果脚本运行到一半被 CtrlC或者某个子进程异常退出后台任务可能被留在那里继续跑导致“僵尸进程”和资源泄漏。所以生产环境脚本最好加上traptrap kill 0; exit 1 INT TERM EXITkill 0会杀死当前进程组里的所有子进程在并发场景下特别管用。建议所有用到的脚本都加上这一句它能帮你少踩很多坑。判断并发数是否合理的经验值I/O 密集型任务并发数 CPU 核心数 x 4 ~ x 8CPU 密集型任务并发数 CPU 核心数。直接套这个起步再用top观察负载如果不高可以再加。4. 并发场景最常见的三个事故输出、文件和退出码4.1 多个进程同时写一个日志文件收到的是什么串行脚本里你写入日志无所谓换个行追加一行就行。但并发场景下多个子进程同时执行echo xxx log会碰到文件描述符竞争。小行还好如果是较长的内容、多行输出两个进程可能互相交错把日志格式撕得稀烂。更严重的是写同一个结果文件。比如批量任务要把结果汇总到一个result.txt如果每个子进程都直接往同一个文件追加你最后得到的一定是缺行、乱序、甚至内容损坏的文件。这个问题有三种常见解法每个任务分开写日志文件例如logs/${id}.log跑完后再合并。这个办法最糙但最稳适合日志本来就不要求实时查看的场景。用flock给文件加锁强制同一时间只有一个进程能写目标文件。例如exec 9/data/result.txt flock -x 9 echo $result /data/result.txt flock -u 9 exec 9-把写文件的重任交给最后的汇总环节子任务只输出到 stdout用 GNU parallel 配合--keep-order统一收集再统一写文件。4.2 终端输出重影和竖行断裂并发脚本最直观的乱象是终端输出多个进程同时打印一行字可能被另一行截断成两半。如果你只想在终端看到大概进度这无所谓但你要是想从输出里解析结果就悲剧了。处理办法是把各任务的 stdout 和 stderr 分别重定向到独立文件cat urls.txt | xargs -P 10 -I {} sh -c curl -sL {} -o /dev/null 2/tmp/err_{}用任务 id 区分错误文件后面排查时能立刻定位是哪个任务出了问题。这里有个小技巧文件名里必须带唯一的任务编号不能所有任务共用一个 stderr 文件否则又变成并发写同一个文件的问题了。4.3 退出码静默丢失并发脚本里最坑的一环后台并行模式下父 Shell 不能像串行那样拿到子进程的退出码。很多人改造完脚本后发现“为什么有的文件没生成脚本却显示成功了”多半就是没检查退出码。你在串行脚本里写的set -e在并发场景下并不好使。后台子进程失败父进程不一定能感知到。我常用的兜底方案是cat ids.txt | xargs -P 10 -I {} bash -c convert src/{}.jpg dst/{}.jpg || echo {} /tmp/failed.txt任务失败时把失败的 id 写入/tmp/failed.txt脚本跑完后再统一看一眼这个文件决定是否重跑。这比单纯依赖退出码靠谱得多。如果是 GNU parallel--joblog参数更好用它会记录每个任务的运行状态和退出码失败重试、失败梳理都很方便。4.4 管道退出码的隐藏问题还有一个坑来自管道本身。cmd1 | cmd2 | xargs -P这个链式结构里最终退出码看的是最后一个命令前面命令的失败会被吞掉。想保留中间失败信息得开set -o pipefailset -o pipefail cat urls.txt | xargs -P 10 -I {} wget -q {} -O /dev/null这样一旦cat读取失败管道整体也会返回非零状态。这个细节很多人不知道等到排查脚本为什么“成功”时才发现源头读取早就断了。5. 实战前后对比批量下载压缩包的效率提升实测5.1 原始串行脚本假设有个批量下载场景需要从内网服务器拉取 500 个压缩包每个约 30MB保存到本地/data/backup/。原始脚本长这样#!/bin/bash for id in $(seq 1 500); do wget -q http://192.168.1.100/packages/${id}.tar.gz -O /data/backup/${id}.tar.gz done我们先用time跑一遍。本地磁盘很快网络局域网延迟也不高单个文件平均耗时约 1.8 秒500 个文件串行总耗时约 900 秒即 15 分钟。5.2 改成并发脚本因为这是典型的 I/O 密集型任务我决定用xargs -P来改造核心思路是并发数开大一些。考虑内网带宽和磁盘写入能力先设 20 并发#!/bin/bash seq 1 500 | xargs -P 20 -I {} wget -q http://192.168.1.100/packages/{}.tar.gz -O /data/backup/{}.tar.gz实测耗时一下子降到 75 秒左右。从 900 秒到 75 秒12 倍的提升本质就是 20 个文件同时在下载等待时间被压缩掉了。5.3 反复调并发数找到最优值我又分别试了-P 10、-P 50、-P 100并发数实测耗时备注1900 秒串行基线10140 秒左右提升明显2075 秒左右收益最大的一段5072 秒左右提升很小10073 秒左右反而有轻微波动这个结果说明当并发数超过某个阈值后带宽和磁盘写入能力已经成为新的瓶颈无脑加并发只会让资源白热化速度不再上升。所以调并发是一个“找拐点”的过程不必一味求大。做这类优化时建议每次都记录耗时和并发数选一个收益最大、稳定性最好的值作为生产配置。5.4 更稳的版本上面脚本虽然快但缺少失败处理。我在生产环境里会再加两行#!/bin/bash set -o pipefail seq 1 500 | xargs -P 20 -I {} bash -c wget -q http://192.168.1.100/packages/{}.tar.gz -O /data/backup/{}.tar.gz || echo {} /tmp/fail.log脚本跑完若是返回非零就去/tmp/fail.log看失败列表。这种方法对付偶发网络抖动非常有效。6. 什么时候不该用 Shell 并发看看你的任务边界在哪里Shell 并发虽然好用但不是所有批量优化都该用它。如果任务之间有数据依赖比如“第二个任务必须等第一个任务生成完文件再开始”那就不要并发老老实实写串行依赖。又或者你要共享大量状态、做复杂的消息通信Shell 的进程边界非常粗糙用起来很别扭这种情况更适合直接用 Python 的多进程库比如multiprocessing或者用专门的任务队列工具。Shell 并发的优势是简单粗暴适合“批量启动一批外部命令并等待结果”的场景。一旦你发现自己开始写复杂的进程间通信、锁管理、状态同步这说明任务复杂度已经超出 Shell 的舒适区了。另外把并发数写进脚本后不要以为万事大吉。上线前要观察机器负载、内存占用、日志输出慢慢调参数。我自己习惯先用 10% 的样本量跑通流程再全量跑。这一步能省下很多半夜被监控告警吵醒的时间。最后分享一个小习惯所有并发脚本我都会在开头加一行注释写明“预计并发数、结合任务类型调整、实测耗时”。这行注释对我自己以及后来接手脚本的同事都有帮助。毕竟脚本能跑只是第一步能稳定、可维护地跑下去才是真正的效率提升。

相关新闻

手提袋检测数据集构建:VOC与YOLO标签格式转换与校验全指南

手提袋检测数据集构建:VOC与YOLO标签格式转换与校验全指南

简介:本资源是面向计算机视觉初学者与目标检测实践者的手提袋专用检测数据集,适用于YOLO系列及VOC兼容框架下的模型训练与验证,解决日常场景中手提袋小目标识别、定位精度不足等实际问题。压缩包共2000个文件,包含7133张JPG格式图…

2026/10/5 10:51:44 阅读更多 →
LangChain Agent 企业级改造:不加业务代码,补齐并发、记忆、可观测与安全

LangChain Agent 企业级改造:不加业务代码,补齐并发、记忆、可观测与安全

1. 先说说我为什么盯上“不改一行代码”这件事前阵子帮团队把一个 LangChain Agent 从 Jupyter Notebook 挪到公司服务器上,原本以为“跑通了就行”,结果第一天就被运维同事拉去开会:并发一起来就把 CPU 干满、会话一多上下文全乱、模型报错连…

2026/10/5 10:50:44 阅读更多 →
洛谷P1387最大正方形:二维前缀和经典题解与踩坑指南

洛谷P1387最大正方形:二维前缀和经典题解与踩坑指南

今天要聊的是洛谷 P1387 最大正方形。这道题在GESP C五级的前缀和练习里算得上“标准课代表”,因为它把二维前缀和的三板斧——建表、区域查询、枚举判断——全都在一道题里完整走了一遍。先说结论:这题最适合用二维前缀和来做,先预处理一个前…

2026/10/5 10:50:44 阅读更多 →

最新新闻

插件加载机制全解析:从设计原理到失败排查的工程实践

插件加载机制全解析:从设计原理到失败排查的工程实践

1. 从“plugins”这个标题说起:它到底在解决什么问题 “plugins”这个词看起来简单到几乎不像一个项目标题,但恰恰是这种极简的词,背后藏着软件工程里最核心的一类设计思想—— 可扩展性 。我做了十多年开发,从桌面端到移动端再…

2026/10/5 13:45:15 阅读更多 →
OpenShell 深度定制指南:Windows 开始菜单增强与效率优化

OpenShell 深度定制指南:Windows 开始菜单增强与效率优化

1. 从零认识 OpenShell:它到底解决什么问题第一次听到 OpenShell 这个名字,很多人会下意识以为它是个远程连接工具或者某种终端模拟器。实际上,OpenShell 是一个面向 Windows 平台的开始菜单与任务栏增强工具,最早由社区开发者发起…

2026/10/5 13:45:15 阅读更多 →
Kubernetes虚拟化编排工具实战:从容器调度到Nginx部署全解析

Kubernetes虚拟化编排工具实战:从容器调度到Nginx部署全解析

项目标题是“知识点8---虚拟化编排工具Kubernetes”,说真的,这个标题放在课程大纲里略显平淡,但放到真实的生产环境中,它可能是运维和开发之间最难跨越的一道坎。如果你已经会写Dockerfile、能在单机跑起容器,那下一步…

2026/10/5 13:45:15 阅读更多 →
Kiro Spec版本规范:从invalid version spec报错到配置实践

Kiro Spec版本规范:从invalid version spec报错到配置实践

如果你在配置 Kiro 的时候,屏幕突然砸过来一行invalidversionspecerror: invalid version spec: 2.7,相信我,你不是第一个。这个报错我第一次遇到时,盯着看了十分钟才回过味来——问题居然只是版本号写法不合法。Kiro 的 Spec 实践…

2026/10/5 13:45:15 阅读更多 →
粒子群算法优化FCM聚类在居民用电行为分析中的实践

粒子群算法优化FCM聚类在居民用电行为分析中的实践

我一个做了快十年算法落地的人,看到“粒子群算法优化FCM聚类”这种标题,第一反应不是“高大上”,而是“当年我调参调到头秃的那段日子”。但确实,这个组合在居民用电行为分析里,是一个特别典型、特别能出成果的研究方向…

2026/10/5 13:45:15 阅读更多 →
超级电容UPS设计:精准实现10-60秒掉电数据保存

超级电容UPS设计:精准实现10-60秒掉电数据保存

1. 项目概述:为什么一个“只撑60秒”的UPS反而更值得深挖?你有没有遇到过这样的场景:嵌入式设备正在往Flash里写关键日志,突然市电跳闸——结果数据没写完,整个固件分区校验失败,设备直接变砖?或…

2026/10/5 13:44:15 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

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

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

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

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/5 5:06:42 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/5 3:06:17 阅读更多 →

月新闻

我发现了一个新思路:用 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/4 11:40:45 阅读更多 →
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/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练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/4 20:14:29 阅读更多 →