阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。
直接回答一个高频故障:当你用
page.get_text()
提取PDF内容准备翻译或归档时,默认输出通常不包含批注(Annotations)。更危险的是,如果直接使用某些“全文提取”模式,批注内容可能被无序地插入正文段落中间,导致语义断裂;或者相反,你以为提取了全部内容,却遗漏了审阅者留在侧边栏里的关键修改意见甚至敏感备注。在文档交接环节,必须将正文与批注作为两个独立的数据流处理,并对批注内容进行显式的安全审查。
为什么批注是独立于正文的隐藏数据层
PDF规范中,页面内容流(Content Stream)与批注字典(Annots Array)在结构上是分离的。正文由文本显示指令构成,而批注是附着在页面上的独立对象,拥有自己的坐标、类型和内容属性。PyMuPDF 的
Page.annots()
方法返回的是一个生成器,它遍历的是这个独立的批注列表,而非页面文本流。
这意味着你不能假设“看到的文字都能被同一种方式提取”。高亮(Highlight)、下划线(Underline)等标记类批注本身不包含文本,它们只记录了对应的原文区域(Quads/Rects),真正的文本需要通过坐标反查正文才能获得;而便签(Text/Note)、自由文本(FreeText)则自带内容字符串。如果不区分类型直接读取
annot.info["content"]
,你会丢失所有标记类批注指向的原文上下文,或者在处理空内容时抛出异常。
合成示例:分层提取与敏感信息拦截
为了演示正确的处理逻辑,我们构造一个包含三种典型批注的合成场景。请注意,以下输入均为代码内构建的合成样本,并非来自真实客户文件,仅用于教学演示。
预期正确输出
应包含三个结构化字段:
1.
正文片段
:仅包含页面原始文本,不含任何批注内容。
2.
批注清单
:每条记录包含类型、位置、作者、时间及内容(如有)。
3.
安全标记
:若批注内容匹配预设敏感规则(如身份证号、内部代号),该条目标记为
redacted=True
,内容替换为占位符。
错误/边界输入及处理
:
*
空内容批注
:某些阅读器创建的墨迹(Ink)或图形批注没有文本属性。直接访问
.info["content"]
可能返回空字符串或None。代码必须做防御性检查,不能假设所有批注都有可读文本。
*
旋转页面坐标
:如果页面被旋转了90度,批注的矩形坐标仍然是基于未旋转页面的。若直接用该坐标去裁剪或定位正文,位置会完全错乱。必须先通过
page.rotation_matrix
或
page.derotation_matrix
进行坐标变换。
*
孤儿对象
:如果在遍历过程中关闭了文档或重新加载了页面,当前的 annot 对象会立即失效。必须在单次页面生命周期内完成所有读取操作。
Python 实现:安全的批注遍历与校验
以下示例使用 PyMuPDF 在内存中构建测试PDF,演示分层提取与基础脱敏逻辑。依赖库为
PyMuPDF
(即
pymupdf
包),请在锁定的依赖环境中运行,版本需 >=1.23.0 以支持完整的 annot 属性访问。本代码未执行网络操作或文件写入,仅为逻辑演示。
import fitz # PyMuPDF
import re
# 合成示例:仅在内存中构造,无需外部文件
def create_sample_pdf():
doc = fitz.open()
page = doc.new_page()
# 插入正文
page.insert_text((72, 72), "Confidential Project Alpha Report", fontsize=12)
# 添加便签批注(含敏感信息模拟)
page.add_text_annot((200, 72), "Reviewer: SSN 123-45-6789 approved.")
# 添加高亮批注(无自身文本,关联正文)
highlight = page.add_highlight_annot(fitz.Rect(72, 60, 250, 80))
# 必须调用update()使info变更生效
highlight.set_info(title="Editor", content="Check this term")
highlight.update()
return doc
def extract_with_safety(page, sensitive_pattern=r"\d{3}-\d{2}-\d{4}"):
body_text = page.get_text("text")
annotations = []
# 显式遍历批注生成器
for annot in page.annots():
info = annot.info
# 防御性获取内容,处理None或空字符串
content = info.get("content", "") or ""
# 边界处理:敏感词检测与脱敏
is_sensitive = bool(re.search(sensitive_pattern, content))
safe_content = "[REDACTED]" if is_sensitive else content
annotations.append({
"type": annot.type[1],
"author": info.get("title", "Unknown"),
"content": safe_content,
"is_redacted": is_sensitive,
# 注意:此为未旋转坐标系,若页面有旋转需配合rotation_matrix使用
"rect": list(annot.rect)
})
return body_text, annotations
# 执行校验
doc = create_sample_pdf()
page = doc[0]
text, annots = extract_with_safety(page)
assert "SSN 123-45-6789" not in str(annots), "敏感信息未被拦截"
assert any(a["type"] == "Highlight" for a in annots), "标记类批注丢失"
assert len(text.strip()) > 0, "正文提取失败"
print(f"Extracted {len(annots)} annotations safely.")
此代码展示了防御性编程的核心:不信任批注内容的纯净度,不假设批注类型的完整性。
assert
语句用于验证逻辑预期,但请注意这仅是代码演示,不代表对所有PDF变体的端到端效果保证。
验收标准与人工审查边界
自动化提取无法覆盖所有边缘情况。下表列出了实际交接中应关注的验收点,区分了代码可处理范围与必须人工介入的场景。
观察现象
代码层面处理
人工审查/决策
批注内容为空但存在图标
记录类型为“Ink”或“Stamp”,内容留空
确认是否为签名或审批章,决定是否保留图像快照
批注位置超出CropBox
坐标仍有效,但渲染不可见
判断是否为历史遗留废弃批注,通常可忽略
富文本批注含HTML标签
提取原始字符串,保留标签
翻译前需清洗标签或转换为纯文本,避免译文夹杂代码
批注作者名为乱码
按原样提取,不做编码猜测
核对源文件元数据,确认是否为特定编码的姓名
嵌套回复线程
PyMuPDF返回扁平列表
需根据ParentID重建对话树,否则上下文丢失
不适用范围
:本方案不适用于加密且未提供密码的PDF、损坏的PDF结构、以及使用非标准私有扩展定义的批注类型。遇到这些情况,脚本可能静默跳过或抛出底层C++异常,必须由人工使用专业工具打开源文件确认。
参考资料与下一步
PyMuPDF Page.annots() 官方文档
下一步行动
:选取一份包含混合批注类型的历史项目文件,运行上述校验脚本,统计其中需要人工审查的条目比例。若比例超过预期,应在交接流程中增加预处理环节,而非依赖下游工具自动兜底。
将这份检查清单用于文档翻译
先把页面顺序、来源位置与文本完整性检查清楚,再把同一份脱敏样本交给翻译流程。译后仍要检查标题、正文和图注是否正确对应,不能只看文字是否通顺。
OmniDoc(产品入口)
提供多格式文档翻译入口,官网介绍了 OCR、术语表与双语对照功能。对希望直接从文件开始处理的团队,这是一条值得用实际样本评估的路径。请使用有权上传的脱敏文件,并用本篇的检查项复核结果;这里不声称 OmniDoc 使用了本文的开源库或内部算法,也不以示例代替产品性能测试。
- 1 -
更新于 2026-10-08