Finite State Entropy(fse)编解码实战:原理、接口、性能与 cloudpods 中的 zstd 集成
后端云原生运维虚拟化人工智能AI 应用【免费下载链接】cloudpods开源、云原生的多云管理及混合云融合平台项目地址https://gitcode.com/yunionio/cloudpods点击查看免费下载导读Finite State Entropy有限状态熵编码即 tANS是一种快速的近最优符号编解码算法在 zstd 压缩标准中被用于字节块的熵编码阶段。本篇指南以 cloudpods 仓库中 vendored 的github.com/klauspost/compress/fse包为对象完整讲解其设计动机、Compress/Decompress底层接口、Scratch复用机制、错误语义、性能特征并结合同仓库内的 huff0 与 zstd 源码说明其真实调用场景。读完本文你将能够独立使用该包对单字节块做熵编码并理解它在 zstd 压缩流水线中的定位。一、什么是 Finite State EntropyFinite State Entropy简称 FSE是一种面向字节块的符号编码/解码算法属于非对称数字系统ANS家族中的 tANS 变体。它的核心特点是以极快的速度提供接近理论熵极限的压缩率实现思路与 zstd 压缩标准中的熵编码阶段完全一致。它的适用场景很明确输入中存在大量相似/重复取值的字节时FSE 可以把它们压缩到最少的字节数。但需要注意边界FSE不做任何跨字节的字典编码LZ 类编码器如 LZ77 才做这种工作因此它适合作为二级压缩步骤跟在 Snappy 这类只做重复消除、不做熵编码的压缩器之后进一步榨干冗余。这一先字典/LZ、后熵编码的分工也正是 zstd 的整体设计LZ 阶段消除重复串随后用 FSE 对字面量、匹配长度、偏移量等符号流做熵编码。二、包结构一览本仓库cloudpods中该依赖以 vendored 形式存在源码目录为 vendor/github.com/klauspost/compress/fse仅有 6 个 Go 源文件职责非常清晰文件职责fse.go包级常量、错误变量、Scratch工作区结构、Histogram/HistogramFinished辅助 APIcompress.goCompress入口、cState状态机、计数归一化normalizeCount、压缩表构建buildCTable、头计数写入writeCountdecompress.goDecompress入口、readNCount头解析、buildDtable解码表构建、decompress主循环bitreader.go反向位流读取器从流末尾开始读bitwriter.go位流写入器LSB-firstbytereader.go小端字节读取辅助注意 fse.go 头部的版权注释该实现基于 Yann Colletzstd 作者2013 年发布的 BSD 许可代码由 Klaus Post 移植为 Go 并持续优化因此与 zstd 的 FSE 实现保持了格式级兼容的设计基础。三、核心常量与参数语义从源码 fse.go 可以看到一组直接影响压缩率与速度的关键常量const ( maxMemoryUsage 14 // 内存上限 2^14 字节 16KB契合 x86 L1 缓存 defaultMemoryUsage 13 // 默认内存 2^13 字节 8KB maxTableLog maxMemoryUsage - 2 // 12 maxTablesize 1 maxTableLog // 4096 defaultTablelog defaultMemoryUsage - 2 // 11 minTablelog 5 maxSymbolValue 255 )注释给出了设计权衡内存占用越大压缩率越高减小内存则因缓存友好而提升速度。默认表对数tableLog为 11即默认状态表大小为 2^11 2048 个状态最大允许 124096 状态符号取值上限为 255单字节全范围。Compress的入参约束也写死在 compress.go输入必须小于 2GB且长度大于 1 字节否则分别返回input too big错误或ErrIncompressible。四、Compress压缩单个独立块包提供的是底层块级接口一次调用只压缩一个独立的字节块且输出中不内置任何完整性校验。这意味着调用方必须自己维护块边界块大小并在需要时自行叠加校验和。4.1 函数签名与调用方式func Compress(in []byte, s *Scratch) ([]byte, error)传入原始输入返回压缩输出和可能的错误。最简单的用法src : []byte(需要压缩的字节块……) out, err : fse.Compress(src, nil) // nil 时内部会临时分配 Scratch if err ! nil { // 需要区分是正常拒绝还是内部错误见 4.2 }4.2 必须处理的三种错误README 的说明非常直白即使一切正常也可能返回错误所以错误处理不能省略。完整错误语义如下表错误值含义触发条件nil一切正常返回输出——ErrIncompressible输入被判为难以压缩输入长度 ≤ 1或每个符号至多出现一次、分布过于均匀或压缩后输出不小于原始输入ErrUseRLE输入是单一字节值重复即全 RLE直方图中最高频符号的出现次数等于输入总长(error)内部错误归一化、表构建等内部步骤失败从 compress.go 可以看到这个判定流程的真实顺序先做直方图统计countSimple得到最高频符号计数maxCount若maxCount len(in)说明整块只有一种符号 → 返回ErrUseRLE这类数据应该由调用方用 RLE 编码若maxCount 1或maxCount len(in)7最高频符号占比不足 1/128→ 返回ErrIncompressible完成压缩后还会再比较一次输出与输入长度压缩没有赚到len(s.Out) len(in)同样返回ErrIncompressible。对调用方而言收到ErrUseRLE或ErrIncompressible时最稳妥的做法是原样透传该块或交给上层选择更合适的编码路径。4.3 内部流水线Compress在 compress.go 中依次执行prepare初始化Scratch、设置默认TableLog、按输入长度预分配输出缓冲区countSimple构建 256 桶直方图顺带确定实际使用的符号长度symbolLenoptimalTableLog依据数据规模所需最小位数与符号数所需最小位数推导最优 tableLog不能小于minTablelog5不能大于maxTableLog12normalizeCount把真实计数归一化为总和等于状态表大小的整数权重遇到分布极端的角案例时切换备用的normalizeCount2writeCount把归一化计数编码进输出头部供解压端readNCount还原buildCTable构建压缩状态转移表compress执行主压缩循环见 4.4。4.4 双状态机主循环compress.go 展示了实现细节中非常关键的两个设计两个交错状态机 c1/c2每个状态机各编码一半字节相当于每两个字节交替编码用双状态交错换取每轮处理更多符号从后向前编码压缩从输入的最后字节开始逆序进行这样最后一个最先解码的字节固定由 c1 编码为解码端初始化状态提供确定性。主循环针对是否有符号需要输出 0 位zeroBits和tableLog 是否 ≤ 8做了 4 个分支优化其中tableLog 8时一次可编码 4 个符号而无需中间 flush是吞吐的关键来源之一。cState.encode每次编码的核心动作是nbBitsOut : (uint32(c.state) symbolTT.deltaNbBits) 16 dstState : int32(c.state(nbBitsOut15)) symbolTT.deltaFindState c.bw.addBits16NC(c.state, uint8(nbBitsOut)) c.state c.stateTable[dstState]即把当前状态的高位作为待写出的位序列位数由该符号的deltaNbBits决定再查表得到下一个状态。这正是 tANS 状态转移的精髓——用状态本身携带信息而非为每个符号固定分配位长。五、Scratch零分配复用的关键每次压缩/解压都临时分配内存是浪费的。为此包提供了Scratch工作区对象压缩和解压共用同一个类型可在多次调用间复用s : fse.Scratch{} // 可反复使用 out1, _ : fse.Compress(src1, s) out2, _ : fse.Compress(src2, s)5.1 输出缓冲复用陷阱README 特别强调复用Scratch时输出缓冲区Out也会被复用。如果调用方在下次压缩/解压之前仍在引用上一次的输出必须把s.Out置为nil否则上次的输出会被下一次调用覆盖out, _ : fse.Compress(src, s) use(out) // 先用完 s.Out nil // 关键解除对旧输出的引用同样的缓冲区在压缩和解压输出之间共享因此在切换方向时也要遵守这一约定。5.2 Scratch 的可调字段从 fse.go 的Scratch定义可以看到两个按块生效的覆盖参数字段作用默认值/边界MaxSymbolValue uint8覆盖下一块的最高符号取值默认 255TableLog uint8覆盖下一块的 tableLog默认 11defaultTablelog超过 12 会在prepare时报错DecompressLimit int解压时限制最大解码输出大小0 时默认上限 2GB 0时解码接近该字节数即报错Out []byte输出缓冲复用时需按 5.1 处理内部预分配prepare的边界检查fse.go还会在TableLog maxTableLog时返回明确错误信息并在DecompressLimit 0时把上限设为(230)-1即 2GB。5.3 直方图预填充 API如果调用方例如上层压缩器已经掌握了符号分布可以通过Histogram()/HistogramFinished跳过 FSE 内部的统计步骤hist : s.Histogram() // 返回长度为 256 的计数切片 hist[65] 100 // 自行填充计数 s.HistogramFinished(maxSymbol, maxCount) // 告知最高符号下标与最高计数 out, err : fse.Compress(src, s) // 内部不再重复 countSimple六、Decompress解压与数据完整性警示解压接口同样简单func Decompress(b []byte, s *Scratch) ([]byte, error)必须传入压缩阶段返回的完整输出大小精确一致。解压流程decompress.goreadNCount从头部读出 tableLog 与归一化计数与压缩端writeCount严格对称buildDtable根据归一化计数重建解码表decompress双状态机s1/s2逆序解码出原始字节流。解码器同样采用双状态交错主循环一次解码 4 个符号nextFast用于无 0 位输出的快速路径普通next用于一般路径尾部用final收尾两个状态机的最后符号。6.1 关于完整性的三条铁律README 与源码共同强调以下三点务必遵守解码成功 ≠ 数据正确包内没有任何完整性校验解压器不报错不代表输出与原始输入一致。README 原话是relying on errors from the decompressor does not assure your data is valid。损坏数据可能静默通过虽然多数损坏会在readNCount或表构建阶段被拦截例如corruption detected (remaining ! 1)、position ! 0、newState outside table size等校验见 decompress.go但 README 明确提示有可能但绝不保证损坏数据一定返回错误。调用方自担完整性块大小记录、校验和checksum都必须由调用方自行负责。七、性能特征README 给出的性能参考在作者当时的典型硬件上、约 64KB 中等块大小测得压缩约 200MB/s/核解压约 300MB/s/核同硬件上 Huffmandeflate压缩约 125MB/s、解压约 100MB/s——即 FSE 在吞吐上明显优于传统 Huffman 编码。需要注意的边界条件影响速度的首要因素是块大小与材料的可压缩性所有压缩函数当前只使用调用它的那个 goroutine单块只占一个核需要多核并行时应由调用方把数据分块后各自调用符号值越小压缩越快。压缩端会用输入的最高字节值来裁剪处理范围因此如果数据所有字节都高于 64例如文本文件常落在 ASCII 可打印区间先把每个值整体下移 64 再做 FSE 往往能明显提速——这是一个简单而实用的预处理技巧。此外 compress.go 中针对zeroBits与tableLog的 4 路分支、bitWriter的flush32批量落盘、bitReader的 64 位容器预取等实现细节都是在为上述吞吐目标服务。八、在 zstd 流水线中的真实位置虽然 fse 是一个独立可用的低层包但它最大的用武之地是作为 zstd 的熵编码内核。在 cloudpods 的 vendored 依赖中可以直接看到两层调用证据8.1 huff0用 FSE 编码 Huffman 权重表huff0/huff0.go 在构建 Huffman 树后把各符号的权重weight序列交给 FSE 二次压缩hist : s.fse.Histogram() // ... 填充权重直方图 ... s.fse.HistogramFinished(huffMax, int(huffMaxCnt)) s.fse.TableLog maxFSETableLog b, err : fse.Compress(huffWeight[:maxSymbolValue], s.fse)这正是FSE 可以作为二级熵编码的官方实践——Huffman 本身也是熵编码但其权重表再经过一次 FSE 压缩还能进一步减小头部开销。8.2 zstd序列表的 FSE 编解码在 zstd/blockdec.go 中zstd 解码块内的字面量长度Literal Length、匹配长度Match Length和偏移量Offset三种序列符号表均通过 FSE 解码表重建readNCountzstd/seqdec.go 与 zstd/fse_decoder_amd64.go 则展示了其状态初始化与 amd64 下的 SIMD 加速解码路径。也就是说FSE 贯穿了 zstd 的两次熵编码Huffman 权重表压缩 序列符号流压缩。九、在 cloudpods 中的依赖关系与版本cloudpods 并未直接在自己的业务代码中调用fse包而是经由github.com/klauspost/compress的zstd/huff0子包间接引入。仓库证据如下go.mod 第 282 行声明github.com/klauspost/compress v1.16.0 // indirectvendor/modules.txt 第 1216-1224 行列出了该版本 vendored 的完整子包集合compress主包、fse、huff0、internal/cpuinfo、internal/snapref、zstd、zstd/internal/xxhash。因此在 cloudpods 项目中fse的实际消费方是 zstd 编解码路径——任何需要 zstd 压缩的场景例如镜像/数据流的高压缩传输都会间接受益于这里讨论的 FSE 实现。如需直接实验该包在仓库目录内以该 vendored 路径导入即可import github.com/klauspost/compress/fse十、已知状态与后续规划README 的 News 部分明确标注2018 年 2 月首发当时视为 beta 软件截至本仓库锁定版本 v1.16.0 时该包的公开 API 一直保持稳定接口即本文所述的Compress/Decompress/Scratch。规划中的方向包括暴露更多内部组件便于专家级拆解使用提供流式接口并倾向于兼容 zstd 生态的 FSE 流格式即基于字节块的逐段 FSE组合成流。当前版本仍是块级block-level接口不提供流式 API调用方需要自行处理分块与边界记录。十一、最佳实践小结把 FSE 当作二级熵编码器先做 LZ/字典去重如 Snappy再对符号流做 FSE认真处理ErrIncompressible与ErrUseRLE这是正常路径的一部分分别代表不值得压缩与请改用 RLE必须自己记账块大小并做校验包内零完整性保护复用Scratch减少分配切换调用前记得把Out置 nil并善用DecompressLimit防解压炸弹符号整体下移可提速当所有字节值都大于 64 时先减去 64 再压缩多核并行由调用方分块实现单块压缩只占一个 goroutine/核。十二、进一步阅读包入口与全部常量/错误定义vendor/github.com/klauspost/compress/fse/fse.go压缩主循环与表构建vendor/github.com/klauspost/compress/fse/compress.go解压主循环与表构建vendor/github.com/klauspost/compress/fse/decompress.go位流读写实现bitreader.go、bitwriter.goFSE 在 Huffman 权重压缩中的调用vendor/github.com/klauspost/compress/huff0/huff0.goFSE 在 zstd 序列解码中的调用vendor/github.com/klauspost/compress/zstd/blockdec.go依赖版本声明go.mod、vendor/modules.txt赞分享后端云原生运维虚拟化人工智能AI 应用【免费下载链接】cloudpods开源、云原生的多云管理及混合云融合平台项目地址https://gitcode.com/yunionio/cloudpods点击查看免费下载相关推荐containerd 依赖中的 FSE 熵编码klauspost/compress fse 包Finite State Entropy / tANS原理与 Go 实战指南containerd 依赖中的 FSE 熵编码klauspost/compress fse 包Finite State Entropy / tANS原理与云原生容器运行时运维Cortex 项目中的 Finite State EntropyFSE熵编码库原理、用法与实战剖析Cortex 项目中的 Finite State EntropyFSE熵编码库原理、用法与实战剖析 导读 本文聚焦于当前仓库所依赖的 vendor/git可观测性时序数据库后端指标监控PoB 完整指南天赋树 Build 规划与伤害计算PoB 完整指南天赋树 Build 规划与伤害计算 Path of BuildingPoB是一款《流放之路》离线 Build 规划工具。你可以用它规划天赋桌面应用上一篇探索VAmPI一个创新的虚拟化平台下一篇5分钟终极指南让普通鼠标在macOS上媲美苹果触控板创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

电商三级分类系统设计:从树形结构到智能类目中枢

电商三级分类系统设计:从树形结构到智能类目中枢

1. 为什么“三级分类”不是前端页面的简单下拉嵌套很多人第一次看到“电商项目——商品服务-API-三级分类”这个标题,下意识会想:不就是后台配个树形结构,前端渲染成三级联动菜单吗?点一级显示二级,点二级展开三级&…

2026/10/16 1:33:40 阅读更多 →
ComfyUI 在 Python 3.13 下报 No module named ‘insightface‘ 装不上?手把手完整修复教程

ComfyUI 在 Python 3.13 下报 No module named ‘insightface‘ 装不上?手把手完整修复教程

ComfyUI 在 Python 3.13 下报 No module named insightface 装不上?手把手完整修复教程 【免费下载链接】ComfyUI The most powerful and modular diffusion model GUI, api and backend with a graph/nodes interface. The fastest local inference engine in the …

2026/10/16 1:32:39 阅读更多 →
VR-reversal 实战指南:用 MPV 插件把 3D 视频转 2D 观看、环视缩放并导出 2D 视频

VR-reversal 实战指南:用 MPV 插件把 3D 视频转 2D 观看、环视缩放并导出 2D 视频

音视频VR视频 【免费下载链接】VR-reversal VR-Reversal - Player for conversion of 3D video to 2D with optional saving of head tracking data and rendering out of 2D copies. 项目地址: https://gitcode.com/gh_mirrors/vr/VR-reversal 点击查看 免费下载 …

2026/10/16 1:32:39 阅读更多 →

最新新闻

Java字符串转数字的正确方法

Java字符串转数字的正确方法

java中把字符串转成 的方法更新时间为二零二三年七月七日零九点四十分零三秒, 作者是。在Java环境里头, 大家可以利用某个类里面的那个叫作静态的方法, 把一段写着数字的字符串变成对应那种类型的数, 这篇文章会配上一些具体的代码例子, 来把这个事情从字符串弄成具体数值的过程…

2026/10/16 3:12:01 阅读更多 →
3ds Max角色动画骨骼绑定全流程:模型准备、蒙皮权重与避坑指南

3ds Max角色动画骨骼绑定全流程:模型准备、蒙皮权重与避坑指南

简介:面向3ds Max角色动画学习者的教学PPT课件,聚焦角色动画设计与骨骼绑定核心流程。课件系统讲解了创建骨骼结构、Bones内置系统与Character Studio工具的结合使用,重点区分直接连接与蒙皮连接两种支配模型的方式;围绕软性骨骼蒙…

2026/10/16 3:12:01 阅读更多 →
先查供电、天线和传感器 CRC:方案测试怎么跟硬件同事对话

先查供电、天线和传感器 CRC:方案测试怎么跟硬件同事对话

副标题:松天线、拔传感器、波特率设错,各记一层;先给数据再让人改版 专栏:《嵌入式学生的方案测试课》第 15 篇(软硬件联测) 前置:第 1 篇环境表;第 5 篇四层;第 9 篇没有…

2026/10/16 3:12:01 阅读更多 →
混沌拓扑学(HDT)灰度思想与布劳威尔直觉主义、选择序列与展形——排中律失效后的数学重构

混沌拓扑学(HDT)灰度思想与布劳威尔直觉主义、选择序列与展形——排中律失效后的数学重构

前言 在经典数学体系中,排中律被视为不可撼动的逻辑公理:对于任意命题 PPP,必然满足 P∨PP \lor \neg PP∨P。 通俗表述为:任何命题非真即假,不存在第三种状态,不存在未确定状态,不存在中间灰度…

2026/10/16 3:12:01 阅读更多 →
浪潮AS5300/AS5500双控存储安装调试指南:从级联到映射

浪潮AS5300/AS5500双控存储安装调试指南:从级联到映射

简介:浪潮存储AS5300/AS5500安装调试指导文档,面向企业级存储实施、运维及售前售后工程师,解决新设备上架、初始化配置、业务上线和故障定位过程中的操作盲区。内容按七个环节组织:先给出设备规格与扩展柜SS5000J-16/SS5000J-24物…

2026/10/16 3:12:01 阅读更多 →
小白程序员必看:AI大模型落地实战指南,从入门到精通

小白程序员必看:AI大模型落地实战指南,从入门到精通

本文深入探讨AI大模型应用落地过程中的关键环节,强调企业需求明确、模型流程整合及持续优化的重要性。文章指出,AI应用需从模糊需求明确化,到模型接入可用流程,再到真实使用中的持续改进。通过分析FDE工程师的核心能力&#xff0c…

2026/10/16 3:11:01 阅读更多 →

日新闻

个人 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/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练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 阅读更多 →