手写区块链核心逻辑:Python实现工作量证明与防篡改机制
从零手写区块链听起来像是个大工程但真动手做一遍你会发现核心逻辑其实并不复杂。这篇文章我打算用Python带你一步步搭一个最小可用的区块链包含区块结构、工作量证明PoW、链的校验这些关键环节全程代码可运行。适合那些看过不少区块链概念、但一直没真正写过代码的开发者也适合刚学完Python基础、想找个有挑战性的练手项目的朋友。我先说结论一个教学用的区块链核心代码量不会超过两百行。难点不在于写代码而在于理解为什么区块要这样设计、哈希在其中扮演什么角色、工作量证明到底在防什么。这篇文章会把这些“为什么”讲清楚再配上能直接跑起来的代码。1. 项目整体设计这到底是在做一个什么东西1.1 先把目标定清楚这个项目不是要做一个生产级的区块链系统那涉及P2P网络、分布式共识、加密签名、存储引擎工作量是另一个量级。我们的目标是在单机上用Python实现一条区块链的核心数据结构和共识逻辑让它具备区块链最本质的三个特征——可追溯、防篡改、需要付出成本才能记账。说得更具体一点完成这个项目后你应该能回答这几个问题一个区块里到底存了哪些字段每个字段是干嘛的为什么说“一旦写入就难以篡改”篡改一个区块会引发什么连锁反应挖矿的“工作量”体现在哪为什么需要改一个随机数反复计算哈希如何验证一条链是健康完整的把这些问题想透了再去接触真实区块链项目比如比特币源码、各类联盟链框架你会觉得亲切得多。因为不管系统多复杂底层那套“区块哈希链式结构共识代价”的骨架是相通的。1.2 为什么选Python而不是Go、Java选Python的理由很直接表达力强、上手快能把注意力放在区块链本身的逻辑上而不是跟语言特性较劲。标准库的hashlib直接提供SHA-256不需要引第三方包。json模块做序列化很方便哈希计算需要的是稳定、可重现的字符串输入JSON天然合适。动态类型让代码写起来很流畅区块、链用类或者字典都能表达灵活度大。当然Python不是性能最优的选择真实区块链几乎没有用纯Python写的。但作为教学和验证思路Python完全够用。我之前也考虑过用Go写并发模型确实适合区块链的节点通信场景但对刚入门的朋友来说Go的工程复杂度容易喧宾夺主。我们的目标是用最少的代码、最短的路径把区块链的主干逻辑跑通那就果断用Python。1.3 技术选型上的一些考虑哈希算法选了SHA-256这是业界最常用的选择。它输出固定长度的256位摘要具备两个关键特性单向性给你一个哈希值你几乎不可能反推出原始输入。雪崩效应输入哪怕只改一个字符输出的哈希值会面目全非。这两个特性是区块链防篡改的第一道防线。后面你会看到区块中任何字段被改动都会导致该区块的哈希变化进而让整条链的校验失败。数据结构上我直接用一个Python列表来存区块。列表天然有序正好对应区块链“链式追加”的特点。真实系统中会用更复杂的存储结构但教学场景下列表是最直观的。数据格式上用JSON序列化区块内容。这里有一个容易踩的坑Python字典的键顺序不固定直接对字典进行json.dumps不同运行环境下键的顺序可能不一样导致同样的内容算出不同的哈希。解决办法是json.dumps时加上sort_keysTrue保证序列化结果是确定性的。这个小细节可以在后面的代码里看到。2. 核心原理解剖区块、哈希与工作量证明2.1 一个区块到底长什么样一个区块就是一个数据容器。我设计的每个区块包含以下字段字段含义作用index区块在链中的位置高度保证顺序timestamp区块生成时间记录时间信息data区块存储的业务数据实际内容比如交易记录previous_hash上一个区块的哈希把区块串成链的“链环”nonce随机数工作量证明的变量hash当前区块的哈希当前区块所有内容的摘要理解区块链关键在于previous_hash和hash这一对字段。每个区块的哈希是把当前区块所有字段除了hash自己打包后做SHA-256得到的。而previous_hash指向了上一个区块的哈希。这样一来每个区块就像手拉手的一串人后面的人紧紧抓着前面人的手。如果中间某个区块的数据被篡改了它的哈希会变那么所有后续区块里记录的“上一个哈希”就对不上了。这种连锁反应就是区块链防篡改的本质。创世区块第一个区块比较特殊它没有前驱previous_hash我一般用一个全零字符串0表示。2.2 哈希在这里到底起了什么作用有人可能会问我直接把整个区块内容存下来不也一样吗为什么非得整个哈希三个原因第一哈希是身份的浓缩。区块内容可能很大但哈希只有固定长度。寻找一个区块时拿着哈希比对效率高得多。第二哈希是校验的币。给别人校验链的时候不需要传完整内容传哈希就能验证数据有没有被动过。第三哈希是共识的载体。后面讲工作量证明你就会看到我们需要找到一个满足特定条件的哈希值。哈希的不可预测性保证了这个“找”的过程必须付出真实的计算成本。用个生活类比哈希就像一个人的身份证号。你说你是某个人我只要比对身份证号就能确认。如果你整容了、改名字了身份证号就变了那所有关联的记录立刻能发现不对劲。2.3 工作量证明让记账这件事变贵在区块链里不允许任何人随随便便就添加一个区块否则攻击者可以瞬间写出几百万个伪造区块把链撑爆。所以需要一种机制添加区块要付出代价。工作量证明的经典实现是这样的系统设定一个目标新区块的哈希值必须以若干个零开头。因为哈希结果无法预测矿工只能不断改变区块里的nonce值重新计算哈希直到碰运气撞出满足条件的哈希。一旦找到广播给全网其他人验证一下哈希确实满足条件就承认这个区块合法。目标里前导零的个数决定了难度。每多一个零难度变成原来的16倍因为一位十六进制数有16种可能。我用4个前导零做默认难度平均需要计算16^4 65536次才能挖出一个区块。这个数字在单机上需要跑几百毫秒演示起来很有“挖矿感”又不会等太久。为什么这种机制能防攻击因为攻击者要想篡改区块然后重新挖矿需要把后面所有区块全部重新挖一遍计算成本随时间线性增长。而诚实的节点只需要在原有链上继续挖新块。算力不足的话攻击者的链永远赶不上诚实的链。这就是所谓的“算力为王”。2.4 难度值怎么定才合适挖矿难度不是拍脑袋定的要考虑演示场景的实际体验。我测试过不同难度的时间表现难度前导零个数平均尝试次数单块挖矿耗时普通电脑34096约30毫秒465536约400毫秒51048576约6秒616777216约90秒这个项目我用4节奏舒服既能让人明显感受到“挖矿需要等待”又不会等得无聊。你可以自行调整difficulty参数感受一下难度和耗时的指数关系。3. 实操从零手写区块链3.1 环境准备只需要一个Python环境我推荐3.8及以上版本。全程只用标准库不需要安装任何第三方依赖。用到的模块hashlib计算SHA-256json序列化区块内容time生成时间戳代码组织上我建议分成三个文件block.py区块类定义blockchain.py区块链类定义main.py演示和测试在开发过程中遇到不确定的地方就把中间量打印出来看。这一步做对了后面debug能省不少力气。3.2 定义区块结构# block.py import hashlib import json class Block: def __init__(self, index, timestamp, data, previous_hash, nonce0): self.index index self.timestamp timestamp self.data data self.previous_hash previous_hash self.nonce nonce self.hash self.compute_hash() def compute_hash(self): block_string json.dumps({ index: self.index, timestamp: self.timestamp, data: self.data, previous_hash: self.previous_hash, nonce: self.nonce }, sort_keysTrue).encode() return hashlib.sha256(block_string).hexdigest()几个关键点__init__里先默认nonce0算出一个初始哈希。挖矿过程中会不断改nonce再重新算哈希。compute_hash是幂等的方法给定区块内容输出永远一致。这保证了验证的时候可以随时重算。sort_keysTrue必须加。如果不加同样的区块内容在不同解释器下可能得到不同的哈希。3.3 实现区块链与工作量证明# blockchain.py import time from block import Block class Blockchain: def __init__(self): self.chain [] self.difficulty 4 self.create_genesis_block() def create_genesis_block(self): genesis Block(0, time.time(), Genesis Block, 0) self.chain.append(genesis) def get_latest_block(self): return self.chain[-1] def proof_of_work(self, block): target 0 * self.difficulty while not block.hash.startswith(target): block.nonce 1 block.hash block.compute_hash() return block def add_block(self, data): latest self.get_latest_block() new_block Block( indexlatest.index 1, timestamptime.time(), datadata, previous_hashlatest.hash ) mined_block self.proof_of_work(new_block) self.chain.append(mined_block) print(f第 {mined_block.index} 个区块已挖出nonce 为 {mined_block.nonce})这里有几个设计决策创世区块用0作为previous_hash。它是链的起点没有前驱这个全零字符串是行业里常见的惯例写法。proof_of_work直接修改传入的block对象。因为在前面已经通过compute_hash算过初始哈希这里循环判断它是否满足目标条件不满足就递增nonce重算。这个循环就是“挖矿”的本质。add_block方法把流程串起来取最新区块作为前驱、创建新块、挖矿、入链。3.4 验证链的完整性# blockchain.py 内继续添加 def is_valid(self): for i in range(1, len(self.chain)): current self.chain[i] previous self.chain[i - 1] if current.hash ! current.compute_hash(): print(f区块 {current.index} 的内容被篡改哈希不一致) return False if current.previous_hash ! previous.hash: print(f区块 {current.index} 的前哈希与区块 {previous.index} 不匹配) return False return True验证逻辑就两条当前区块的哈希必须等于把当前区块内容重算得到的结果。如果有人改了data、timestamp、nonce任意一个字段重算出来的哈希立刻对不上。当前区块记录的previous_hash必须等于上一个区块的哈希。这保证了链的连续性。两者都通过说明整条链自洽完整。3.5 数据封装与演示入口# main.py import time from blockchain import Blockchain def main(): b Blockchain() data_list [ A 转给 B 10元, B 转给 C 5元, C 转给 A 3元 ] for data in data_list: b.add_block(data) time.sleep(0.5) # 避免多个区块时间戳完全相同 print(当前链长度:, len(b.chain)) for block in b.chain: print(f索引 {block.index}, 数据: {block.data}, 哈希: {block.hash}) print(链完整性校验:, b.is_valid()) if __name__ __main__: main()这里加了一个time.sleep(0.5)是一个小细节区块链本身不依赖时间戳排序我们用index排序但时间戳完全相同的相邻区块看起来会有点奇怪观感不佳。实测间隔0.5秒就够了。3.6 完整项目代码结构python-blockchain/ ├── block.py ├── blockchain.py └── main.py三个文件加起来不到150行逻辑非常清晰。你完全可以合并成一个文件不过分文件组织更贴近真实工程的习惯也方便后续扩展。4. 运行测试看看区块链是怎么工作的4.1 正常运行的输出在终端执行python main.py类似的输出第 1 个区块已挖出nonce 为 2847 第 2 个区块已挖出nonce 为 563 第 3 个区块已挖出nonce 为 12341 当前链长度: 4 索引 0, 数据: Genesis Block, 哈希: 5d8a5f2b9f2c3a1e... 索引 1, 数据: A 转给 B 10元, 哈希: 00ab31f2cfe1a2d4... 索引 2, 数据: B 转给 C 5元, 哈希: 0045bf12e0aa33c1... 索引 3, 数据: C 转给 A 3元, 哈希: 0007d9a12b5c8f21...注意观察创世区块的哈希不是以00开头的而后面挖出来的区块哈希都以至少4个零开头。这说明创世区块是系统直接初始化的不需要经过工作量证明而其他新区块都经过了挖矿确认。每次运行的nonce会完全不同因为时间戳在变哈希碰撞的结果也在变。这是正常的别担心。4.2 篡改测试区块链的报警机制光看运行输出还不够我们来做一个关键实验人为篡改一个区块然后调用is_valid看看会发生什么。# tamper_test.py from blockchain import Blockchain b Blockchain() b.add_block(第一条交易) b.add_block(第二条交易) print(篡改前校验结果:, b.is_valid()) # 试图篡改第一个数据区块的内容 b.chain[1].data 篡改后的交易记录 print(篡改后校验结果:, b.is_valid())运行结果篡改前校验结果: True 第 1 个区块的内容被篡改哈希不一致 篡改后校验结果: False这个实验直观地演示了区块链防篡改的精髓。只改了data一个字段compute_hash的结果就完全不同了校验逻辑立刻抓到异常。而且后续所有区块也都会跟着校验失败因为它们的previous_hash指向的是旧哈希跟新算出来的对不上。这给我们的启示是区块链防的不是“不能改”而是“改了就会被发现”。在共识网络里一旦发现假链整个网络会拒绝它作恶者丢掉的不仅是信誉还有投入的算力和时间。4.3 尝试调整难度感受共识的成本把blockchain.py里的difficulty改成5再运行一次。你能明显感觉到时间变长了每个区块的nonce数值也大得多。这正好验证了前面说的指数关系难度从4升到5只增加了一个前导零但平均尝试次数从6万多次飙升到100多万次。为什么要这样设计因为哈希结果不可预知谁都无法“抄近路”只好老老实实穷举。这就确保了记账权的获取成本是真实存在的。5. 常见问题与排查实录5.1 常见问题速查表问题表现排查方向哈希一直不满足条件挖矿死循环程序卡死检查difficulty是否太大检查nonce是否正确递增两个区块哈希一样校验失败或者数据混乱检查timestamp是否相同检查序列化是否用了sort_keys校验总是不通过is_valid始终返回False检查previous_hash是否正确传递检查操作的是不是同一个区块对象挖矿速度太慢每个区块要等几十秒降低difficulty确认没有在循环里意外重置nonce数据里有中文报编码错误字符串encode时报错Python 3默认UTF-8一般不会确认终端编码正常5.2 我踩过的几个坑第一个坑json序列化时键顺序不稳定。刚开始我直接对dict做json.dumps在自己的电脑上没问题换个环境跑就出现哈希不一致。排查半天发现是键顺序变了。sort_keysTrue一出马问题立刻解决。这个教训告诉我凡是涉及哈希计算的序列化输入必须是确定性的。第二个坑修改了nonce却忘了重算hash。写第一版挖矿循环的时候我天真地以为改完noncehash会自动更新。结果当然不是区块永远满足不了条件。调试的时候我在循环里打印了block.hash发现从头到尾没变过才意识到要重新调用compute_hash赋值。第三个坑测试时把sleep加错了地方。我一开始把sleep放在挖矿循环里结果每个nonce都等0.1秒挖一个区块要等一个多小时。后来才醒悟sleep只是为了隔开时间戳应该放在每次交易之间而不是放在挖矿循环里。5.3 对初学者的一些建议如果你照着代码敲完发现自己卡在某个地方我建议按这个顺序排查先确认compute_hash能正常算出64位十六进制字符串。打印创世区块的哈希确认它不以00开头这是正常的。单独调proof_of_work挖一个区块观察nonce的变化。最后再跑整体流程。还有个小技巧把difficulty先设为2跑通整个流程看到效果后再调回4或5。难度2几乎瞬间出块适合先验证流程确认没问题再体验真正的挖矿。5.4 后续可以怎么扩展这个最小区块链跑通之后往上加东西的思路就很清晰了添加交易池data里面不再直接存一条记录而是存一个交易列表。实现地址与签名用非对称加密生成地址交易要附上签名这是真实区块链的标配。P2P网络用WebSocket或HTTP接口让多个节点同步链数据。持久化把链存储到SQLite或文件里重启程序后仍然存在。动态难度根据最近区块的挖矿时长自动调节difficulty维持稳定的出块速率。每一步都值得单独写一篇实战记录编程的乐趣就在于把最小模型一点点扩展成接近真实的东西。写在最后的体会把这段代码跑通的那一刻我对区块链的理解有了一个质的变化。以前看各种科普文章知道“区块链是不可篡改的账本”这句话但不知道为什么不可篡改。真正自己写了校验逻辑、亲手改坏一个区块看到报错提示才彻底明白所谓不可篡改其实是“篡改会留下痕迹、需要付出高昂代价”。我建议你也亲手把某个区块的数据改掉然后看着is_valid返回False这种体验比读十篇概念文章都来得深刻。最后分享一个小技巧把整个项目推到一个代码仓库里按一次提交一个功能节点的节奏提交代码从“定义区块结构”到“实现挖矿”再到“添加校验”每个阶段的提交信息都写清楚。过一个月再回来看这些提交你能清晰地看到自己当时是怎么一步步把区块链这个抽象概念落成代码的那种感觉本身就是学习编程最珍贵的东西。

相关新闻

用Python从零写一个区块链:核心原理与代码实践

用Python从零写一个区块链:核心原理与代码实践

1. 项目拆解:一个区块链到底需要什么先说说我为什么想写一个自己的区块链。很多人觉得区块链是那种高不可攀的底层技术,一上来就去看白皮书、啃共识算法,结果被各种术语劝退。但如果你愿意静下心,用自己最熟悉的语言——比如Pytho…

2026/10/13 18:58:02 阅读更多 →
VAR模型与Stata实操:从时间序列分析到论文实证

VAR模型与Stata实操:从时间序列分析到论文实证

1. VAR模型入门:为什么论文里它如此常见最近几年,经济金融类论文里出现频率最高的计量模型,向量自回归(VAR)绝对算一个。不管是宏观政策效应评估、股价与宏观变量的动态关系,还是行业间的溢出效应&#xff…

2026/10/13 18:57:02 阅读更多 →
C++全量编译从40分钟到5分钟:ccache与sccache缓存加速实战

C++全量编译从40分钟到5分钟:ccache与sccache缓存加速实战

刚接手这个C项目的时候,最让我头疼的倒不是代码历史包袱,而是全量编译一次要等足足四十分钟。项目体量不算夸张,真实业务代码也就几百个编译单元,但牵扯到Protobuf、Boost这类重型依赖,每次clean build都像一次长途旅行…

2026/10/13 18:57:02 阅读更多 →

最新新闻

互联网大厂薪酬结构全解析:总包、级别、绩效与谈薪技巧

互联网大厂薪酬结构全解析:总包、级别、绩效与谈薪技巧

1. 先说结论:今年这个薪酬盘子到底什么情况每年这个节点,总有人拿着某头部互联网平台的薪酬截图或者 Offer 在各个群里问“这个数正常吗”。说实话,我在这一行摸爬滚打了十几年,带过团队、做过招聘、也背过绩效,今年这…

2026/10/13 19:37:19 阅读更多 →
LeNet实战:训练MNIST手写数字识别并测试自己图片的完整指南

LeNet实战:训练MNIST手写数字识别并测试自己图片的完整指南

先问一个扎心的问题:你在 MNIST 测试集上跑出了 99% 的准确率,然后自己用画图工具写了个数字,丢进模型一测,它居然信心满满地告诉你那是另一个数。这个场景我见过太多次了,包括我自己第一次跑 LeNet 时也没逃过去。这个…

2026/10/13 19:37:19 阅读更多 →
Word成绩登记表解析:从.doc到结构化数据的工程化管道

Word成绩登记表解析:从.doc到结构化数据的工程化管道

简介:这份成绩登记表文档面向中小学教师、班主任及教务管理人员,用于日常记录与管理班级学生的语文学习成绩,也可作为数据库设计、信息系统入门学习的参考素材。资源包共1个doc文件,约153KB,内容以表格形式呈现&#x…

2026/10/13 19:37:19 阅读更多 →
个人AI助手缓存系统实战:用语义缓存降低延迟与成本

个人AI助手缓存系统实战:用语义缓存降低延迟与成本

1. 为什么一个AI助手需要先解决"记得快"的问题先交代一下背景:这是"个人AI助手"系列的第7篇实践记录,前面几个阶段已经把这台助手从"能调API的大模型客户端"逐步做到了"有记忆、有工具调用能力的Agent"。阶段1打…

2026/10/13 19:37:19 阅读更多 →
仓库管理系统数据库设计实例:从课程设计到触发器实现

仓库管理系统数据库设计实例:从课程设计到触发器实现

简介:这份PDF面向数据库课程设计的学生与需要完成仓库管理系统建模的开发者,围绕需求分析、系统功能划分、数据流图、数据字典、概念结构设计、逻辑与物理结构设计及可行性分析展开,帮助读者掌握从业务需求到关系模型的完整设计思路。资源包为…

2026/10/13 19:37:19 阅读更多 →
传智健康PRD文档1:从需求到可执行工程资产的落地实践

传智健康PRD文档1:从需求到可执行工程资产的落地实践

简介:传智健康PRD文档1是一份面向健康管理系统产品设计与开发人员的需求说明书,适合产品经理、后端开发及医疗信息化学习者参考,用于理解健康管理平台从需求定义到功能落地的完整思路。压缩包内共1个docx文件,约890KB,…

2026/10/13 19:36:17 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战: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/12 21:05:09 阅读更多 →
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 阅读更多 →