news 2026/9/8 2:13:53

视频文件加密程序设计与实现:基于FFmpeg和AES-128-CBC的HLS方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
视频文件加密程序设计与实现:基于FFmpeg和AES-128-CBC的HLS方案

简介:这是一套基于C#开发的视频文件加密与转码程序源代码,适合有一定WinForms/WPF基础、希望为自有视频内容增加版权保护能力的开发者使用。程序采用AES算法对视频流进行完全加密,并借助开源VLC播放器直接解码解密后的字节流,同时内置微型Web服务器实现边解密边播放,从源头提高破解成本;转码部分则调用FFmpeg完成,工程内已附带相关工具,开箱即可调试。压缩包共591个文件,包含381个dll、34个cs、19个png及resx/resources等资源文件,另有exe、config等运行配置,整体体积64.32MB,目录结构较为完整,便于按模块阅读与二次开发。目前已有2527人学习下载,适合作为视频加密、流媒体播放及FFmpeg集成方向的项目参考,也能帮助初学者理解AES加解密与播放器对接的完整流程。 视频文件加密这件事,听起来挺高大上,其实就是给视频素材加一道锁,让不该看的人打不开。我大概是三年前开始折腾这个方向的,当时是接了个小项目,对方要求把一批教学视频做版权保护,既要防止被人直接下载盗用,又得保证付费用户在不同设备上能流畅播放。查了一圈资料,发现市面上成熟的视频加密方案要么收费太高,要么绑死平台,最后决定自己写一套“视频文件加密程序”,顺手把视频转码也整合进来了。这套源代码到现在还在我的Git仓库里持续维护,今天把它拿出来拆开讲讲,希望能帮到有同样需求的开发者。

先说清楚这套程序能干什么:它接收一个普通视频文件作为输入,先做格式统一转码,再对视频内容做真正的加密处理,最终输出两个东西,一个是加密后的视频文件(或流媒体分片),另一个是配套的解密播放器前端。简单理解就是完成了“视频的不可见性”和“播放的可行性”这对天然矛盾的统一。功能上一点也不花哨,但每一步都是刚需,特别适合做在线教育、私域内容分发、企业内部培训资料保护这些场景。

这篇文章不是只放代码堆料,我会把整个项目的设计思路、技术选型、核心代码片段、常见坑位一次性讲透。无论你是想照搬这套方案自己搭一个视频加密服务,还是想理解视频加密与转码的核心原理,都能从这里找到可以直接用的东西。

1. 项目定位与总体架构设计

1.1 这套程序到底解决了什么问题

先给这个项目画个像:它不是那种开箱即用的大而全DRM系统,而是面向轻量级应用场景的自托管视频保护方案。现在很多中小团队做视频内容交付,最头疼的就是防盗链和防盗播问题。用网盘发视频,链接转出去就失控了;直接RTMP/RTSP拉流,抓包就能拿到流地址;就算套上普通HTTP服务器,用IDM之类的下载工具一样能把MP4拽下来。

我做的这个程序,通过“转码+加密”两步走,把视频在存储阶段变成密文。即使有人拿到了文件,没有密钥和解密逻辑,也只是一堆毫无意义的二进制乱码,彻底绕开了“先下载再破解”这个环节。对于HLS流媒体场景,它还把切片也做了加密,从传输链路到存储介质都覆盖到。

1.2 总体架构与核心模块划分

整个程序从代码结构上分成三大块:

  • 转码服务模块:基于FFmpeg做视频格式、编码、分辨率、码率的统一处理,相当于所有视频进入加密流程前的“质检车间”。
  • 密钥管理系统:负责生成加密密钥、管理密钥版本、输出解密所需的信息文件,是整个加密方案的“安全中枢”。
  • 加密执行引擎:实际执行AES-128-CBC加密逻辑,同时处理HLS切片和加密索引文件(m3u8)的生成。

三块业务逻辑完全解耦,你可以只拿加密引擎配合自己的转码通道用,也可以把整个流程串起来做成一个命令行工具。我当时为了调试方便,把调度逻辑写成了一个Python脚本,核心模块用C扩展和FFmpeg二进制完成,兼顾开发效率和密码学层面的执行安全。

一个完整的加密流程用文字描述是这样的:原始视频输入后,先交由FFmpeg探测文件信息,按照预设的参数档位执行转码(比如全部统一成H.264+AAC、分辨率压到1080P、码率按源文件自动判断);转码后的临时文件进入加密模块,系统生成随机密钥,按切片长度切分后逐段做AES加密;最后输出加密分片和索引文件,并生成一个包含密钥信息的JSON文件,这块信息后续要转移到脱离Web目录的独立位置。

注意:无论流程怎么调整,密钥的存储路径不应该和加密视频处于同一可被Web访问的目录下。任何人只要能同时拿到密文和密钥,加密保护就成了摆设。

1.3 为什么选AES-128-CBC做核心算法

加密算法是我最早定下来的部分。视频是典型的大文件类型,动辄几百MB甚至几个GB,选择算法需要考虑加解密速度、硬件兼容性和安全性三个维度。AES是目前应用最广泛的对称加密标准,AES-128虽然密钥长度比AES-256短,但加密速度更快,在视频场景下128位的安全强度已经足够。CBC(密码分组链接)模式通过前一个密文块参与当前块的加密运算,让相同明文块产生不同密文块,抗统计分析能力比ECB强得多,这也是它在TLS和各类文件加密工具中的主流选择。

如果以后要针对不同安全等级做区分,可以给AES-128-CBC做一个上层封装,预留密钥长度切换的接口。但从实际效果看,AES-128-CBC的破解难度在当前算力条件下已经属于天文数字级别,普通商业盗版团队根本不会为了一个视频去硬碰这种级别加密,他们要的是“软柿子”,而CBC块链式加密足以劝退一大批人。

2. 视频转码模块的选型与参数设计

2.1 FFmpeg在这里扮演什么角色

视频转码这个事,FFmpeg基本是事实标准。这套程序选型时代替方案也考虑过GStreamer,但FFmpeg的命令行生态太方便了,各种滤镜、编码器封装得非常完善,用子进程调用就行,不需要自己拉一堆依赖库。而且FFmpeg对格式的兼容性是所有开源方案里最好的,从MP4、MOV到MKV、FLV,它都能吃下去。

不过FFmpeg的转码参数不是随便写写就行的,参数配置直接关系到转码效率、输出画质和后续加密流程的稳定性。我在程序里设计了一组“阶梯转码配置”,根据输入文件的编码格式自动选择参数策略。比如输入本身就是H.264+AAC的MP4,就直接走流复制模式(copy),不做二次编码,既省时间又不损失画质;遇到编码不规范的老视频,再落到重新编码分支。

2.2 转码参数怎么定才合理

一个靠谱的转码参数表,核心是H.264编码参数、AAC音频参数和封装格式这三者的配合。我把自己实践中验证过的一组模板贴出来:

# 标准转码模板(1080P,适合大多数在线播放场景) ffmpeg -i input.mp4 \ -c:v libx264 -preset medium -profile:v high -level 4.1 \ -crf 23 -maxrate 4M -bufsize 8M \ -c:a aac -b:a 128k -ar 44100 -ac 2 \ -movflags +faststart \ -vf "scale='min(1920,iw)':'-2'" \ output.mp4

逐项解释一下这些参数的含义:-preset medium是速度和压缩率的平衡点,追求最快速度可以降到ultrafast,但同等码率下文件体积会增加20%-30%;-crf 23是H.264的质量控制参数,数值越小画质越高文件越大,23对大多数视频是一个肉眼几乎无损的点;-maxrate-bufsize限制了编码瞬时码率峰值,防止网络抖动时播放器缓冲爆掉;-movflags +faststart这个参数非常关键,它把MP4的moov元数据从头移到文件头部,让播放器不用下载完整文件就能开始播放。

CRF的具体选择取决于你的视频类型。如果是PPT录屏这类静态画面多、纹理少的内容,CRF调到25甚至27问题不大;如果是电影、体育比赛这类运动场景多的素材,建议保持在CRF 20-23区间,否则画面会出现可见的模糊和块效应。

2.3 转码后文件校验与清洗

转码完成不等于可以进入加密流程了,我这里有一个强制校验步骤:用FFmpeg的-v error模式做全文件解码测试,捕获解码过程中出现的报错信息;同时用ffprobe读取输出文件的时长、流信息,和源文件对比,时长的误差超过0.5秒就判定转码异常。这个步骤看起来额外多花了一点时间,但对于加密流程来说,所有输入文件必须是完好无损且结构统一的,否则后续切片加密环节很可能因为文件索引损坏导致输出废片。

还有一个容易被忽视的细节:转码输出的临时文件一定要写在加密程序可控的临时目录里,并设置合理的权限,避免在中间环节被其他用户读取。毕竟转码过程是明文存盘的窗口期,这属于安全的边边角角,但真正的防护体系就是靠这些边角料拼起来的。

3. 加密模块的设计与核心实现

3.1 密钥的生成与管理方案

密钥管理是整个加密程序中最容易出问题的环节。我在程序里实现的方案是:每次启动加密任务时,用Python的secrets模块生成一个32字节(256位)的随机主密钥,但实际加密过程中仅使用前16字节作为AES-128密钥。为什么故意取256位却只用128位?因为secrets.token_bytes(32)底层走操作系统提供的加密安全随机数源,生成高熵的随机值没有额外成本,而真正的密码学强度取决于加密算法选用的AES-128,这样设计是为了在代码里预留未来升级到AES-256的能力。

另一个关键设计是“一视频一钥”。也就是每个视频文件在加密时都生成独立的随机密钥,绝对不会多个视频共享同一个密钥。这样做的好处是,即使某个视频的密钥泄露了(比如遭到恶意购买者的屏幕录制),其他视频的安全性完全不受影响,把风险隔离在单文件级别。

3.2 HLS切片加密流程与代码实现

HLS加密,本质上就是把一个完整的视频切成很多小块(TS分片),对每个分片做AES-128-CBC加密,然后用一个m3u8索引文件记录每个分片的URL和加密信息。播放器只要拿到m3u8和正确的密钥,就能顺序下载分片并完成解密和连续播放。

我用Python实现切片加密核心逻辑,整体流程如下:

import os import subprocess from pathlib import Path from Crypto.Cipher import AES from Crypto.Util.Padding import pad class VideoEncryptor: def __init__(self, ffmpeg_path="ffmpeg"): self.ffmpeg = ffmpeg_path def generate_key(self, key_file_path: str) -> bytes: key = os.urandom(16) # 128-bit AES key with open(key_file_path, "wb") as f: f.write(key) os.chmod(key_file_path, 0o600) # 仅当前用户可读写 return key def create_key_info_file(self, key_uri: str, key_file_path: str, iv_hex: str) -> str: """创建HLS加密所需的keyinfo文件""" key_info_path = str(Path(key_file_path).with_suffix(".keyinfo")) with open(key_info_path, "w") as f: f.write(f"{key_uri}\n") f.write(f"{key_file_path}\n") f.write(f"{iv_hex}\n") # 可选的IV,16字节hex格式 return key_info_path def encrypt_as_hls(self, input_file: str, output_dir: str, segment_time: int = 10, key_uri: str = "enc.key") -> str: output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) key_file = output_dir / key_uri key = self.generate_key(str(key_file)) # IV取密钥的MD5哈希,保证每个分片IV相同但难预测 import hashlib iv_md5 = hashlib.md5(key).hexdigest()[:32] key_info = self.create_key_info_file( key_uri=key_uri, key_file_path=str(key_file), iv_hex=iv_md5 ) output_m3u8 = output_dir / "index.m3u8" cmd = [ self.ffmpeg, "-i", input_file, "-c", "copy", # 已转码文件直接流复制,不再二次编码 "-hls_time", str(segment_time), "-hls_key_info_file", str(key_info), "-hls_playlist_type", "vod", "-hls_segment_filename", str(output_dir / "seg_%03d.ts"), "-y", str(output_m3u8) ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"FFmpeg HLS encryption failed: {result.stderr}") return str(output_m3u8)

在这个实现中,最关键的是-hls_key_info_file参数。它读取的keyinfo文件里包含三行:密钥的URI地址(相对m3u8的路径)、密钥文件的本地磁盘路径、IV初始化向量(16字节hex字符串)。FFmpeg读了这份配置后,会自动给生成的每个TS分片套上AES-128-CBC加密。

IV(Initialization Vector)的设计也值得展开说。CBC模式下,第一个明文块会先与IV异或再执行加密运算,所以IV不能重复且应该有不可预测性。我用密钥的MD5值作为IV来源,保证了IV和密钥的关联性,又不需要单独保存IV文件,在HLS场景下这种单文件方案最省事。

3.3 整文件加密的备用方案

HLS加密非常适合流媒体点播场景,但不是所有视频都需要切片播放。有些场景下,用户希望下载完整视频后在本地播放器里看,这时候HLS就不太合适了。于是我在程序里保留了一个“整文件加密”模式:用AES-128-CBC把MP4整体加密成一个.closed格式的自定义文件。

def encrypt_whole_file(self, input_file: str, output_file: str): """整文件AES-128-CBC加密""" key = os.urandom(16) iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv) chunk_size = 64 * 1024 # 64KB块,兼顾内存与效率 with open(input_file, "rb") as fin, open(output_file, "wb") as fout: # 写入头信息:magic标识 + IV fout.write(b"VENC" + iv) while True: chunk = fin.read(chunk_size) if len(chunk) == 0: break if len(chunk) % AES.block_size != 0: padding_len = AES.block_size - (len(chunk) % AES.block_size) chunk += bytes([padding_len]) * padding_len # 但这种方式对最后一个块的padding不严谨,应对完整块立即加密 encrypted_chunk = cipher.encrypt(chunk) fout.write(encrypted_chunk)

声明一下,上面这段为了简化只展示了核心思路。真正落地的代码不应该用这种末尾判断的方式做padding,而是应该用PyCryptodome的pad()函数处理最后一个块,避免出现因padding错误导致的解密失败。整文件加密的终端体验是配合一个带解密功能的桌面播放器使用,播放器先输入密钥完成解密流程,再临时落盘播放并立即删除。这个方案天然不支持拖拽进度条,所以它更适合短视频或音频类业务,长视频慎用。

3.4 解密播放端的前端实现要点

加密做得再好,如果播放器拉垮,整个方案也是零。解密播放这件事在浏览器端其实已经被很好地解决了:支持HLS的播放器(比如hls.js)原生支持AES-128解密,只要m3u8里的EXT-X-KEY标签配置正确,播放器会自动拉取密钥并完成解密。这意味着对于HLS方案,前端几乎不用写解密代码。

但注意,如果你用了上面那种自定义的整文件加密格式,前端就需要自己实现一套解密逻辑了。Web Crypto API提供了crypto.subtle.importKeycrypto.subtle.decrypt接口,可以实现AES-CBC解密。这种做法不适合大文件,但在数据量小的场景里完全可行。

4. 实操过程的完整流程详解

4.1 环境准备与目录结构设计

第一步是准备运行环境。建议在一个干净的Linux服务器或macOS环境跑这套程序,Windows需要额外处理FFmpeg路径和chmod权限问题,能跑但不推荐。需要的东西:

  • Python 3.8+(推荐3.10以上)
  • FFmpeg 4.4以上版本(带libx264与openssl支持)
  • PyCryptodome库:pip install pycryptodome

推荐的项目目录结构:

video-encryptor/ ├── input/ # 原始视频 ├── transcoded/ # 转码后待加密的视频 ├── encrypted/ # 加密输出目录 │ └── {video_id}/ │ ├── index.m3u8 │ ├── enc.key │ ├── enc.keyinfo │ └── seg_000.ts ... ├── keys/ # 密钥备份目录(对外隔离) └── main.py # 调度主脚本

4.2 完整操作步骤:从原始视频到加密输出

这一条完整流程我手动跑过无数次,每一步的输出和耗时都在预期范围内,直接把命令列出来:

# 1. 转码:统一成H.264/AAC/MP4 ffmpeg -i input/raw_course.mp4 -c:v libx264 -preset medium -profile:v high -crf 23 -maxrate 4M -bufsize 8M -c:a aac -b:a 128k -movflags +faststart transcoded/course_1080p.mp4 # 2. 校验转码文件 ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 transcoded/course_1080p.mp4 # 3. 调用Python脚本完成HLS加密 python main.py encrypt \ --input transcoded/course_1080p.mp4 \ --output encrypted/course_001 \ --segment-time 10 \ --key-uri key.bin

整个加密过程最耗时的是第一步转码,一个1小时的1080P视频用medium预设在主流CPU上大约需要12-20分钟。加密切片步骤由于只做流复制不做编解码,速度非常快,同样是1小时视频,几秒钟就能完成全部分片的加密封装。别看到加密耗时短就放松警惕,密钥管理才是整个流程里最需要严谨对待的一环,建议在脚本执行完后立即将enc.key文件移到Web目录之外的路径,甚至在发布到生产环境时将它放到独立的安全存储服务里。

4.3 验证加密结果是否达标

加密完不能直接上线,必须走一遍完整的验证流程。第一层验证是检查分片文件,用xxd seg_000.ts | head看文件头,正常TS文件以0x47同步字节开头,加密后的TS分片应该是乱码,没有了0x47的特征。第二层验证是模拟播放,先用ffplay打开加密的m3u8验证能正常解密播放,再用hls.js在浏览器里试播。第三层验证是安全边界检查,把加密目录里的TS分片单独取出来,不带密钥直接尝试播放,必须是无法播放的状态。

我曾经遇到过一次比较尴尬的情况:FFmpeg提示加密成功,TS分片数据也确实乱码了,但换成某个旧版播放器依然能直接播放,查了半天发现是老播放器根本不解析EXT-X-KEY标签。这种情况属于播放器兼容性问题,解决办法是建立一份经过充分测试的播放器兼容性清单,并对不支持的播放器做能力检测后友好降级。

5. 常见问题排查与性能调优

5.1 加密后播放黑屏或卡死

黑屏是最容易遇到的播放端问题。一般先检查三件事:第一,enc.keyinfo文件路径是否正确,注意这个文件里写的本地密钥路径是相对于FFmpeg执行时的当前目录而言的,路径写错直接导致FFmpeg无法读取密钥,报错也五花八门;第二,密钥URI在m3u8里的相对路径能否在当前目录下正确解析;第三,网络请求是否被防盗链策略拦了,因为加密后的播放流程中,key请求和分片请求往往是不同的HTTP资源,如果服务器限制Referer,很容易出现m3u8能加载但key请求403。

排查方法是开浏览器DevTools的Network面板,把m3u8、key、TS分片三个请求按时间顺序逐个看,哪一环返回非200就定位到哪一环。

5.2 加密与转码的性能瓶颈分析

这套方案中,转码阶段通常是耗时大头,加密阶段虽然也占CPU,但基本不会成为瓶颈。如果转码速度太慢,可以考虑几条优化路线:用-preset faster换取更快的编码速度;给FFmpeg加-threads 0让它自动利用所有CPU线程;批量视频任务做并发转码时,每核只跑一个FFmpeg实例,别硬塞双进程,否则上下文切换开销反而会导致总吞吐量下降。

加密阶段如果并发量特别大,瓶颈往往在磁盘IO。TS分片是小文件,切片数量动辄几百上千个,大量随机小文件写入会让普通机械硬盘直接卡死,强烈建议在SSD上跑加密任务,或者给加密进程加一层内存盘缓存,全部写完再统一回盘。

5.3 密钥泄露和盗播事件怎么应急处理

没有任何加密方案是绝对安全的,所以还需要设计一套应急响应机制。我的做法是给密钥加“版本号”,每当密钥有泄露风险,就在密钥管理系统里废弃旧版本并发布新版本,用新密钥重新加密视频分片,同时快速下线泄露文件的外链下载。如果要限制单个视频的播放范围,还可以在应用层把获取key的接口加上鉴权,只有登录用户且验证了购买权限,服务器才返回密钥内容。

这套机制的现实意义是:加密防止“批量、无差别”的盗版,配合授权接口则能把盗版限制在“个案、可控”的层面。真有付费用户刻意录屏,这类操作系统级别的攻击不是视频加密本身能解决的,需要结合业务策略(比如内容水印、用户身份追溯)去反制,这也是DRM方案里补充水印的常见原因。

6. 经验心得与后续扩展方向

这套程序从第一版跑通到现在,我最大的体会是:视频加密永远是一个平衡“用户体验”和“安全强度”的工程问题。加密强度上去了,播放器兼容性就变差,用户稍微用个老机型就播不了;加密太弱,又起不到保护作用。用了这么久,我一般建议团队先明确自己的威胁模型——你是怕普通用户白嫖,还是怕同行恶意扒库,还是怕盗版商批量打包转卖。想清楚了,再决定要不要上整文件加密、要不要对接昂贵的商用DRM。

很多朋友问我,既然有现成的云厂商DRM方案(比如阿里云视频加密、腾讯云私有加密),为什么还要自己写源码。我的回答是:自研方案的核心价值不是省钱,而是可控和可裁剪。你可以完全控制密钥的存储位置,可以方便地横向集成到自己的账户体系里,可以针对小众播放场景自由定制。缺点是维护成本由自己承担,适合有一定研发能力的团队;如果只是想快速给视频加个保护层,确实直接用云厂商的加密功能更省心。

最后再分享一个我现在已经在做的扩展方向:把这套加密程序伪装成一个服务化的域名接口,业务方直接调接口传视频URL,几秒钟后拿到加密后的流地址。再接上一个授权回调,播放器请求key之前先调用业务后端做一次实时鉴权,把“视频加密”从技术模块升级成完整的视频授权分发微服务。这个扩展方向现在已经在我的Git私有仓库里跑了三个多月,期间只遇到过一次非预期的密钥路径问题,整体稳定性超预期。如果大家在这条路上踩了其他坑,欢迎多交流,这些源码级的东西只有在真实业务里摔打过才知道哪里最疼。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 2:12:47

CAD图纸粘贴到TinyMCE保持矢量输出的芯片厂实战方案

芯片厂里跑MES、OA、知识库系统的朋友,应该都被同一件事折磨过:工艺工程师把CAD图纸从设计端复制出来,贴到基于TinyMCE编辑器的文档里,前两秒看着没问题,一保存一放大,线条全是锯齿,标注文字糊成…

作者头像 李华
网站建设 2026/9/8 2:11:22

RAG技术全解析:从原理到代码实现的企业知识库搭建指南

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

作者头像 李华
网站建设 2026/9/8 2:10:42

AI模型基准测试与实际表现差异分析及工程实践指南

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

作者头像 李华
网站建设 2026/9/8 2:08:08

STM32+RC522刷卡模块全攻略:从接线、代码到门禁实战排障

简介:STM32RC522刷卡模块工程包面向嵌入式入门开发者与物联网爱好者,是一套软硬件结合的完整非接触式RFID读卡方案。工程以MIFARE卡片ID读取为主线,覆盖RC522驱动、SPI接口初始化、防冲突处理、CRC校验及数据帧解析,能够帮助使用者…

作者头像 李华
网站建设 2026/9/8 2:08:07

一站式硬件测试平台:简化开发板调试工作流

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

作者头像 李华
网站建设 2026/9/8 2:06:55

设计竞赛复盘指南:从落选到提升的评审维度与策略

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

作者头像 李华