pypdf XmpInformation 类完全指南:读写 PDF 的 XMP 元数据
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
导读
PDF 文件除了页面内容之外,还携带一套结构化的元数据——XMP(Extensible Metadata Platform,可扩展元数据平台)。pypdf 通过XmpInformation类把这份隐藏在 PDF 根对象/Metadata流中的 XML 数据,包装成一系列可读可写的 Python 属性。读完本文,你将掌握:如何从PdfReader/PdfWriter中获取 XMP 元数据、如何读取和修改 Dublin Core(dc:*)、Adobe XMP(xmp:*)、PDF(pdf:*)、XMPMM(xmpMM:*)以及 PDF/A(pdfaid:*)各命名空间的字段、如何读取自定义元数据属性,并理解 pypdf 在解析 XMP 时的安全防护与底层实现机制。
本文以 pypdf 官方 API 文档 docs/modules/XmpInformation.rst 中定义的XmpInformation类为骨架展开,结合 pypdf/xmp.py 的完整源码与 tests/test_xmp.py 的测试用例进行纵深讲解。
XmpInformation 是什么
XmpInformation是 pypdf 中专门表示 XMP 元数据的类,定义于 pypdf/xmp.py,它同时继承自XmpInformationProtocol(协议约束)与PdfObject(PDF 对象基类)。
在 PDF 文件内部,XMP 元数据以一个类型为/Metadata、子类型为/Subtype /XML的流对象形式挂在文档目录(root)上。XmpInformation将这段 XML 流解析成 DOM 树(self.rdf_root),再通过一系列属性把 RDF 三元组映射成易用的 Python 数据结构。
构造时机上,通常不需要直接实例化它,而是通过三个入口间接获取:
PdfReader.xmp_metadata——只读访问,见 pypdf/_reader.py;PdfWriter.xmp_metadata——可读可写,见 pypdf/_writer.py;DictionaryObject.xmp_metadata——底层实现,从根对象字典中读取/Metadata键,见 pypdf/generic/_data_structures.py。
from pypdf import PdfReader reader = PdfReader("document.pdf") xmp = reader.xmp_metadata # 无 XMP 元数据时返回 None if xmp is not None: print(xmp.dc_title)类文档中明确声明:若 XML 无效,构造时会抛出PdfReadError。这一点在 pypdf/xmp.py 的实现中得到印证——解析失败(AttributeError或ExpatError)会被统一包装为PdfReadError,缺少rdf:RDF根元素同样会报错。
支持的命名空间与常量
XmpInformation内部维护了一张命名空间 URI 到前缀的映射表(pypdf/xmp.py),所有属性都围绕这些命名空间展开:
| 常量 | 命名空间 URI | 前缀 | 对应属性组 |
|---|---|---|---|
RDF_NAMESPACE | http://www.w3.org/1999/02/22-rdf-syntax-ns# | rdf | 容器结构(Bag/Seq/Alt) |
DC_NAMESPACE | http://purl.org/dc/elements/1.1/ | dc | dc_*系列 |
XMP_NAMESPACE | http://ns.adobe.com/xap/1.0/ | xmp | xmp_*系列 |
PDF_NAMESPACE | http://ns.adobe.com/pdf/1.3/ | pdf | pdf_*系列 |
XMPMM_NAMESPACE | http://ns.adobe.com/xap/1.0/mm/ | xmpMM | xmpmm_*系列 |
PDFAID_NAMESPACE | http://www.aiim.org/pdfa/ns/id/ | pdfaid | pdfaid_*系列 |
PDFX_NAMESPACE | http://ns.adobe.com/pdfx/1.3/ | pdfx | custom_properties |
其中PDFX_NAMESPACE是 Adobe 官方文档中专门用于存放"自定义元数据"(custom metadata)的命名空间:元素名即键名,内容即值;当键中包含非法 XML 标识符字符时,会用\u2182(ROMAN NUMERAL TEN THOUSAND)加 4 位 Unicode 十六进制编码进行转义,例如键"my car"会变成my\u21820020car。源码注释明确建议:除非必须兼容旧文件,否则应避免使用 pdfx 命名空间,优先采用自定义 schema 和语义明确的 XML 元素(pypdf/xmp.py)。
Dublin Core 元数据:dc_*属性
Dublin Core(都柏林核心)是 XMP 中最常用的一组元数据。XmpInformation为每个dc:*元素提供了成对的 getter/setter 属性。按其数据类型可划分为四类:
单值属性(rdf:Description直接子元素)
dc_coverage:资源覆盖范围或范围的文字描述dc_format:资源的 MIME 类型dc_identifier:资源的唯一标识符dc_source:当前资源派生自的原始资源的唯一标识符
无序数组(rdf:Bag)
dc_contributor:除作者外的其他贡献者dc_language:资源使用的语言dc_publisher:发布者名称dc_relation:与其他文档关系的文字描述dc_subject:描述资源主题的关键词/短语dc_type:文档类型的文字描述
有序数组(rdf:Seq)
dc_creator:按优先级排序的作者姓名数组dc_date:对资源有意义的日期数组,统一以 UTC 时区的datetime.datetime返回
多语言备选(rdf:Alt,按语言键控的字典)
dc_title:资源标题,键为语言代码(如"x-default"、"en")dc_description:资源内容的多语言文字描述dc_rights:用户对资源所拥有权利的多语言文字描述
以dc_creator为例(pypdf/xmp.py):
# 读取 xmp.dc_creator # ['John Doe'] # 写入 xmp.dc_creator = ["Alice", "Bob"]读取时,底层通过_get_seq_values从rdf:Seq容器中提取rdf:li项;该函数还包含一个重要的容错分支(pypdf/xmp.py):部分应用程序违反 XMP 标准,把本应是rdf:Seq(有序数组)的dc:creator写成了rdf:Bag(无序数组),pypdf 会将其作为回退方案一并接受,对应测试见 tests/test_xmp.py 的test_dc_creator__bag_instead_of_seq。
dc_date的读取还会经过_converter_date转换(pypdf/xmp.py),它用正则解析 ISO 8601 日期字符串,支持YYYY、YYYY-MM、YYYY-MM-DD、带时间与毫秒、以及Z或±HH:MM时区偏移的完整形式;时区偏移会被换算为 UTC。写入时_format_datetime_utc会把带时区的datetime先转成 UTC,再格式化为形如2023-12-25T10:30:45.000000Z的字符串(pypdf/xmp.py)。
PDF 命名空间元数据:pdf_*属性
对应pdf:*元素,全部为单值字符串属性:
pdf_keywords:文档关键词(对应pdf:Keywords)pdf_pdfversion:PDF 文件版本,如1.0、1.3(对应pdf:PDFVersion)pdf_producer:生成该 PDF 的软件名称(对应pdf:Producer)
这些字段与PdfReader.metadata(文档信息字典/Info)中的传统键存在对应关系,但存储位置不同:部分 PDF 使用 XMP 元数据流而非文档信息字典,metadata属性不会读取这些流,必须通过xmp_metadata访问(pypdf/_writer.py 中的 docstring 明确说明了这一点)。
Adobe XMP 命名空间元数据:xmp_*属性
对应xmp:*元素,全部为单值属性:
xmp_create_date:资源最初创建的日期时间,返回 UTCdatetime对象(xmp:CreateDate)xmp_modify_date:资源最后修改的日期时间(xmp:ModifyDate)xmp_metadata_date:元数据本身最近一次变更的日期时间(xmp:MetadataDate)xmp_creator_tool:创建资源时首次使用的工具名称(xmp:CreatorTool)
这三个日期属性的 setter 都接受datetime对象(内部自动格式化为 UTC 字符串)或None(删除该字段),对应实现见 pypdf/xmp.py。
XMPMM 命名空间元数据:xmpmm_*属性
XMP Media Management(媒体管理)命名空间用于文档版本与衍生关系的追踪:
xmpmm_document_id:该资源所有版本与衍生形式的公共标识符(xmpMM:DocumentID)xmpmm_instance_id:文档某一特定实例的标识符,每次文件保存都会更新(xmpMM:InstanceID)
典型取值形如uuid:ca96e032-c2af-49bd-a71c-95889bafbf1d,测试用例见 tests/test_xmp.py。
PDF/A 合规元数据:pdfaid_*属性
对应pdfaid:*元素,用于声明文档符合的 PDF/A 归档标准:
pdfaid_part:符合的 PDF/A 标准部分(如1、2、3)pdfaid_conformance:符合级别(如A、B、U)
测试用例 tests/test_xmp.py 验证了带 PDF/A 元数据的文件(021-pdfa/crazyones-pdfa.pdf)能正确读出pdfaid_part == "1"、pdfaid_conformance == "B",而无 PDF/A 元数据的文件则返回None。pypdf 的 PDF/A 合规性分析还依赖该模块,相关文档可参考 docs/user/pdfa-compliance.md。
自定义属性:custom_properties
custom_properties属性(pypdf/xmp.py)返回一个字典,包含 pdfx 命名空间下所有自定义元数据键值对。读取时会自动把\u2182转义序列还原为原始字符(只对格式合法的转义做还原,遇到格式异常会跳过而非崩溃):
xmp.custom_properties # 例如:{'Style': 'FooBarStyle', 'other': 'worlds', '⏰': 'time'}该属性是惰性计算的——首次访问时缓存到_custom_properties,后续直接返回。测试用例 tests/test_xmp.py 和样本测试 tests/test_xmp.py 均验证了这一行为。
底层解析机制与缓存
安全的 XML 解析器_XmpBuilder
XmpInformation使用自定义的_XmpBuilder(继承自ExpatBuilderNS,pypdf/xmp.py)解析 XMP 流,它做了两件事:
- 拒绝一切实体声明:
custom_entity_declaration_handler直接抛出ExpatError,从根源上阻断 XXE(外部实体注入)攻击。Python 标准库的 libexpat 默认限制只能拦截指数级实体膨胀,无法拦截二次方级膨胀造成的巨大内存占用,因此 pypdf 自己实现了这一层防护; - 限制元素总数:
start_element_handler每解析一个元素就计数,超过xmp_maximum_element_count(默认 100,000)即抛LimitReachedError。
对应测试覆盖了外部实体、指数膨胀、超大流和超量元素四种攻击场景,见 tests/test_xmp.py。
元素查找与缓存
解析后的 DOM 树保存在self.rdf_root中,get_element(about_uri, namespace, name)和get_nodes_in_namespace(about_uri, namespace)提供了面向命名空间的通用查询接口(pypdf/xmp.py),可供需要读取非标准命名空间(如 TIFF)的进阶用法使用——测试中的get_all_tiff正是利用get_nodes_in_namespace读取http://ns.adobe.com/tiff/1.0/命名空间的例子(tests/test_xmp.py)。
所有 getter 都经过self.cache字典做结果缓存(以命名空间和元素名为键),重复读取同一属性不会重新遍历 DOM。
四种取值策略
属性 getter 底层对应四种取值函数,理解了它们就理解了所有属性返回类型的由来:
| 函数 | 容器类型 | 返回值 |
|---|---|---|
_get_single_value | 直接子元素/属性 | str或转换后的单值 |
_getter_bag | rdf:Bag | list[str](无序) |
_get_seq_values | rdf:Seq(回退rdf:Bag) | list(有序) |
_get_langalt_values | rdf:Alt | dict(键为xml:lang) |
_get_single_value同时兼容属性节点(attribute)与元素节点(element)两种 XMP 写法——测试样本中dc:source就是以属性形式存储的(tests/test_xmp.py)。
创建、修改与写回
从零创建:XmpInformation.create()
类方法create()(pypdf/xmp.py)基于模块内预定义的_MINIMAL_XMP模板(pypdf/xmp.py)创建一个空白的XmpInformation实例,模板已声明全部标准命名空间,x:xmptk标记为pypdf:
import pypdf from pypdf import PdfWriter xmp = pypdf.xmp.XmpInformation.create() xmp.dc_title = {"x-default": "我的文档"} xmp.dc_creator = ["作者甲", "作者乙"] xmp.xmp_create_date = datetime.now(timezone.utc) writer = PdfWriter() writer.xmp_metadata = xmp writer.add_blank_page(width=200, height=200) with open("out.pdf", "wb") as f: writer.write(f)创建后的对象所有字段均为空:dc_title == {}、dc_creator == []、xmp_create_date is None(tests/test_xmp.py)。
写入机制
每个属性 setter 都会调用对应的_set_*_values函数(如_set_single_value、_set_bag_values、_set_seq_values、_set_langalt_values,见 pypdf/xmp.py),其流程为:清除缓存 → 获取或创建rdf:Description节点(_get_or_create_description)→ 删除同命名空间下旧的同名元素 → 按容器类型构建新的 XML 节点 → 调用_update_stream把 DOM 重新序列化为字节并写回底层流。因此对属性的修改是就地生效的,xmp.stream.get_data()会返回更新后的 XML。
写回 PDF:PdfWriter.xmp_metadata
PdfWriter.xmp_metadata的 setter(pypdf/_writer.py)接受三种值:
None:删除根对象中的/Metadata条目;bytes:直接作为新的 XMP 流数据写入;XmpInformation实例:取其内部流的字节数据写入。
写入时会确保/Metadata指向一个间接引用对象(IndirectObject),否则先创建新的StreamObject并注册到 writer。
已弃用的方法
XmpInformation.write_to_stream(stream, encryption_key=None)已被弃用,将在 pypdf 6.0.0 移除,官方建议改用PdfWriter.xmp_metadata(pypdf/xmp.py);其encryption_key参数自 5.0.0 起也不再提供替代方案。测试 tests/test_xmp.py 验证了弃用警告的触发。
资源消耗防护:相关配置项
XMP 解析受到 pypdf 全局配置的约束(定义于 pypdf/_configuration.py):
| 配置项 | 默认值 | 作用 |
|---|---|---|
xmp_maximum_input_length | 5,000,000(字节) | 解压后的 XMP 流最大允许长度,超限抛LimitReachedError |
xmp_maximum_element_count | 100,000 | XMP 数据中允许的最大元素数量 |
这两个配置项的前身是 pypdf/xmp.py 中的模块级常量XMP_MAX_INPUT_LENGTH、XMP_MAX_ELEMENT_COUNT(已标记弃用,迁移映射见 pypdf/_configuration.py)。需要调整时可通过pypdf.config.overwrite_configuration或apply_configuration上下文管理器完成,用法可参考 docs/modules/configuration.rst。超限测试见 tests/test_xmp.py。
一个完整的读写示例
结合以上全部能力,下面是一个端到端的实操示例:读取现有 PDF 的 XMP 元数据、增补字段、写回新文件。
from datetime import datetime, timezone from pypdf import PdfReader, PdfWriter # 1. 读取现有元数据 reader = PdfReader("source.pdf") xmp = reader.xmp_metadata if xmp is None: xmp = __import__("pypdf").xmp.XmpInformation.create() # 2. 修改/新增字段(就地生效) xmp.dc_title = {"x-default": "更新后的标题"} xmp.dc_subject = ["pypdf", "XMP", "metadata"] xmp.xmp_modify_date = datetime.now(timezone.utc) xmp.pdf_keywords = "xmp; metadata; pypdf" # 3. 写回新文件 writer = PdfWriter(clone_from=reader) writer.xmp_metadata = xmp with open("output.pdf", "wb") as f: writer.write(f) # 4. 验证 check = PdfReader("output.pdf") assert check.xmp_metadata.dc_title["x-default"] == "更新后的标题"需要说明的边界情况:如果 PDF 的/Metadata流中不是合法的 XML,PdfReader.xmp_metadata会抛出PdfReadError(测试见 tests/test_xmp.py);pypdf 并不会因此拒绝读取整个 PDF,只是 XMP 元数据不可用。此外,xmp_metadata从根对象读取时不受加密影响——PdfReader在访问时会临时覆盖加密设置(pypdf/_reader.py)。
小结
XmpInformation是 pypdf 中访问 XMP 元数据的统一入口,覆盖了 Dublin Core、Adobe XMP、PDF、XMPMM、PDF/A 五大标准命名空间及 pdfx 自定义属性,同时提供了从零创建、就地修改、写回 PDF 的完整链路。其底层的安全解析器、资源上限配置和命名空间级缓存机制,使其在解析不可信 PDF 时兼具健壮性与效率。更多元数据相关的背景知识可参见 docs/user/metadata.md,类定义的权威 API 清单见 docs/modules/XmpInformation.rst。
【免费下载链接】pypdfA pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files项目地址: https://gitcode.com/GitHub_Trending/py/pypdf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考