概述
文档属性(core propertities)是docx文档的元信息,读写文档属性是OPC中解包与打包的一个缩影,理解文档属性的读写,可以加深对OPC中解包与打包的原理。另一方面,在docx三方库中,可以将其大致分为四大类:opc子库、oxml子库、proxy class、其它。
opc中定义的类主要与Package、Part、Relationship相关;oxml主要定义CT_为前缀的自定义元素类;proxy class一般封装CT_元素类,对外提供接口。本文结合文档属性读写,对这三种类型的类别定义也加以介绍。本文源码引用自python-docx=1.1.0
解包中的文档属性处理
序列化阶段
在序列化阶段,首先会处理“[Content_Types].xml”文件,收集包内节点的content_type信息。然后会创建序列化的包级别关系边集合,当解析“_rels/.rels”包级别关系文件时,某一关系边属性值Type等于
“http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties”, Target属性值等于“docProps/core.xml”,此关系边表示的是包节点对文档属性节点的引用。
创建序列化包内节点需要指定partname、content_type、reltype、blob、srels,对于文档属性序列化节点:
partname = "/docProps/core.xml"
content_type = "application/vnd.openxmlformats-package.core-properties+xml" # CT.OPC_CORE_PROPERTIES
reltype = "application/vnd.openxmlformats-package.core-properties+xml"
blob = "..." # zip包内子文件docProps/core.xml的字符串
srels = None # 文档属性节点不引用其它包内节点或者外部资源
所有输入信息均具备,因此可以创建完序列化的文档属性节点。
数据编排节点
在解包第二阶段,PartFactory根据序列化包内节点的content_type,选择合适的Part或者其子类,将序列化的包内节点转换为Part或者其子类的实例包内节点。在docx的初始化模块中,在PartFactory的part_type_for类属性字典中配置了CT.OPC_CORE_PROPERTIES与CorePropertiesPart的映射关系,因此选择CorePropertiesPart来实例化文档属性序列化包内节点。
from docx.opc.constants.Content_Type as CT
PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart
CorePropertiesPart 定义
CorePropertiesPart 的部分源码如下所示:
class CorePropertiesPart(XmlPart):
@property
def core_properties(self):
return CoreProperties(self.element)
CorePropertiesPart类的core_prepeties特性,返回的是CoreProperties代理类——定义于docx.opc.coreprops,该代理类初始化时传入的self.element是CT_CoreProperties实例——定义于docx.oxml.coreprops。
XmlPart 定义
CorePropertiesPart 类继承 docx.opc.part.XmlPart, docx.opc.part.XmlPart 继承 ocx.opc.part.Part。
class XmlPart(Part):
def __init__(self, partname, content_type, element, package):
super(XmlPart, self).__init__(
partname, content_type, package=package
)
self._element = element
@classmethod
def load(cls, partname, content_type, blob, package):
element = parse_xml(blob)
return cls(partname, content_type, element, package)
记录了CorePropertiesPart与XmlPart的定义,那CorePropertiesPart是如何实例化的呢?在PartFactory的__new__特殊方法定义如下:
class PartFactory(object):
...
def __new__(cls, partname, content_type, reltype, blob, package):
...
return PartClass.load(partname, content_type, blob, package)
在文档属性节点编排中,这里的PartClass就是CorePropertiesPart。调用load方法本质是调用XmlPart的load方法:1.第一步执行parse_xml(blob)
,由于parse_xml使用docx自己配置的oxml_parser,该解析器配置了register_element_cls('cp:coreProperties', CT_CoreProperties)
,因此函数调用会返回CT_CoreProperties实例对象。2.第二步将partname、content_type、element、package封装进实例属性。
从document_part到core_propetities_part
当解包完成后,如何从docx文档主体包内节点到文档属性包内节点呢?Part或者其子类初始化时为什么需要传入package呢?由于文档属性并未引用其它包内节点,因此需要通过抽象包做为中转站,即从文档主体包内节点->抽象包->文档属性包内节点。从类信息看,路线为DocumentPart->OpcPackage->CorePropertiesPart,即传入package位置参数的值为OpcPackage实例对象。
打包中的文档属性处理
打包文档属性信息相对比较简单。打包流程中依然会首先创建“[Content_types].xml”文件——从包内所有Part或其子类的实例包节点创建。然后将包级别关系写入“_rels/.rels”文件。因为文档属性节点未引用其它包内节点或者外部资源,因此将文档属性节点的信息写入zip包内子文件“docProps/core.xml”即可。
另外,在打包文档属性时,如果对文档属性进行了修改,需注意Part或者其子类的“blob”特性值。如PackageWriter的_write_parts类方法所示:
class PackageWriter(object):
@staticmethod
def _write_parts(phys_writer, parts):
"""
Write the blob of each part in *parts* to the package, along with a
rels item for its relationships if and only if it has any.
"""
for part in parts:
phys_writer.write(part.partname, part.blob)
if len(part._rels):
phys_writer.write(part.partname.rels_uri, part._rels.xml)
注意 phys_writer.write(part.partname, part.blob)
, 第二项参数是 “part.blob”, 分别查看 XMLPart 与 Part 的 blob 属性:
class XmlPart(Part):
def __init__(self, partname, content_type, element, package):
super(XmlPart, self).__init__(
partname, content_type, package=package
)
self._element = element
@property
def blob(self):
return serialize_part_xml(self._element)
class Part(object):
def __init__(self, partname, content_type, blob=None, package=None):
super(Part, self).__init__()
self._partname = partname
self._content_type = content_type
self._blob = blob
self._package = package
@property
def blob(self):
return self._blob
可以发现 XmlPart 中的 blob 属性返回值是 self._element
元素转为 XML 后的字节流——在文档属性中,就是CT_CoreProperties元素实例, 而 Part.blob 则是初始化时传入的 blob, 并且不支持更改。注意文档延伸属性节点“docProps/app.xml”,默认使用Part类处理,所以修改单位、文档模版等文档属性,变化情况并不会存储到物理包文件中。
另外,当通过Coreproperties代理类修改文档属性时,代理类封装的CT_Coreproperties发生改变,将其转为 xml 作为 blob 属性值并写入文件, 则最终物理包的文档属性存储的是变化后的信息。
代理类Coreproperties修改的是CT_Coreproperties元素,以作者属性信息为例:
class CoreProperties:
"""Corresponds to part named ``/docProps/core.xml``, containing the core document
properties for this document package."""
def __init__(self, element):
self._element = element
@property
def author(self):
return self._element.author_text
@author.setter
def author(self, value):
self._element.author_text = value
- 初始化传入的实参就是CT_Coreproperties实例。
小结
本文从解包、打包,与docx库功能分类,两个角度对docx格式文档文档属性信息读写进行了解析,以加深对docx与opc的认知。