Python手搓区块链:从哈希指针到工作量证明的完整实现
1. 从零构建一条链为什么用Python手搓区块链是理解分布式账本的最短路径很多人第一次接触区块链这个概念脑子里冒出来的都是比特币、以太坊、智能合约、挖矿这些词感觉离自己特别远。但如果你把那些金融属性、共识博弈、代币经济全部剥掉只看最核心的数据结构会发现它其实简单得有点出人意料——本质上就是一个带哈希指针的链表再加上一套校验规则和共识机制。我当初决定用Python手搓一个区块链动机很朴素看再多白皮书不如自己写一遍。Python在这件事上有天然优势——语法干净、不需要处理内存管理、哈希库和JSON序列化都是标准库自带你可以把全部注意力放在结构设计和逻辑验证上而不是被语言本身的复杂度分散精力。这篇文章适合两类人一类是刚学完Python基础语法想找一个有挑战性但又不至于劝退的练手项目另一类是有后端开发经验想搞清楚区块链到底是怎么串起来的工程师。我会从最基础的数据结构开始一步步加上哈希计算、工作量证明、交易打包、链校验最后给出一个可以直接跑起来的完整实现。整个过程不依赖任何第三方区块链框架只用Python标准库。需要提前说明的是这里实现的是教学性质的简化区块链它不具备生产级区块链的完整特性——没有P2P网络层、没有真正的分布式共识、没有账户签名体系。但它的核心数据结构和校验逻辑和真实区块链的骨架是一致的。理解了这套骨架你再去看比特币或以太坊的源码就不会觉得无从下手。2. 区块与链的骨架设计哈希指针到底是怎么串起来的2.1 区块应该包含哪些字段一个区块本质上就是一条记录它需要回答几个问题我是谁区块高度、我什么时候产生的时间戳、我里面装了什么交易数据、我前面是谁前一个区块的哈希、我怎么证明自己是被认真计算出来的工作量证明。基于这几个问题我设计的区块字段如下字段名类型作用indexint区块高度从0开始递增timestampfloat区块创建时的Unix时间戳transactionslist打包的交易列表previous_hashstr前一个区块的哈希值nonceint工作量证明的计数器hashstr当前区块自身的哈希值这里有一个设计决策值得展开说为什么用previous_hash而不是直接存前一个区块对象的引用因为区块链的核心特性是不可篡改而不可篡改的实现方式就是每个区块的哈希都依赖于前一个区块的哈希。如果你改了任何一个历史区块的数据它的哈希就会变后面所有区块的previous_hash就对不上了整条链的校验就会失败。用哈希值而不是对象引用是为了让这个依赖关系变成密码学层面的而不是内存层面的。2.2 哈希函数的选型与计算范围Python标准库的hashlib提供了SHA-256这是比特币也在用的哈希算法输出256位64个十六进制字符。选它的理由很直接雪崩效应足够强输入改一个字符输出面目全非、计算速度快、碰撞概率极低。计算区块哈希时需要把区块的所有关键字段拼成一个字符串再哈希。这里有个容易踩的坑字段拼接的顺序和格式必须固定否则同一个区块在不同时间计算出来的哈希会不一样。我的做法是用JSON序列化并且指定sort_keysTrue和separators保证输出格式稳定import hashlib import json def compute_hash(index, timestamp, transactions, previous_hash, nonce): block_string json.dumps({ index: index, timestamp: timestamp, transactions: transactions, previous_hash: previous_hash, nonce: nonce }, sort_keysTrue, separators(,, :)).encode() return hashlib.sha256(block_string).hexdigest()注意这里没有把hash字段本身纳入计算——这是必须的否则就成了用哈希算哈希的循环依赖。hash字段是计算结果不是输入。2.3 创世区块的特殊处理链上的第一个区块叫创世区块它没有前一个区块。通常的做法是把previous_hash设成0或者空字符串index设为0。创世区块一般由系统在初始化时自动生成不包含实际交易或者包含一条创世备注。def create_genesis_block(): return Block( index0, timestamptime.time(), transactions[Genesis Block], previous_hash0 )创世区块的nonce也需要通过工作量证明算出来不能随便填。这一点很多人第一次写的时候会忽略导致链的校验逻辑不一致。3. 工作量证明为什么必须让计算变得困难3.1 从直接算哈希到必须满足条件如果只是把区块数据哈希一下就算完那任何人改完数据重新算一遍哈希就行了篡改成本为零。工作量证明的核心思想是给哈希计算加一个门槛让算出合法哈希需要消耗计算资源。具体做法是引入一个难度值difficulty要求区块哈希的前difficulty个字符必须是0。比如difficulty4哈希就必须长这样0000a3f2...。因为SHA-256的输出是均匀分布的你没法预测改哪个nonce能得到前四位为0的结果只能一个一个试。def mine_block(block, difficulty4): target 0 * difficulty while not block.hash.startswith(target): block.nonce 1 block.hash compute_hash( block.index, block.timestamp, block.transactions, block.previous_hash, block.nonce ) return block3.2 难度值的实际影响与调整策略难度值每增加1期望的计算次数大约翻16倍因为十六进制每一位有16种可能。我实测过在普通笔记本上难度值平均耗时平均尝试次数3 0.01秒约 4,000 次4约 0.05秒约 65,000 次5约 0.8秒约 1,000,000 次6约 13秒约 16,000,000 次教学演示用difficulty4比较合适既有挖矿的感觉又不会让你等太久。如果你想体验一下真实挖矿的难度可以试试difficulty6但建议只在单独测试时用。注意难度值不是越高越好。在单机环境下难度太高会导致每次添加区块都要等很久调试体验极差。建议先用difficulty2跑通逻辑确认无误后再调到4。3.3 nonce的起始值与溢出问题nonce从0开始递增Python的整数没有溢出问题所以理论上可以无限试下去。但实际测试中difficulty4的情况下nonce通常在几万到几十万之间就能找到解。如果你发现nonce已经涨到几百万还没出结果大概率是哈希计算的输入有问题——检查一下字段拼接格式是否稳定。4. 交易打包与链的校验逻辑怎么判断一条链有没有被篡改4.1 交易的简化表示教学场景下交易不需要搞得太复杂。我用一个字典表示一笔交易transaction { sender: Alice, receiver: Bob, amount: 10 }真实区块链里交易还需要数字签名、手续费、nonce防重放等机制但那些属于账户体系的范畴和链结构本身关系不大。这里先用最简形式把注意力集中在链的串联和校验上。4.2 添加新区块的完整流程添加一个新区块需要做四件事获取上一个区块、构造新块、挖矿、追加到链上。def add_block(blockchain, transactions, difficulty4): previous_block blockchain[-1] new_block Block( indexprevious_block.index 1, timestamptime.time(), transactionstransactions, previous_hashprevious_block.hash ) mine_block(new_block, difficulty) blockchain.append(new_block) return new_block这里的关键点是新区块的previous_hash必须取自上一个区块的hash字段而不是重新计算上一个区块的哈希。因为上一个区块的hash是挖矿时确定下来的重新计算可能会因为时间戳精度等问题得到不同的结果。4.3 链校验的完整逻辑校验一条链是否合法需要逐块检查三件事当前区块存储的hash是否等于重新计算出来的hash防止区块内部数据被改当前区块的previous_hash是否等于前一个区块的hash防止区块之间的链接被断当前区块的hash是否满足难度要求防止有人绕过挖矿直接伪造def validate_chain(blockchain, difficulty4): target 0 * difficulty for i in range(1, len(blockchain)): current blockchain[i] previous blockchain[i - 1] # 检查当前区块的哈希是否被篡改 recalculated compute_hash( current.index, current.timestamp, current.transactions, current.previous_hash, current.nonce ) if current.hash ! recalculated: print(f区块 {i} 的哈希不匹配) return False # 检查与前一个区块的链接 if current.previous_hash ! previous.hash: print(f区块 {i} 与前一个区块的链接断裂) return False # 检查工作量证明 if not current.hash.startswith(target): print(f区块 {i} 的工作量证明不合法) return False return True4.4 篡改实验亲手改一个区块看看会发生什么理解校验逻辑最好的方式就是亲手篡改一个区块然后看校验怎么失败。你可以这样试blockchain [create_genesis_block()] add_block(blockchain, [{sender: A, receiver: B, amount: 5}]) add_block(blockchain, [{sender: B, receiver: C, amount: 3}]) print(validate_chain(blockchain)) # True # 篡改第一个区块的交易数据 blockchain[1].transactions [{sender: A, receiver: B, amount: 9999}] print(validate_chain(blockchain)) # False篡改之后区块1的哈希会变导致区块2的previous_hash对不上校验在区块1的哈希检查就会失败。这就是区块链不可篡改的底层逻辑——改一个块后面全崩。5. 实操中容易踩的坑与调试技巧5.1 时间戳精度导致的哈希不一致Python的time.time()返回浮点数不同平台或不同调用时刻的精度可能不同。如果你在计算哈希时用了时间戳然后在校验时重新获取时间戳那哈希肯定对不上。正确做法是把时间戳作为区块的一个固定字段存下来计算和校验都用这个存储值不要重新调用time.time()。5.2 JSON序列化的键顺序问题Python 3.7的字典是有序的但如果你不指定sort_keysTrue不同版本或不同构造方式可能导致键的顺序不一致进而导致哈希不一致。我在第一次实现时就踩过这个坑构造区块时字典的键顺序是index、timestamp、transactions但校验时从对象属性重建字典的顺序变成了timestamp、index、transactions结果哈希完全对不上。加上sort_keysTrue之后问题消失。5.3 难度值不一致导致的校验失败挖矿时用的difficulty和校验时用的difficulty必须一致。如果你挖矿用4校验用3那校验会通过因为前3位确实是0但这意味着难度要求被降低了。建议把difficulty作为链的一个全局参数存下来或者作为配置项统一管理。5.4 创世区块的校验跳过校验循环从i1开始跳过创世区块。这是因为创世区块没有previous_hash可校验而且它的previous_hash是0这种特殊值。如果你把创世区块也纳入校验需要单独处理它的previous_hash检查逻辑。5.5 调试时打印中间哈希值当校验失败时最有效的调试手段是把每个区块的存储哈希和重新计算的哈希都打印出来对比看是哪一块开始出现不一致。我通常会在validate_chain里加一个verbose参数失败时输出详细信息if current.hash ! recalculated: print(f区块 {i}: 存储哈希{current.hash}) print(f区块 {i}: 计算哈希{recalculated}) print(f区块 {i}: nonce{current.nonce}) print(f区块 {i}: previous_hash{current.previous_hash}) return False这样一眼就能看出是数据被改了还是哈希计算方式不一致。6. 从教学实现到真实区块链还差哪些关键拼图6.1 网络层与节点通信目前这条链只存在于单个进程的内存里。真实区块链需要多个节点各自维护一份链通过P2P网络广播新区块和交易。节点收到新区块后要校验合法性然后决定是否追加到自己的链上。如果两个节点同时挖出新区块还需要一套共识规则来决定哪条链是主链比特币用的是最长链原则。6.2 账户体系与数字签名现在的交易只是明文字典任何人都可以伪造一笔A转给B 100块的交易。真实系统需要用非对称加密发送方用私钥对交易签名接收方用公钥验证签名。只有签名合法的交易才会被打包进区块。6.3 共识机制的引入单机环境下你自己就是唯一的节点想怎么改就怎么改。但在分布式环境下需要一套机制来防止恶意节点篡改数据。工作量证明本身就是一种共识机制——它让篡改历史链的成本变得极高因为你需要重算所有后续区块的工作量证明并且要超过其他诚实节点的算力总和。6.4 交易池与手续费市场真实区块链不会每来一笔交易就立刻打包而是先把交易放进交易池矿工从池子里挑选手续费高的交易打包。这涉及到交易优先级、手续费估算、交易替换等复杂逻辑。教学实现里可以简化为来一笔打包一笔。6.5 状态存储与智能合约比特币的链上只存交易账户余额需要通过遍历所有历史交易来计算。以太坊则引入了状态树每个区块头里存了全局状态的根哈希还支持在链上部署和执行智能合约。这部分属于进阶内容建议先把基础链结构吃透再研究。7. 完整可运行代码与实测效果把前面的片段拼起来就是一个完整的可运行实现。我把它整理成一个文件你可以直接复制到本地跑import hashlib import json import time class Block: def __init__(self, index, timestamp, transactions, previous_hash, nonce0): self.index index self.timestamp timestamp self.transactions transactions 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, transactions: self.transactions, previous_hash: self.previous_hash, nonce: self.nonce }, sort_keysTrue, separators(,, :)).encode() return hashlib.sha256(block_string).hexdigest() def mine_block(block, difficulty4): target 0 * difficulty while not block.hash.startswith(target): block.nonce 1 block.hash block.compute_hash() return block def create_genesis_block(difficulty4): genesis Block(0, time.time(), [Genesis Block], 0) return mine_block(genesis, difficulty) def add_block(blockchain, transactions, difficulty4): previous blockchain[-1] new_block Block( indexprevious.index 1, timestamptime.time(), transactionstransactions, previous_hashprevious.hash ) mine_block(new_block, difficulty) blockchain.append(new_block) return new_block def validate_chain(blockchain, difficulty4): target 0 * difficulty for i in range(1, len(blockchain)): current blockchain[i] previous blockchain[i - 1] if current.hash ! current.compute_hash(): return False, f区块 {i} 哈希不匹配 if current.previous_hash ! previous.hash: return False, f区块 {i} 链接断裂 if not current.hash.startswith(target): return False, f区块 {i} 工作量证明不合法 return True, 链校验通过 if __name__ __main__: difficulty 4 chain [create_genesis_block(difficulty)] add_block(chain, [{sender: Alice, receiver: Bob, amount: 10}], difficulty) add_block(chain, [{sender: Bob, receiver: Carol, amount: 5}], difficulty) add_block(chain, [{sender: Carol, receiver: Dave, amount: 2}], difficulty) for block in chain: print(f区块 {block.index} | nonce{block.nonce} | hash{block.hash[:16]}...) ok, msg validate_chain(chain, difficulty) print(f校验结果: {ok} - {msg}) # 篡改实验 chain[1].transactions [{sender: Alice, receiver: Bob, amount: 99999}] ok, msg validate_chain(chain, difficulty) print(f篡改后校验: {ok} - {msg})实测输出大致如下区块 0 | nonce48213 | hash0000a3f2c8d1... 区块 1 | nonce91204 | hash0000b7e1f4a2... 区块 2 | nonce33781 | hash0000c9d3e5b6... 区块 3 | nonce67092 | hash0000d1f7a8c4... 校验结果: True - 链校验通过 篡改后校验: False - 区块 1 哈希不匹配每次运行的nonce和哈希都会不同因为时间戳变了。但校验逻辑是稳定的不改数据就通过改了数据就失败。8. 几个值得继续折腾的扩展方向如果你已经把基础版本跑通了可以试试这几个扩展每一个都能让你对区块链的理解更深一层第一个扩展是持久化存储。目前链只存在内存里程序一关就没了。你可以用JSON文件或SQLite把链存下来启动时加载关闭时保存。这会让你接触到序列化和反序列化过程中的哈希一致性问题——存进去再读出来哈希还能对得上吗第二个扩展是简单的P2P模拟。不需要真的搞网络编程可以用多线程模拟多个节点每个节点维护自己的链通过队列互相广播新区块。当一个节点收到新区块时校验并决定是否追加。这能让你直观感受到最长链原则是怎么工作的。第三个扩展是交易签名。用Python的cryptography库生成一对密钥发送方用私钥签名交易节点用公钥验证。只有签名合法的交易才能被打包。这会让你理解为什么区块链上的交易是不可抵赖的。第四个扩展是难度动态调整。让程序根据出块速度自动调整difficulty如果最近10个区块的平均出块时间小于目标值就提高难度大于目标值就降低难度。这是比特币维持稳定出块间隔的核心机制。我在实际写这套代码的过程中最大的体会是区块链的复杂性不在单个模块而在模块之间的耦合关系。哈希、链式结构、工作量证明、校验逻辑每一个单独看都不难但把它们串在一起就形成了一个改一处而动全身的系统。这种耦合不是设计缺陷而是刻意为之——它正是不可篡改特性的来源。你在调试时如果发现改了一个字段导致整条链校验失败不要觉得烦那说明你的实现是对的。

相关新闻

Flutter Windows安装包3.0.4 stable离线配置与常见问题排查

Flutter Windows安装包3.0.4 stable离线配置与常见问题排查

简介:Flutter 3.0.4稳定版Windows安装包是面向Windows开发者的跨平台应用开发工具,适合希望用单一代码库构建Android和iOS应用的中初级开发者。压缩包内集成了完整SDK组件,包括dart-sdk运行环境、开发工具集合、框架依赖包以及各类平台配置文…

2026/10/18 22:12:30 阅读更多 →
手写BP神经网络实现人脸识别:从原理到MATLAB工程实践

手写BP神经网络实现人脸识别:从原理到MATLAB工程实践

简介:本资源是一份面向高校人工智能课程学习者与Matlab初学者的BP神经网络实战项目,聚焦人脸图像识别这一经典模式识别任务,提供从理论原理到可运行代码的完整实现路径。压缩包共67个文件,包含23张人脸样本JPG图像、22个MATLAB预训…

2026/10/18 22:12:30 阅读更多 →
性能测试环境搭建实战:规划、部署、数据与监控全流程

性能测试环境搭建实战:规划、部署、数据与监控全流程

干性能测试的朋友应该都有过这种经历:压测脚本写好了,场景也调通了,结果一跑出来的数据自己都不敢信——响应时间忽高忽低,CPU用量莫名其妙拉满,或者数据库连接数动不动就爆。查来查去,最后发现根本不是系统…

2026/10/18 22:12:30 阅读更多 →

最新新闻

水文站点实时数据爬虫:Selenium+Requests混合方案实战

水文站点实时数据爬虫:Selenium+Requests混合方案实战

简介:本资源是一套面向Python初学者与数据爱好者设计的水文实时数据采集与可视化实践项目,聚焦网络爬虫与基础数据分析能力训练,适用于环境监测、水利信息化等场景的入门级技术验证。压缩包共9个文件,含3个核心Python脚本&#xf…

2026/10/18 23:09:18 阅读更多 →
PLaMo 2的MLX版本初体验:plamo-2-translate-bf16在Mac上免费本地运行8B英日翻译模型的完整概览指南

PLaMo 2的MLX版本初体验:plamo-2-translate-bf16在Mac上免费本地运行8B英日翻译模型的完整概览指南

【免费下载链接】plamo-2-translate-bf16 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/plamo-2-translate-bf16 点击查看 免费下载 本文带你完整了解 plamo-2-translate-bf16:由 mlx-community 制作的 PLaMo 2 8B 英日翻译模型 MLX 版本…

2026/10/18 23:09:18 阅读更多 →
Windows 64位下CUDA 11.4与cuDNN v8.2.2.26运行库配置指南

Windows 64位下CUDA 11.4与cuDNN v8.2.2.26运行库配置指南

简介:这是一份面向 Windows 深度学习开发者的 CUDA 11.4 配套组件包,提供 NVIDIA cuDNN v8.2.2.26 的完整 x64 运行库与头文件,专为搭配 CUDA Toolkit 11.4 使用,适合需要配置 TensorFlow、PyTorch 等框架 GPU 加速环境的开发者。…

2026/10/18 23:09:18 阅读更多 →
深入理解 Claude Code 源码:终端 Agent 的工程解剖与实现

深入理解 Claude Code 源码:终端 Agent 的工程解剖与实现

简介:《深入理解Claude Code源码》面向希望深入终端AI Agent内部实现的技术人员,系统剖析Anthropic命令行编程助手的架构设计、工程实践与编程哲学。全书25章加2个附录,覆盖查询引擎、工具系统、Agent编排、六层权限模型、MCP协议、状态管理与…

2026/10/18 23:09:18 阅读更多 →
提示工程架构师:从提示词设计到稳定落地的完整指南

提示工程架构师:从提示词设计到稳定落地的完整指南

1. 提示工程架构师到底在做什么——角色拆解与价值锚点1.1 从一个不起眼的输入框说起大概在两三年前,我很难想象"提示工程架构师"会成为一个正经职业头衔。当时身边人听说我在研究"怎么跟AI说话",第一反应都是:那不就是写…

2026/10/18 23:09:18 阅读更多 →
78B多模态大模型如何落地?InternVL2_5-78B-MPO多卡部署与省显存量化完整攻略

78B多模态大模型如何落地?InternVL2_5-78B-MPO多卡部署与省显存量化完整攻略

【免费下载链接】InternVL2_5-78B-MPO 项目地址: https://ai.gitcode.com/OpenGVLab/InternVL2_5-78B-MPO 点击查看 免费下载 InternVL2_5-78B-MPO 是一款基于 ViT-MLP-LLM 架构的开源多模态大模型,融合了 6B 视觉编码器 InternViT 与 72B 语言模型 Qwe…

2026/10/18 23:08:17 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 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/18 0:55:02 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →