PDF 水印去除的代码实现:检测、指令级删除与保存净化全流程
上一篇《PDF 水印去除的技术迷宫为什么 90% 的工具都会失败》讲了原理层水印在技术上分六层主流方法各有射程边界。这篇讲工程层——用代码把检测 → 定位 → 分类 → 处理 → 验证这条管线完整实现出来。先说背景这套管线不是纸上谈兵——它是我们开发PDFnotedpdfnoted.com的 PDF Clean 文档清理工作台时的实现框架下面按我们的工程实践展开。先说全文最重要的结论去水印在代码层面不是一个函数是一条管线。跳过检测直接删是所有开源脚本翻车的头号原因——你看到的删除成功可能只是把水印从可见层挪到了增量更新的历史里一个恢复工具就能把它翻出来。文中代码为伪代码基于 PyMuPDF 与 pikepdf 的真实 API 风格书写省略异常处理与边界细节实际使用请以你所装版本的官方文档为准。一、动手之前建立对象图心智模型写代码处理 PDF脑子里必须有一张对象图。PDF 文件本质上是四种对象组成的网字典、流、数组、字符串通过xref交叉引用表互相引用。和页面相关的一切都挂在这三棵子树下页面元素对象图位置代码切入点页面内容页面字典的/Contents一个或多个内容流读流、解析操作符、重写流水印图片 / 表单/Resources /XObject下的命名资源枚举资源、删除引用或资源本身文字字体/Resources /Font字体特征匹配识别文本层水印透明/混合状态/Resources /ExtGState水印寄生的高发地注释、印章/Annots数组遍历注释、按类型/内容删除元数据文件尾部的 Info 字典 XMP 流双重重写内容流本身是一串操作符q/Q保存恢复图形状态、cm变换矩阵水印旋转就靠它、BT/ET文本块、Tf选字体、Tj/TJ画文字、gs设透明度、Do画一个命名 XObject。定位水印 在对象图上做搜索。六层水印对应六种搜索策略这就是分类处理的代码含义。二、检测层先出报告再动手检测层的任务输出一份水印清单——每页有什么类型的水印、挂在哪个对象上、签名叫什么。这份清单既是处理计划的输入也是给用户的交付物动手前告诉用户边界在哪。2.1 枚举对象按类型统计# 伪代码全文档对象体检 doc fitz.open(path) for xref in range(1, doc.xref_length()): obj_type doc.xref_get_key(xref, Type) # e.g. (name, /XObject) subtype doc.xref_get_key(xref, Subtype) # e.g. (name, /Form) if obj_type indicates XObject: record(xref, subtype, is_formsubtype/Form, ...)2.2 页面资源清单for page in doc: # 每页引用了哪些命名资源 fonts page.get_fonts() # [(xref, ext, type, basefont, name, encoding), ...] images page.get_images(fullTrue) contents page.get_contents() # 内容流 xref 列表可能一页多条2.3 内容流特征扫描文本层水印的三个内容流特征透明度gs配/CA ca、旋转cm矩阵带 sin/cos 分量、跨页共现同一个Do名字在每一页都出现。三个特征同时命中的水印嫌疑极高for page in doc: stream read_and_concat(page.get_contents()) for name in find_named_xobject_calls(stream): # 所有 name Do if name used on every page: # 跨页共现 mark as watermark_candidate(name)2.4 输出检测报告report { file: path, pages: doc.page_count, watermarks: [ {layer: xobject_image, name: /WM, xref: 42, pages: [0..N-1], signature: 45deg_rotatealpha0.15}, {layer: text, font: ArialMT, size: 60, color: #FF0000, pages: [0, 3, 7], signature: rotate45}, {layer: metadata, keys: [Author, Producer]}, ] }为什么产品必须先检测后处理三个理由。一、寄生指令让最后一段内容流就是水印的假设失效不检测就删删掉的可能是正文二、水印字体可能被正文共用字体子集复用删资源会误伤三、检测报告是免责依据——用户授权你处理的是哪一页的哪个水印白纸黑字。这份清单就是 PDFnoted 输出给用户的水印检测报告的原型——用户上传后先看到它再决定清理哪些动手之前边界透明。三、文本层两条技术路线文本层水印选中能复制的CONFIDENTIAL、斜体大字、白字有两条代码路线适用场景完全不同。3.1 路线 A红action API区域擦除高层封装PyMuPDF 的红action 机制先在区域上画一个待擦除标注再统一执行。适合按位置批量擦除不需要理解内容流page.add_redact_annot(rect) # rect 水印包围盒可由文本检测得到 page.apply_redactions() # 1.23执行本页所有红actionapply_redactions有三个参数族决定顺带处理什么参数取值语义选择建议text移除 / 保留与区域相交的文字默认移除只想擦图就保留graphics移除与区域相交/被覆盖的矢量线水印带边框时选被覆盖才移除images移除 / 模糊 / 保留与区域相交的图片擦文字水印选保留避免误伤插图局限它是遮盖式的——文字从内容流移除但页眉页脚被擦后正文不会自动上移复杂排版会留白边对烧进主图的水印无能images参数只能移除整个图片对象不能做局部修复。3.2 路线 B指令级删除签名匹配精准手术路线 B 不画遮挡而是直接从内容流里删掉画水印的那几条操作符。核心是四元组签名——真实水印在内容流里的指纹签名维度水印典型值正文典型值字体名专用水印字体或与正文共用正文字体字号异常大60pt或异常小1pt 白字9-12pt颜色固定单色常配透明度黑色/多色变换矩阵带旋转45°、跨页相同无旋转、随排版变化伪代码# 第一步从文本抽取层拿水印文本签名 for page in doc: for span in page.get_text(dict) spans: sig (span.font, span.size, span.color, span_matrix) if is_watermark_sig(sig): # 四元组判定 watermark_spans[page.number].append(span) # 第二步内容流里按签名过滤文本操作符 for page in doc: for xref in page.get_contents(): raw doc.xref_stream(xref) tokens tokenize_content_stream(raw) # 操作符 操作数 kept [t for t in tokens if not (t is text_drawing_op and match(t, watermark_spans))] doc.update_stream(xref, serialize(kept)) # 重写流三个实现细节决定成败字符串转义内容流里()\要转义十六进制字符串形如4F6E——按字面量匹配必错先解码再比。TJ 数组现代 PDF 几乎不用Tj而用TJ [CON -20 FIDENTIAL] TJ水印文字被切成碎片、中间夹字距调整。匹配必须按数组重组后的完整字符串比不能逐元素比。状态残留删了BT..ET块之后块外的gs透明度、cm旋转操作符还挂着。清一次文本水印要把这三类操作符一起纳入签名匹配否则旋转和透明状态会传染给后续正文。四、图像层XObject 移除与递归陷阱水印图片是每页贴一遍的命名 XObject。检测逻辑天然简单跨页共现的Do调用水印候选。# 统计每个 XObject 名字出现在哪些页 usage defaultdict(set) for pno, page in enumerate(doc): for img in page.get_images(fullTrue): usage[img.xref].add(pno) # 内容流里的 name Do 也要统计XObject 不一定是图片可能是 Form for name in find_named_xobject_calls(read(page.get_contents())): usage[resolve_name_to_xref(page, name)].add(pno) watermark_xrefs {x for x, pages in usage.items() if len(pages) doc.page_count}移除有两条路语义完全不同# 路线 1删引用——从每页内容流里删掉 /WM Do 这一行 # 优点资源对象还在其他页/其他文档若共用不会误伤 # 缺点文件里水印图片数据还在需配合保存净化见第六节 # 路线 2删资源——从 /Resources /XObject 字典里摘掉名字 # 优点彻底缺点若被多处引用会连锁错误必须先确认共现统计4.1 递归陷阱水印嵌在 Form XObject 里很多正规水印系统不把图片直接贴页面上而是打包成一个Form XObject带自己的资源字典和内容流页面只留一个Do。更深的还有Form 里再套 Form透明度在 ExtGState 里ExtGState 又被多个 Form 共享。只删页面上的Do资源树完好无损——文件照样能翻出水印原图。def strip_watermark_resources(resources, watermark_names, depth0): # 递归清理先摘本层资源再钻进每个 Form XObject 清它的资源 for name in watermark_names: if name in resources.XObject: del resources.XObject[name] for sub in resources.XObject.values(): if sub.get(/Subtype) /Form: strip_watermark_resources(sub.Resources, watermark_names, depth1)4.2 pikepdf 的资源级手术PyMuPDF 强在高层封装pikepdf 强在保留完整对象模型基于 qpdf 内核适合资源字典级别的精细操作且改写无损pdf pikepdf.open(path) for page in pdf.pages: xobjects page.Resources.XObject for name in list(xobjects.keys()): if is_watermark(xobjects[name], cross_page_stats): del xobjects[name] pdf.save(out, object_stream_mode...) # 全量重写见第六节五、注释层与元数据层5.1 注释层for page in doc: for annot in page.annots(): if annot.type[1] Stamp or match(annot.info, watermark_keywords): page.delete_annot(annot) # 注意先收集再删边遍历边删会丢元素5.2 元数据层双写结构PDF 的元数据存在两处文件尾部的 Info 字典doc.set_metadata({})清空和 XMP 元数据流doc.del_xml_metadata()。只清 Info 不清 XMP专业工具一扫就现形。另外注意/ID文档标识——它不存作者信息但可用于跨文档追踪敏感场景一并重置。doc.set_metadata({}) # Info 字典 doc.del_xml_metadata() # XMP 流 # 隐藏文本层检查每页是否存在渲染不可见的小字号/白色文本见 3.2 签名匹配六、保存即净化增量更新陷阱最多人翻车的一节为什么代码显示删除成功文件里水印还在因为 PDF 的默认保存方式是增量更新新内容追加到文件尾部旧对象一个字节都没动。原对象、原内容流、甚至删除前的历史版本全都完整保留在文件里。任何PDF 历史版本恢复工具都能把它读出来。正确做法全量重写 垃圾回收。保存参数作用不设的后果garbage4清除所有无引用对象合并重复对象删掉的水印对象仍是无引用但存在可被 forensic 提取cleanTrue重写内容流、清理未用资源残留空操作符、失效资源引用deflateTrue压缩流文件膨胀且明文水印字符串可被 greplinearize线性化网页快速查看按需与净化无关doc.save(out, garbage4, cleanTrue, deflateTrue) # pikepdf 侧等价物pdf.save(out, ...) 默认即全量重写注意别用 incrementalTrue验证层删干净了没有代码说了算处理完必须验证这是管线的最后一环也是产品给用户的处理报告的数据来源# 1. 文本 diff重开文档对比处理前后 get_text()确认水印文本消失、正文零损失 # 2. 二进制 grep在输出文件里搜水印明文字符串deflate 后应先解压或搜压缩前基线 # 3. 对象计数xref_length 前后对比确认水印对象被 garbage 回收 # 4. 渲染哈希逐页 get_pixmap() 做感知哈希确认视觉变化只发生在水印区域 # 5. 历史版本扫描检查文件尾部是否还存在旧增量段save 后追加写入会重新引入七、批量管线把单文档逻辑升级成生产管线for file in walk(input_dir, *.pdf): try: report process_one(file) # 检测→分类→处理→验证任一环失败即记录 report.status ok if verify(report) else needs_review except Exception as e: report {status: failed, error: str(e)} # 错误隔离不中断批次 write_json(report, output_dir / f{file.stem}.json)三个工程决策错误隔离一份坏文档不能杀死整批、报告先行每份文档的处理结果落盘可审计、失败升级验证不过的进needs_review交人工而不是静默通过。八、五个工程陷阱踩一个翻车一个陷阱症状对策寄生指令删最后一段内容流后水印依旧全流解析 签名匹配不做位置假设Form XObject 嵌套页面上的 Do 删了资源树完好递归清理 Resources4.1字体子集共用删水印字体后正文缺字删除前统计字体被哪些文本引用扫描件射程外内容流方案对扫描件完全无能类型分流烧进主图的走视觉修复不走本条管线增量更新历史删完的文件能被恢复工具翻出旧版garbage4 clean全量重写保存后禁止再追加九、从脚本到产品PDFnoted 是怎么落地的这套管线产品化时有四个决策直接决定专业度。这也是我们做PDFnoted 的 PDF Clean 工作台时的核心设计逐个对回去先出检测报告再让用户动手——PDFnoted 上传后先扫描按类型列出文档里的水印、批注、印章、隐藏对象与元数据用户勾选要清理的再处理边界透明免责有据分类路由不一键通吃——文本层走指令级删除、XObject 走资源手术、元数据走双重写走哪条路线写在检测报告里用户看得见验证输出随文件交付——用户拿到的不只是干净版还有清掉了什么、哪些区域是重建的处理报告隐私边界默认本地——合同财务文档的基础结构清理在浏览器本地完成只有用户主动开启高级视觉修复时才上传必要页面清理完无需重新上传可直接继续编辑、转换或 AI 合规审查。如果你不想写代码只想把这篇的管线用起来**PDFnotedpdfnoted.com**免费版可以直接体验完整流程——上传、六层检测、分类清理、输出验证轻度使用不收费。适合偶尔处理扫描件合同的职场人也适合想先看效果再决定要不要自建管线的工程师。FAQQ1PyMuPDF 和 pikepdf 怎么选PyMuPDFMuPDF 绑定高层封装全文本抽取、渲染、红action 开箱即用适合快速搭管线pikepdfqpdf 绑定保留完整对象模型资源级手术、无损改写更强适合精细活。生产环境常见组合pikepdf 做对象手术PyMuPDF 做检测与验证。Q2指令级删除会破坏文档吗会如果签名匹配不严谨。误删正文操作符、删掉共用字体、清掉旋转状态影响后续内容都是真实事故。所以匹配要四元组签名 全文验证第六节五步不要只信删完了。Q3为什么删完水印文件里还能搜到水印字符串三个原因增量保存没做全量重写clean没开内容流里留下明文残片水印字体子集的 ToUnicode 映射没清理。逐个对照第六节的保存参数表。Q4扫描件能用这套代码处理吗不能。扫描件每页就是一张位图水印和正文像素级混合内容流里什么都没有。这类要走视觉修复路线AI inpainting且要接受重建≠还原修复区域必须人工核对合同类文档的关键条款尤其如此。Q5去水印合法吗取决于文档权利状态和用途。处理自己生成或已获授权的文档没问题去除他人版权材料的水印用于分发在多数法域都有法律风险。工具能做的是检测先行、留痕可审计把权利判断留给用户——这也是所有正规文档工具加合规声明的原因。结语去水印这件事脚本和产品的差距不在算法在管线思维检测、定位、分类、处理、验证五步一步不能少增量保存、寄生指令、字体共用三个坑一个都躲不掉。把这篇的伪代码按管线串起来你得到的不只是一个去水印脚本而是一个任何文档处理场景都能复用的工程框架。你在实际项目里处理过什么棘手的 PDF 结构评论区聊我挑典型的写进下一篇。不想写代码直接体验这条管线PDFnoted 的 PDF Clean 工作台把本文的检测 → 分类 → 处理 → 验证做成了开箱即用的流程上传即出六层水印检测报告文本层 / XObject / 烧入主图 / 注释 / 元数据 / 暗水印基础结构清理在浏览器本地完成扫描件红章走 AI 视觉修复输出附处理报告。免费版可跑完整流程适合偶尔处理的职场用户也适合验证效果后再自建管线的工程师。入口**https://pdfnoted.com/pdf-clean**也可从 pdfnoted.com 首页进入上一篇《PDF 水印去除的技术迷宫为什么 90% 的工具都会失败》原理层六层水印结构与主流方法射程分析本文仅用于你有权处置的文档。涉及他人版权材料的处理请求请先获得授权。

相关新闻

VOC格式吸烟检测数据集解析:从标注规范到YOLO转换避坑指南

VOC格式吸烟检测数据集解析:从标注规范到YOLO转换避坑指南

简介:本资源是一套面向计算机视觉初学者与算法工程师的吸烟行为检测专用数据集,聚焦室内与室外多场景下人员吸烟动作识别任务,可直接用于YOLO、Faster R-CNN等目标检测模型的训练与验证。数据集共2000个文件,包含1507个VOC格式XML…

2026/10/14 14:52:19 阅读更多 →
高校科研IC样品封装打样:从设计到流片的关键桥梁与产业化思考

高校科研IC样品封装打样:从设计到流片的关键桥梁与产业化思考

高校科研IC样品封装打样的现状与重要性 在集成电路产业高速发展的今天,高校作为基础研究与原始创新的策源地,其科研项目对芯片验证的需求日益迫切。从微电子实验室的创新电路设计到新型材料器件的功能验证,高校科研IC样品封装打样已成为连接学…

2026/10/14 14:52:19 阅读更多 →
Java并发编程核心:线程模型、生命周期、线程池与线上排查实战

Java并发编程核心:线程模型、生命周期、线程池与线上排查实战

线程是Java并发编程的基石,也是从初级走向高级必须跨过的一道坎。很多同学写了好几年业务代码,线程也用了不少,但一遇到线上问题就抓瞎:线程池参数怎么配?CPU飙高怎么排查?死锁怎么定位?这些背后…

2026/10/14 14:52:18 阅读更多 →

最新新闻

2026跨境电商新规落地后,这5个隐藏坑千万别踩,有人已亏光全年利润

2026跨境电商新规落地后,这5个隐藏坑千万别踩,有人已亏光全年利润

新规实施后的第一个完整季度,不少卖家发现账面利润和预期出现了明显偏差。有人归因于流量成本,有人怀疑汇率波动,但真正的问题往往藏在合规细节里。以下五个容易被忽略的环节,正在悄悄侵蚀利润空间。 一、低申报的“惯性操作”已进…

2026/10/14 15:34:27 阅读更多 →
毕业论文耗时最多的不是写作,而是反复格式排版!okbiye 一站式搞定论文规范

毕业论文耗时最多的不是写作,而是反复格式排版!okbiye 一站式搞定论文规范

很多同学花两三个月完成论文内容撰写,查重、逻辑修改全部完成,却栽在格式排版这最后一关。导师简单一句 “格式不符合学校规范”,整篇文档就要反复调整,页眉、页码、目录、图表、参考文献,随便一处细节出错&#xff0c…

2026/10/14 15:34:27 阅读更多 →
AVO正演C++源码实战:从模型到CDP道集与噪音控制

AVO正演C++源码实战:从模型到CDP道集与噪音控制

简介:这份资源面向石油物探方向的研究生及地震数据处理初学者,聚焦AVO正演模型实验与地震数据正演这一核心课题,帮助读者理解AVO响应特征、角度域分析以及CDP道集的形成过程。压缩包内共4个文件,全部为cpp源代码,整体约…

2026/10/14 15:34:27 阅读更多 →
Farm CLI 命令与选项全解:farm dev、build、watch、preview、clean 的参数详解与源码实现

Farm CLI 命令与选项全解:farm dev、build、watch、preview、clean 的参数详解与源码实现

前端构建构建工具开发工具 【免费下载链接】farm Extremely fast Vite-compatible web build tool written in Rust 项目地址: https://gitcode.com/gh_mirrors/fa/farm 点击查看 免费下载 本文基于 Farm 仓库的 CLI 文档 cli-api.md,系统讲解 farmfe/c…

2026/10/14 15:34:27 阅读更多 →
dplyr filter() 与 filter_out() 错误处理机制与快照测试体系深度解析

dplyr filter() 与 filter_out() 错误处理机制与快照测试体系深度解析

数据分析 【免费下载链接】dplyr dplyr: A grammar of data manipulation 项目地址: https://gitcode.com/gh_mirrors/dp/dplyr 点击查看 免费下载 导读 本文以 dplyr 仓库中的快照文件 tests/testthat/_snaps/filter.md 为核心,系统剖析 filter() 与 …

2026/10/14 15:34:27 阅读更多 →
2026年跨境卖家集体破防:这5个冷门类目正在闷声收割老外,别再死磕低价了

2026年跨境卖家集体破防:这5个冷门类目正在闷声收割老外,别再死磕低价了

当不少卖家还在宠物、服饰、3C配件里卷价格时,一些更细分的需求正在海外市场悄悄放量。它们不算热门,却对应着明确的生活场景和稳定的复购逻辑。与其在红海里反复比价,不如看看这五个类目为什么能跑出利润空间。 一、庭院微生态用品 欧美独栋…

2026/10/14 15:33:26 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29: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/13 22:01:27 阅读更多 →
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/14 7:53:34 阅读更多 →