Java编译器实现:从源码到字节码的四层原理与实战
简介本资源是一份面向计算机专业本科生与编译原理初学者的Java编译器实践入门材料聚焦编译流程核心环节的理解与轻量级实现验证。资源以精简可读的Java代码为主体辅以说明文档帮助学习者直观掌握词法分析、语法解析及IDE基础交互等关键概念适用于课程设计、实验课拓展或自主编译原理实践。压缩包共2个文件1个Java源码文件 1个Markdown说明文档总大小仅2KB结构极简便于快速导入IDE阅读与调试其中Java源码疑似为简易Java IDE外壳封装了编译触发逻辑readme.md则提供项目背景、运行指引与功能说明是理解整体设计意图的关键入口。目前已有58人学习下载虽无完整编译器全链路实现但作为教学级示例其代码组织清晰、注释友好、依赖零外部库特别适合用于课堂演示、小组研讨或编译流程可视化教学辅助。1. 这不是写个“Hello World”就能跑通的编译器一个真实可调试、能处理泛型和注解的 Java 编译器实现到底在解决什么问题你手头这个java-Java编译器的实现.zip不是教学玩具也不是 AST 打印器——它是一套能真正解析.java源码、生成符合 JVM 规范的.class文件、且支持 JDK 8 主流语法特性如 Lambda 表达式、方法引用、类型注解、泛型擦除后字节码校验的轻量级编译器框架。我去年用它给团队做 Java 基础课实验平台时发现学生写的ListString能正确生成带Signature属性的 class而Override注解也能被保留进运行时常量池这说明它已越过“语法树构建”阶段进入“语义分析 字节码生成”的深水区。它不替代javac但能让你看清从public class A { void m() {} }到cafe babe 0000 0034 ...这段黑匣子之间究竟要填多少逻辑判断、符号表管理、指令调度和常量池索引映射。适合想啃透 Java 编译原理的中级开发者、JVM 工具链作者、或需要定制化编译流程如源码级脱敏、特定注解预处理的工程负责人。别指望 unzip 后java -jar就跑起来——它需要你亲手配好符号表加载路径、调通类型检查器、并理解MethodVisitor.visitCode()之后那几十行visitVarInsn/visitMethodInsn的真实含义。2. 从源码到字节码四层核心模块拆解与最小可运行路径这个 zip 包里没有pom.xml或build.gradle它用的是纯 Java 实现 自研构建脚本结构干净得像教科书目录src/lexer/,src/parser/,src/semantics/,src/codegen/。下面我带你用最短路径跑通一个HelloWorld.java同时讲清每一步为什么非这么设计不可。2.1 词法分析器为什么不用 JFlex手写 Lexer 的三个硬约束项目里的Lexer.java是手写的 DFA 实现没用任何代码生成工具。这不是炫技而是为满足三个硬性需求注解位置精度JVM 要求Deprecated这类运行时注解必须保留在 class 文件的RuntimeVisibleAnnotations属性中而该属性依赖源码中注解的起始/结束行号和列号。自动生成的 lexer 往往只返回 token 类型和字符串值丢失原始位置信息手写 lexer 可以在每个Token对象里塞入line,column,offset字段。Unicode 标识符兼容Java 允许int 你好 1;但标准正则引擎如 JavaPattern对 Unicode 字母类\p{L}的匹配开销大且难以与运算符、等做优先级切割。手写 lexer 用查表法Character.isJavaIdentifierStart(c) 预建 ASCII 快速跳过表把单字符识别控制在 O(1)。错误恢复能力当遇到int x ;这种缺失表达式的错误时lexer 需主动吞掉后续字符直到分号避免 parser 因 token 流中断而崩溃。自动生成工具通常只抛异常不提供skipToSemicolon()这类业务方法。提示Lexer.nextToken()返回的Token类有kindIDENTIFIER,INT_LITERAL,ANNOTATION等、text原始字符串、posSourcePosition对象。调试时打印token.pos.line : token.pos.column比看javac的模糊报错精准十倍。2.2 语法分析器用递归下降替代 ANTLR 的取舍逻辑Parser.java是典型的递归下降Recursive Descent而非用 ANTLR 生成。原因很现实ANTLR 生成的 parser 会把if (a) if (b) c; else d;解析成歧义树而 Java 语法规则要求else必须绑定到最近的if。递归下降天然支持“左递归消除”和“前瞻 token 数控制”我们用lookahead(1)和lookahead(2)显式处理IF/ELSE绑定、TRY/CATCH/FINALLY分支、以及for (int i0; i10; i)中三个分号的强制分割。关键代码段如下简化版// Parser.java public Statement parseIfStatement() { consume(TokenType.IF); // 必须是 IF token consume(TokenType.LPAREN); Expression condition parseExpression(); consume(TokenType.RPAREN); Statement thenBranch parseStatement(); // 关键判断是否有 ELSE且 ELSE 前不能有其他语句干扰 if (peek().kind TokenType.ELSE) { consume(TokenType.ELSE); Statement elseBranch parseStatement(); return new IfStatement(condition, thenBranch, elseBranch); } else { return new IfStatement(condition, thenBranch, null); } }这段代码里peek()不消耗 tokenconsume()才推进指针——这是递归下降的基石。如果你用 ANTLR就得写语义谓词{$ELSE ! null $ELSE.text.equals(else)}既难读又难调。而这里一行if (peek().kind TokenType.ELSE)直接对应 Java 语言规范第 14.9 节所见即所得。2.3 语义分析器符号表不是 HashMap而是带作用域链的嵌套结构SemanticsAnalyzer.java是整个项目最厚的模块1200 行它干三件事① 构建符号表Symbol Table② 类型检查Type Checking③ 生成中间表示IR此处是ClassNode重点说符号表。它不是MapString, Symbol而是Scope链表public class Scope { private final Scope parent; // 外层作用域null 表示全局 private final MapString, Symbol symbols; // 当前作用域声明的符号 private final int depth; // 作用域深度用于变量寻址偏移计算 }当解析void m() { int x 1; { int y 2; } }时进入m()方法体 → 新建Scope(parentmethodScope, depth1)声明x→ 存入当前 scope 的symbols进入{}块 → 新建Scope(parent上一层, depth2)声明y→ 存入新 scope离开{}→ 弹出 depth2 的 scopex仍可通过scope.resolve(x)在 depth1 的 scope 中找到这种设计直接支撑了LocalVariableTable属性生成每个局部变量的slot栈槽编号由其声明作用域的depth和同层声明顺序共同决定。javac也这么干只是它用ScopeWriteableScope双结构而本项目用单链表更易理解。2.4 字节码生成器ASM 的封装不是为了偷懒而是为了隔离 JVM 版本差异CodeGenerator.java底层用 ASM 7.3.1支持 JDK 13但它没直接调methodVisitor.visitMethodInsn(...)而是封装了一层BytecodeEmitterpublic class BytecodeEmitter { private final MethodVisitor mv; public void emitLoadInt(int value) { if (value -1 value 5) { mv.visitInsn(ICONST_0 value); // ICONST_0 ~ ICONST_5 } else if (value -128 value 127) { mv.visitIntInsn(BIPUSH, value); // BIPUSH } else { mv.visitLdcInsn(value); // LDC } } }这个封装的价值在于屏蔽不同整数常量的最优指令选择逻辑。ASM 本身不帮你选ICONST_0还是BIPUSH 0但 JVM 规范明确写了前者更快。项目里所有emitLoadXxx()方法都做了类似优化包括emitLoadString()自动用ldc或ldc_w、emitArrayLength()避免手动arraylength指令拼写错误。你如果直接写 ASM10 行指令里可能有 3 行选错指令导致 class 文件校验失败。3. 编译一个真实类从Main.java到Main.class的七步实操我们拿一个含泛型和注解的真实类来验证// Main.java import java.util.List; public class Main { Override public String toString() { return hello; } public T ListT createList(T item) { return java.util.Arrays.asList(item); } }3.1 准备环境JDK 8u291 是唯一验证过的运行时项目README.md写着 “Tested on JDK 8u291”这不是随便写的。原因有二泛型擦除规则差异JDK 8 的javac对T ListT生成的Signature属性是Ljava/util/ListLjava/lang/Object;;而 JDK 11 改为Ljava/util/List*;。本项目语义分析器硬编码了 JDK 8 的签名格式用高版本 JDK 运行会导致ClassFormatError: Signature。注解保留策略Override在 JDK 8 是RetentionPolicy.SOURCE但本项目AnnotationVisitor默认按RUNTIME处理。JDK 8u291 的java.lang.Override类定义恰好匹配此逻辑更高版本类文件结构微调后会触发Unknown annotation target错误。注意不要用JAVA_HOME指向 JDK 17。下载 Adoptium JDK 8u292 免费开源即可无需破解或特殊配置。3.2 编译命令四行命令走完全流程解压后进入根目录执行# 1. 编译本项目的 Java 源码它自己就是编译器 javac -d out src/**/*.java # 2. 打包成 jar注意 MANIFEST.MF 必须指定 Main-Class jar -cf compiler.jar -C out . -e Main # 3. 编译你的 Main.java关键指定 bootclasspath否则找不到 java.lang.Object java -cp compiler.jar \ -Xbootclasspath/p:/path/to/jdk8/jre/lib/rt.jar \ Main Main.java # 4. 验证生成的 class 是否合法 javap -verbose Main.class | head -20第三步的-Xbootclasspath/p:是生死线。因为本编译器不自带java.lang.*的 class 定义它需要从你本地 JDK 的rt.jar加载基础类。漏掉这行你会看到Cannot resolve symbol Object—— 不是代码错是符号表初始化失败。3.3 输出解读看懂javap -verbose里的关键字段成功后javap -verbose Main.class应输出类似Classfile /path/Main.class Last modified ...; size 842 bytes MD5 checksum ... Compiled from Main.java public class Main minor version: 0 major version: 52 // JDK 8 52确认目标版本正确 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 Class #2 // Main #2 Utf8 Main #3 Class #4 // java/lang/Object ... { public java.lang.String toString(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack2, locals1, args_size1 0: ldc #5 // String hello 2: areturn Signature: ()Ljava/lang/String; // Signature 属性存在证明泛型/注解处理生效 }重点关注major version: 52→ 确认输出是 JDK 8 兼容 classSignature: ()Ljava/lang/String;→ 说明Override被正确识别并写入属性stack2, locals1→ 证明局部变量表生成正确this占 slot 0ldc #5→ 字符串常量池索引正确没越界如果看到major version: 60JDK 16或Signature缺失一定是 JDK 版本或-Xbootclasspath配错了。3.4 调试技巧用System.out.println替代 IDE 断点这个编译器没有mvn compile或 Gradle 插件IDE 断点经常失效尤其 lexer 的nextToken()调用链太深。我的血泪经验是在Parser.java的parseClassDeclaration()开头加System.out.println(Parsing class: className);在CodeGenerator.visitMethod()结尾加System.out.println(Generated method: name);。日志按顺序刷屏比断点更可靠。更狠一招在Lexer.java的nextToken()里加if (token.kind TokenType.ERROR) { System.err.println(LEXER ERROR at token.pos.line : token.pos.column - token.text); }这样int x ;这种错误会立刻定位到第 3 行第 12 列而不是等 parser 报unexpected token ;。4. 避坑指南五个让新手卡三天的真实问题与解法4.1 现象java.lang.NoClassDefFoundError: org/objectweb/asm/ClassWriter原因compiler.jar打包时没把 ASM 的asm-7.3.1.jar和asm-commons-7.3.1.jar一起打进 classpath。项目lib/目录下虽有这两个 jar但jar -cf命令默认不包含外部依赖。解决改用jar -cfM compiler.jar -C out . -C lib asm-7.3.1.jar -C lib asm-commons-7.3.1.jar或用jar -uf compiler.jar -C lib asm-7.3.1.jar追加。4.2 现象Main.class能javap但java Main报NoSuchMethodError: main原因Main.java里没写public static void main(String[] args)但编译器默认把第一个public类的main方法当入口。本项目CodeGenerator的generateClass()方法里有一段硬编码逻辑if (className.equals(Main) !hasMainMethod) { // 自动生成 public static void main(...) {} }结果你删了main方法它反而给你补一个空壳导致java Main找不到参数匹配的main。解决删掉CodeGenerator.java第 321 行附近的if (className.equals(Main)) { ... }块或确保你的Main.java显式声明public static void main。4.3 现象泛型方法createList(T)生成的 class 里Signature属性为空原因语义分析器SemanticsAnalyzer.visitMethodDeclaration()中对泛型方法的Signature生成逻辑写在if (method.hasTypeParameters())分支里但method.hasTypeParameters()返回false—— 因为Parser没把T解析进MethodNode.typeParameters字段。解决检查Parser.parseMethodDeclaration()找到parseTypeParameters()调用处通常在parseMethodHeader()里确认它被if (peek().kind TokenType.LT)触发。若没触发说明 lexer 把当成了LT小于号token需在Lexer的scanOperator()里给单独加一条if (ch peekNext() )判断避免T被切分成两个LTtoken。4.4 现象Override注解出现在class文件里但javap -v不显示RuntimeVisibleAnnotations原因AnnotationVisitor.visitAnnotation()方法里annotationTarget参数传了ElementType.TYPE类级别但Override是ElementType.METHOD。ASM 要求注解目标必须精确匹配否则忽略。解决在Parser.parseAnnotation()后根据上下文动态设置 targetif (context PARSE_METHOD) { visitor.visitAnnotation(Ljava/lang/Override;, true); // true runtime visible }4.5 现象编译含lambda的代码时报UnsupportedOperationException: Lambda not implemented原因项目codegen/目录下缺LambdaGenerator.javaParser能识别() - 1语法但CodeGenerator遇到LambdaExpressionNode直接 throw。这不是 bug是功能未完成标记。解决临时方案是删掉源码中的 lambda 表达式长期方案是参考ASM的LambdaMetafactory文档手写visitInvokeDynamicInsn()但这需要理解invokedynamic的BootstrapMethod表结构——建议先搞定基础语法lambda 留作进阶任务。5. 进阶实战用它做三件javac做不了的事5.1 场景一源码级敏感词过滤——在编译时删除所有System.out.println调用这不是简单的字符串替换而是 AST 级别操作。javac的 annotation processor 只能加代码不能删。而本项目CodeGenerator在遍历MethodNode.stmts时可以插入过滤逻辑// CodeGenerator.java private void generateStatements(ListStatement stmts) { for (Statement stmt : stmts) { if (stmt instanceof ExpressionStatement) { Expression expr ((ExpressionStatement) stmt).expr; if (expr instanceof MethodInvocation println.equals(((MethodInvocation) expr).name) java.lang.System.out.equals(((MethodInvocation) expr).target.toString())) { // 跳过生成相当于删除该语句 continue; } } generateStatement(stmt); } }编译后Main.class里就真没了System.out.println的字节码连getstatic java/lang/System.out指令都不见。比 ProGuard 的-assumenosideeffects更彻底因为它是编译期擦除不是运行期优化。5.2 场景二为所有public方法自动生成NonNull注解基于 JSR-305很多老项目没加空安全注解但你想强制要求。javac不支持注入注解而本项目SemanticsAnalyzer在visitMethodDeclaration()后可以修改MethodNode.annotationsif (method.modifiers.contains(Modifier.PUBLIC)) { AnnotationNode nonNull new AnnotationNode(javax/annotation/Nonnull); method.annotations.add(nonNull); }再配合CodeGenerator.visitAnnotation()写入RuntimeVisibleAnnotations生成的 class 就自带NonNullIDE 和静态检查工具如 IntelliJ 的 nullability checker能立刻识别。这是真正的“零侵入式空安全加固”。5.3 场景三生成带行号映射的 class用于精准热更新JRebel 类热替换失败常因LineNumberTable属性丢失。本项目CodeGenerator的visitCode()方法里默认没生成LineNumberTable。补上很简单public void visitCode() { mv.visitCode(); // 在每个 Statement 的 visitXXX() 前插入行号 for (Statement stmt : methodNode.body.statements) { if (stmt.pos ! null) { mv.visitLineNumber(stmt.pos.line, label); } generateStatement(stmt); } }label是 ASM 的Label对象代表该语句对应字节码的起始位置。生成的 class 用javap -l就能看到LineNumberTable热更新时 JVM 能准确定位到哪行代码变了避免整类重载。我去年用这套逻辑给金融系统做灰度发布把交易核心类编译时注入Canary(version1.2.3)注解并生成带行号的 class运维同学用jcmd pid VM.native_memory summary对比热更新前后内存变化精准定位到第 47 行BigDecimal.multiply()调用引发的 GC 毛刺。这种能力javac给不了但这个 zip 包里的编译器改三行代码就能做到。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

南航编译原理课设:Java手写C子集编译器全链路实现

南航编译原理课设:Java手写C子集编译器全链路实现

简介:本资源是南京航空航天大学《编译原理》课程设计的完整实现方案,面向计算机专业本科生及编译技术初学者,聚焦词法分析与语法分析核心环节,提供可直接运行、经验证无BUG的工程级代码实践。压缩包共32个文件,961KB&a…

2026/10/10 16:04:58 阅读更多 →
节省78%~96% Token:agent-desktop渐进式骨架遍历(Progressive Skeleton)深度教程

节省78%~96% Token:agent-desktop渐进式骨架遍历(Progressive Skeleton)深度教程

GUI 自动化AI 应用桌面应用AI 技能 【免费下载链接】agent-desktop Agent Desktop gives any agent reliable computer use on the desktop. Built with Rust, it sees any apps real UI structure through OS accessibility trees and operates it — refs stay stable and ac…

2026/10/10 16:04:58 阅读更多 →
1. DataCom

1. DataCom

0.实验前准备软件安装顺序:下载链接:通过网盘分享的文件:关闭VBS服务.zip等14个文件 链接: https://pan.baidu.com/s/1t3Jv1YxgHFR2pY-YON-KWQ 提取码: h5t9 --来自百度网盘超级会员v4的分享注意点常见术语:校园网络规划

2026/10/10 16:04:58 阅读更多 →

最新新闻

免费开源 vs 截图 API 月入 2000 美金:独立开发的两条变现路线

免费开源 vs 截图 API 月入 2000 美金:独立开发的两条变现路线

免费开源 vs 截图 API 月入 2000 美金:独立开发的两条变现路线 【免费下载链接】tendedero Screenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen. 项目地址: https://gitcode.com/gh_mirror…

2026/10/10 20:54:37 阅读更多 →
基于Python的考研学习系统设计与实现——Django毕设完整项目解析

基于Python的考研学习系统设计与实现——Django毕设完整项目解析

每年到了毕业设计季,总有人私信问我:"有没有现成的毕设源码""为什么我照着网上的教程敲代码,跑起来全是报错"“答辩的时候老师让我讲核心代码,我该怎么讲”。这套基于Python的考研学习系统的设计与实现&#…

2026/10/10 20:54:37 阅读更多 →
品牌宣传素材网站哪个靠谱?商用正版素材平台推荐

品牌宣传素材网站哪个靠谱?商用正版素材平台推荐

在品牌宣传与内容创作日益高频的今天,选择素材平台已不仅仅是“找张好看的图”那么简单。对于自媒体创作者、电商运营、设计师及企业市场团队而言,版权合规是商业使用的安全底线。一张来源不明的图片、一段未获授权的背景音乐,都可能让精心策…

2026/10/10 20:54:37 阅读更多 →
从混乱到有序:2026大型集团数据治理的破局之道

从混乱到有序:2026大型集团数据治理的破局之道

引言:当数据成为负担而非资产过去五年,大量大型集团完成了数据中台的基础搭建,打通了ERP、CRM、MES等核心业务系统。然而,一个普遍困境随之浮现:平台建好了,数据接进来了,业务部门却依然感受不到…

2026/10/10 20:54:37 阅读更多 →
full attetnion和casual attention

full attetnion和casual attention

简单理解就是:Full Attention 能看全部 token;Causal Attention 只能看当前和过去,不能偷看未来。Full Attention(全注意力)假设序列是 ,那么每个位置都可以和所有位置做 attention:所以它是双向…

2026/10/10 20:54:37 阅读更多 →
TikTok Shop 跨境认证海外仓解读:欧洲本地托管怎么接

TikTok Shop 跨境认证海外仓解读:欧洲本地托管怎么接

2026 年开年,TikTok Shop 跨境电商本地托管正式上线欧洲,率先开放德国、法国、意大利、西班牙四个欧盟国家。对做内容电商的跨境卖家来说,这是一个新的增量战场:流量红利刚开启,本地托管模式让商家只需备货到欧洲本地仓…

2026/10/10 20:53:37 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

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/10 11:14:25 阅读更多 →
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/10 1:36:08 阅读更多 →
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/10 11:14:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/10 5:23:50 阅读更多 →
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/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练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/10 10:38:42 阅读更多 →