Rust模块系统详解:mod.rs布局、可见性与现代工程实践
Rust的模块系统尤其是mod.rs的用法我自认为早就玩明白了可每次看到刚入门的朋友问“为什么我的mod一直找不到文件”时还是忍不住要把整套思路重新捋一遍。很多教程喜欢把mod.rs讲成一种历史遗留写法好像分分钟要被淘汰实际上到了1.94版本模块系统的核心规则不但没变反而在工具链完善之后催生出一套更清晰的现代工程实践。这篇内容我不想再复述官方文档而是从文件布局、可见性规则、真实项目拆分三个层面把mod.rs和现代模块组织方式一次性讲透。如果你只是想在项目里快速跑通demo确实感受不到模块设计的好坏可一旦开始维护一组互相调用的业务模块或者想把某个库的公开API与内部实现彻底隔离模块就是你最趁手也是最先翻车的地方。我会尽量少说空话多放可以直接复制的目录结构和代码片段顺带把我在项目里踩过的坑一并交代清楚。1. 为什么到了1.94版本还要重新聊mod.rs1.1 模块系统在Rust进化中的真实位置Rust模块系统从诞生起就带着两个目标按层级组织代码以及把可见性收窄到最小。2015 edition时代官方文档里的标准姿势就是目录加mod.rsCargo项目里根模块放在src/lib.rs或src/main.rs每个子目录里放一个mod.rs作为该目录模块的入口。那个时候模块解析非常直觉化——mod foo;就是在当前目录下找foo/mod.rs没有第二种选择。Rust 2018 edition对路径规则做了一次大统一模块路径一律从crate根部理解不再依赖当前文件位置做相对拼接。这顺带催生了第二种文件布局foo.rs作为foo模块的入口文件foo/目录放foo的子模块文件。官方后来也明确建议新项目优先采用这种布局理由是文件名和模块名对应直观不再需要机械地每层目录创建一个重复的mod.rs。到了1.94版本两种布局在语法层面依然同时可用。rustc不会因为你用哪种布局而改变模块语义唯一的硬性条件是不允许foo.rs和foo/mod.rs同时存在否则立即报E0761。所以与其争论哪种是“正统”不如把选择依据建立在你项目的结构需求上。1.2 当下社区实际怎么用我在不同规模的项目里观察到的规律挺有意思。2018之前启动、后来一路升级的老项目模块树基本还保持着mod.rs形态因为整批迁移目录结构对代码审查和git历史都是一次打扰收益却未必明显。2021之后新写的项目多数直接采用同名文件布局而且rust-analyzer的“创建模块”动作默认生成的就是foo.rs加foo/目录这基本代表了工具链的态度。但“混合派”也不少。比如顶层业务模块用同名文件而plugin/、event/这样聚合语义很强的目录反而保留mod.rs作为对外出入口。回到标题里的1.94版本模块语法层面确实没动刀动的是错误提示的友好程度、rust-analyzer的重构能力以及社区约定。所以这篇文章真正要解决的不是“mod.rs死没死”而是在现代项目里到底什么时候用mod.rs、什么时候用同名文件、以及背后的可见性和路径规则怎么配合。2. mod.rs与同名文件布局选择的工程视角2.1 编译查找规则与目录结构对比当你写下mod foo;时编译器会在包含这条声明语句的文件所在目录里查找模块文件。假设声明在src/main.rs它会按固定顺序尝试查找src/foo.rs查找src/foo/mod.rs两个文件都不存在报E0583file not found for module两个文件同时存在报E0761并且提示你把其中一个移除。这个查找顺序在1.94稳定版里是固定的同名文件优先于mod.rs。这一点有非常实际的影响。如果你原先有foo.rs后来又因为要拆包子模块而创建了foo/目录并且放入了mod.rs但忘了删除foo.rs编译器会直接报错而不是默默选择其中一个。我见过不止一个人因为“我以为它会自动挑一个”在这个错误上浪费不少时间。正确的处理方式很干脆二选一删掉另一个入口。两种布局的目录结构分别长这样。mod.rs布局src/ └── foo/ ├── mod.rs ├── bar.rs └── baz.rs同名文件布局src/ ├── foo.rs └── foo/ ├── bar.rs └── baz.rs两种情况下foo模块内部的子模块声明分别写在mod.rs或foo.rs里// 无论 mod.rs 还是 foo.rs内容一致 pub mod bar; pub mod baz;所以两种布局在语言层面完全等价差异全部落在文件系统组织意图上。2.2 从工程角度比较两种布局给出一张我在内部评审时常用的对比表维度mod.rs布局同名文件布局模块名对应关系目录名是模块名mod.rs本身没有语义文件名即模块名一眼可知目录列表中辨识度多个子目录里全是mod.rs重名率高每个入口文件名不同扫目录更快子模块的相对引用子模块常用super::回到父模块crate::foo::bar路径更直白IDE与重构支持可用但模块级重命名不如文件级顺滑rust-analyzer对文件路径跟踪更好聚合导出场景完美mod.rs天然就是聚合根也能做但语义上稍弱目录仅作归类时多一层无意义入口文件更合适目录只是收纳选择唯一正确的标准应该是目录到底表达“一个模块”还是“一组同级文件”。如果你的目录语义是“这是一个完整的模块内部还有子模块分工”用mod.rs很协调目录本身承担了模块根的职责如果你的目录只是把几个独立文件收拢在一起那个目录本身并不对应任何核心逻辑同名文件布局更舒服。2.3 一个可以照抄的混合布局示例我在实际项目里常用的方式是这样把真正承担“聚合导出”职能的目录保留mod.rs把纯粹的归类目录用同名文件。举个例子src/ ├── main.rs ├── server/ │ ├── mod.rs │ ├── handlers.rs │ ├── routes.rs │ └── middleware.rs ├── utils/ │ ├── time.rs │ ├── logging.rs │ └── parsing.rs这里的server是业务聚合模块外部只跟server打交道它内部整合了handlers、routes、middleware所以server/mod.rs里可以做pub use集中导出对外接口。而utils下面每个文件是独立小工具没有聚合需求直接utils/time.rs这种平铺方式就好。如果你在一个项目里同时使用两种布局我建议收敛成一条简单规则模块根有重导出需求时用mod.rs模块根只是占位时用同名文件。项目里不限制具体哪一种但最好以某一种为主、另一种作为少数特例否则新人刚进仓库会非常困惑。3. 可见性、路径与私有性模块设计的核心规则3.1 树形作用域为什么子模块能访问父模块的私有项很多Rust新手会把模块可见性理解成类似文件的“私有成员”。但Rust规则其实更接近树形作用域一个项的可见范围是从其定义处开始一路向下延伸到所有后代模块。这句话意味着两件反直觉的事子模块可以访问父模块的私有项父模块反过来不能访问子模块的私有项。看代码更直接。下面这段是合法的mod outer { fn secret() - u32 { 42 } mod inner { // 子模块可以访问父模块的私有函数 fn read_secret() - u32 { super::secret() } } }下面这段则编译失败mod outer { mod inner { fn hidden() {} } fn try_call() { inner::hidden(); // 编译错误函数 hidden 在这个作用域不可见 } }这个机制影响深远。你可以在一个模块内部定义仅供自己和子模块协作的私有项完全不用担心被外部看到但如果某个模块希望被父模块或兄弟模块使用就必须显式把可见性往上提。模块树越深这种“顶层可见、底层受控”的梯度越需要精心维护。3.2 pub的四个等级怎么选Rust的可见性修饰符比很多语言细具体看这张表写法可见范围典型使用场景不加修饰当前模块及其所有后代模块内部实现细节子模块可以协作pub(super)父模块及其后代模块只在某个模块分支内部共享pub(crate)整个crate内所有模块库或二进制内部跨模块协作隔离外部pub(in crate::a::b)指定路径及其后代模块极少数精细化控制场景pub对所有外部使用者可见公开API举一个非常普遍的例子。写一个HTTP客户端库时request模块可以被视为完全内部实现但client模块需要调用request::build来组装请求。如果request::build不加修饰client模块根本看不到它如果声明为pub外部使用者也会看到这个内部函数导致API面臃肿。这时用pub(crate)最合适crate内随便调对外完全隐藏将来你把request模块整个改名也不会破坏外部兼容性。3.3 use与pub use真正决定公共API面的操作2018 edition之后模块路径有了统一规则以crate::开头表示绝对路径self::表示当前模块super::表示父模块。外部依赖直接用包名即可。实际开发中最容易踩坑的是2015 edition遗留习惯——在模块内部写use client::Client;而漏掉crate::。1.94版本下只要edition是新版这种写法编译器会直接报错或者给出警告要求你写明use crate::client::Client;。比路径写法更重要的是pub use的重新导出。它本质上是在建立一层facade把内部模块里定义的项提到外层让调用方只面对一个稳定的节点。看一个简化示例// src/lib.rs mod internal_impl; mod public_api; pub use public_api::{build, send, Client, Error as ClientError};这样外部使用者只需要写use my_lib::Client;完全不需要关心Client究竟定义在public_api还是某个更深层的子模块里。好处非常直接内部模块怎么拆、怎么改名对外接口可以保持不变反过来说如果你把所有内部模块都声明成pub外部使用者和编译器都会把每个内部路径当成承诺之后任何重构都可能变成破坏性变更。模块化和mod.rs之争到这里其实已经退居二线了——真正决定API稳定性的是你会不会用pub(crate)和pub use把“内部实现”和“对外契约”切割干净。4. 现代工程中的模块组织从玩具项目到真实代码库4.1 按功能划分而不是按技术层划分Rust没有类模块就是最大的组织单元。很多人从Java或Go转过来会本能地按技术分层建模块controller、service、dao各占一层。这种做法在Rust里非常容易制造大量又薄又空的模块而且依赖方向会被技术类型牵着走最后变成互相纠缠。我建议优先按业务功能或子系统划分模块。举一个模拟HTTP服务项目的模块树例子src/ ├── main.rs ├── client/ │ ├── mod.rs │ ├── config.rs │ └── requests.rs ├── server/ │ ├── mod.rs │ ├── handlers.rs │ └── routes.rs └── common/ ├── mod.rs └── types.rs这里的依赖方向很明确common谁都可以依赖client可以依赖commonserver可以依赖common和client但不能反过来。内部实现中需要跨模块协作的类型用pub(crate)暴露对外一概关进各自的模块根。你不需要告诉新人“controller层在哪”只需要说“用户相关逻辑在user模块里”认知成本会低很多。4.2 测试怎么摆放进模块体系单元测试和集成测试在Rust里遵循完全不同的可见性规则。单元测试写在模块内部的#[cfg(test)]子模块里因为它本质上是当前模块的后代模块所以可以直接访问当前模块的私有项集成测试放在tests/目录下只能访问crate对外公开的API。// src/client/requests.rs fn build_url(base: str, path: str) - String { format!({}/{}, base.trim_end_matches(/), path.trim_start_matches(/)) } #[cfg(test)] mod tests { use super::build_url; #[test] fn test_build_url_joins_with_single_slash() { assert_eq!(build_url(https://example.com, /api/v1), https://example.com/api/v1); } }这里use super::build_url;能生效靠的正是“子模块可以访问父模块私有项”的树形作用域规则。如果你把测试代码全部搬到tests/目录反而访问不到这些内部实现只能通过公开API做黑盒验证。还有个容易忽略的点#[cfg(test)]模块只在cargo test时编译平时不会进入产物。所以你可以放心把大量辅助测试函数放在模块底部不必担心影响生产代码体积。测试类型推荐位置能接触到的代码单元测试模块内#[cfg(test)] mod tests当前模块及其子模块的私有项集成测试tests/目录下独立文件仅crate公开API4.3 特性开关与条件编译模块的搭配在真实工程里我经常用模块配合feature开关来切换实现。比如某个模块在正式环境走真实网络在测试或demo环境走mock可以这样组织// src/client/mod.rs #[cfg(feature mock)] mod mock; #[cfg(not(feature mock))] mod real; #[cfg(feature mock)] pub use mock::Client; #[cfg(not(feature mock))] pub use real::Client;这种做法把“不同实现”隔离在独立模块里Cargo.toml里的feature定义驱动编译期选择。既不会把mock和real的代码混在同一个文件里用if判断也不会让运行时的分支逻辑污染业务代码。模块系统在这里承担了“编译期依赖注入”的职责比在代码里写各种条件分支清晰得多。4.4 工作区与模块的边界划分如果你的项目已经进入了Cargo workspace需要额外理解一层每个工作区成员是一个独立的cratecrate之间只能访问彼此公开的API模块是crate内部的划分方式不能也不应该跨越crate边界。换句话说工作区负责“多包协作”模块负责“包内组织”两者不要混着用。我见过有人试图把一个大模块拆到工作区里让不同成员互相引用内部路径结果就是pub(crate)失效、循环依赖增多、构建顺序难以维护。1.94版本的工作区能力越来越强但模块边界的核心思想没有变crate是发布和依赖的最小单位模块只是私有布局的手段。5. 模块化改造中踩过的坑一次完整的排查记录5.1 把mod.rs改成同名文件后为什么全项目爆红有一次我接手一个老模块出于统一布局的目的把src/dispatch/mod.rs重命名为src/dispatch.rs。操作本身非常简单编辑器一改git一提交看起来没有任何问题。结果打开IDE整个项目飘红大量use crate::dispatch::xxx显示找不到。排查过程是这样的第一步先看编译器的真实报错因为IDE飘红不一定等于编译失败。运行cargo check后发现并没有硬性错误说明问题主要出在rust-analyzer的索引缓存上。第二步重启rust-analyzer重新加载项目飘红就消失了。但还有一类报错是真问题如果在旧布局下有些路径依赖了super::super::button这种深度相对路径改名后子模块的层级虽然没有变可一旦有人顺手调整了目录深度super的数量就会错位。更隐蔽的是2015 edition遗留下来的不带crate::前缀的路径在2021及之后的edition下语义不一样老项目往往会在这时集中爆雷。处理办法不是逐个文件改而是先跑一遍编译器按错误里给出的“help: 建议使用crate::...”逐条批量替换。5.2 模块循环依赖的识别与消除很多人以为Rust模块不能循环引用其实语法层面循环模块引用可以编译问题会体现在逻辑纠缠和类型初始化上。典型场景是error模块定义错误类型handler模块引用它后来handler里又需要根据错误来源反向做一些类型判断导致error模块反过来又依赖handler。代码能编译但每改一处都要在两个模块之间跳来跳去测试也不好写。我处理的方法是先把公共类型抽到第三个“中立”模块比如common/error.rs让handler只依赖common另一个模块也只依赖common。依赖关系收敛成DAG之后每个模块的职责立刻清晰编译报错也少了。模块拆分时多问一句“这个类型到底属于谁”比事后重构省力得多。5.3 过度拆分的代价与很多人想的相反模块不是拆得越细越好。我有一次把大约1500行的一个业务模块按照“一个文件一个核心结构体”的原则拆成三十多个小文件结果看代码时得在文件之间反复横跳公共helper散落得到处都是最终改动反而更贵。Rust编译器的增量编译在文件级确实有一定优化但拆成几十个文件并不会带来可感知的编译速度变化反而增加了认知负担。现在我判断是否要拆出新模块会检查四条标准这部分代码是否需要一个明确的对外接口供其他模块调用是否具备独立的、可以单独测试的功能边界拆分后依赖方向是否保持单向不会引入循环团队里其他人是否能在十秒内说出“这个功能应该去哪个模块找”。如果四条都满足拆如果只有一个模糊的“文件太大”的理由我会先尝试在同一个文件内部用子模块或者分组注释整理而不是直接动目录结构。最后说点个人体会我在多个项目里固定下来的一条准则是项目启动的前两三个月就把模块布局确定下来并且把“以模块名沟通”当成团队习惯。比如宁可说“这个逻辑在server模块里”也不说“在src/server/handlers.rs里”。因为前者关注职责边界后者只是文件路径职责边界才决定依赖方向依赖方向才决定这个项目能不能长期维护下去。mod.rs去留只是表象真正值得反复打磨的是对模块可见性和聚合边界的理解。另外一个小技巧当你准备大规模调整模块目录时先用一次独立的提交完成纯文件移动保证这个提交不混入任何代码逻辑改动。这样git历史会清晰很多评审同事也更容易看出“只是移动了位置”和“顺便改了逻辑”之间的边界。模块化改造别追求一次到位小步提交每个节点都能编译通过比一次大爆炸式迁移稳妥得多。

相关新闻

栈与队列实验三:从基本操作到经典应用,掌握LIFO与FIFO的工程思维

栈与队列实验三:从基本操作到经典应用,掌握LIFO与FIFO的工程思维

1. 栈与队列到底在解决什么问题1.1 从“实验三”这个编号说起但凡计算机相关专业出身的人,看到“实验三”这三个字,脑子里大概都会浮现出一些共同的记忆碎片:实验一通常是线性表,实验二可能是链表,到了实验三&#xff…

2026/10/16 4:39:23 阅读更多 →
AI代码生成实战:提示词工程与生产级质量把控

AI代码生成实战:提示词工程与生产级质量把控

最近小半年,我把工作和业余项目里相当一部分编码任务交给了AI代码生成工具,从最开始“用一下试试”的心态,到后来逐步形成一套固定的工程实践流程。这个转变比我想象中来得快,过程中踩过的坑也不少。我身边有朋友问得最多的问题是…

2026/10/16 4:39:23 阅读更多 →
OpenCode对接中转API的JSON Schema兼容性问题排查与解决

OpenCode对接中转API的JSON Schema兼容性问题排查与解决

OpenCode 这类终端 AI 编程助手火了之后,越来越多的人开始通过中转 API 来降低调用成本、解锁更多模型。但中转 API 和标准 OpenAI 接口之间经常出现 JSON Schema 对不上的情况,轻则解析报错,重则整个会话直接挂掉。这篇文章从问题现象、根因…

2026/10/16 4:39:23 阅读更多 →

最新新闻

咸阳工商招标信息抓取实战:从数据源分析到合规爬虫方案

咸阳工商招标信息抓取实战:从数据源分析到合规爬虫方案

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

2026/10/16 5:30:36 阅读更多 →
kolla-ansible构建可调试OpenStack开发环境实战指南

kolla-ansible构建可调试OpenStack开发环境实战指南

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

2026/10/16 5:30:36 阅读更多 →
MP2642GR-Z与ATmega328协同实现2S锂电精准主动均衡

MP2642GR-Z与ATmega328协同实现2S锂电精准主动均衡

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

2026/10/16 5:30:36 阅读更多 →
生成对抗网络GAN入门:从原理到MNIST实战

生成对抗网络GAN入门:从原理到MNIST实战

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

2026/10/16 5:30:36 阅读更多 →
MathCyclus:构建高中数学循环学习题库的核心设计

MathCyclus:构建高中数学循环学习题库的核心设计

1. 项目概述与设计起点1.1 这个项目到底要解决什么力了不少高中数学题库类产品,说实话,大多数都是把题目堆在一起,按章节分个类就算完事。学生刷题刷得挺爽,但过两周回头再看,同一个知识点换件马甲又不会了。这根本不是…

2026/10/16 5:30:36 阅读更多 →
Python入门必破关:PTA判题机制与输出输入规范解析

Python入门必破关:PTA判题机制与输出输入规范解析

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

2026/10/16 5:29:36 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 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/15 0:14:36 阅读更多 →
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/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练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/15 9:49:14 阅读更多 →