基于 python-docx 读写 App Properties

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

环境依赖

Office Word 2013
python-docx == 0.8.11

 

概述

python-docx 支持对 docx 文档 core properties 的读写操作——即查看与改写 “/docProps/core.xml” 中的字段值但并未提供对 app properties 的读写 api。 参考 python-docx 源码中 docx.opc.parts.CorePropertiesPart docx.oxml.CT_CorePropertiesdocx.opc.CoreProperties 的源码以及 docx.opc.package.OpcPackage 的 open 与 save 源码 实现对 app properties 的读写操作。

 

python-docx 实现 core properties 的读写功能

读取 “/docProps/app.xml” 中的字段值相对写操作更为容易由于 docx 遵循 zip 标准因此使用标准库 zipfile 打开 “/docProps/app.xml” 文件然后使用正则匹配就能实现读操作的目标。本部分采用另外一种更简便的方式实现——即模拟 docx.opc.parts.CorePropertiesPart docx.oxml.CT_CoreProperties 以及 docx.opc.CoreProperties 的逻辑。

在 docx.opc.package.OpcPackage.open 编排“序列化” part 时会依据 part 的媒体类型、docx.opc.part.PartFactory 选择合适的 PartClass 来创建一个“序列化”的 part 实例 其中 docx.__init__.py 对 docx.opc.part.PartFactory.part_type_for 字典进行了配置。

from docx.opc.constants.Content_Type as CT
PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart

CorePropertiesPart 的部分源码如下所示

class CorePropertiesPart(XmlPart):
    @property
    def core_properties(self):
        return CoreProperties(self.element)  # docx.opc.CoreProperties

CorePropertiesPart 类继承 docx.opc.part.XmlPart docx.opc.part.XmlPart 继承 docx.opc.part.Part。这里的 self.element 本质是 CT_CoreProperties 的实例那么 CT_CoreProperties 是何时实例化的呢—— 在 docx.opc.package.OpcPackage 编排 “/docProps/core.xml” 时

    @staticmethod
    def _unmarshal_parts(pkg_reader, package, part_factory):
        """
        Return a dictionary of |Part| instances unmarshalled from
        *pkg_reader*, keyed by partname. Side-effect is that each part in
        *pkg_reader* is constructed using *part_factory*.
        """
        parts = {}
        for partname, content_type, reltype, blob in pkg_reader.iter_sparts():
            parts[partname] = part_factory(
                partname, content_type, reltype, blob, package
            )
        return parts

当 partname 为 “/docProps/core.xml” 时 content_type 等于 docx.opc.constants.Content_Type.OPC_CORE_PROPERTIES 这时 PartFactory 会使用 CorePropertiesPart 类来创建实例

class PartFactory(object):
    part_class_selector = None
    part_type_for = {}
    default_part_type = Part

    def __new__(cls, partname, content_type, reltype, blob, package):
        PartClass = None
        if cls.part_class_selector is not None:
            part_class_selector = cls_method_fn(cls, 'part_class_selector')
            PartClass = part_class_selector(content_type, reltype)
        if PartClass is None:
            PartClass = cls._part_cls_for(content_type)
        return PartClass.load(partname, content_type, blob, package)

即最后一句执行 CorePropertiesPart .load(partname, content_type, blob, package) 注意 XmlPart 与 Part 类 load 方法的差异

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)

XmlPart.load 函数中的 parse_xml 来源于 docx.oxml.__init__.py 在该模块中对 core properties 所在的命名空间注册了 CT_CorePerperties 类

register_element_cls('cp:coreProperties', CT_CoreProperties)

因此 parse_xml(blob) 会返回 CT_CoreProperties 实例元素并且被封装到 CorePropertiesPart._element 实例属性中。

而当使用 python-docx 接口读取 core_properties 时

docx.document.Document().core_properties

会依次进入 docx.parts.document.DocumentPart -> docx.opc.package.OpcPackage -> docx.opc.parts.coreprops.CorePropertiesPart -> doc.opc.coreprops.CorePropertiesdoc.opc.coreprops.CoreProperties 对属性的读写本质是对 doc.oxml.coreprops.CT_CoreProperties 元素中子节点的读写。

对于写操作有一点需要注意

class PackageWriter(object):
    @staticmethod
    def write(pkg_file, pkg_rels, parts):
        """
        Write a physical package (.pptx file) to *pkg_file* containing
        *pkg_rels* and *parts* and a content types stream based on the
        content types of the parts.
        """
        phys_writer = PhysPkgWriter(pkg_file)
        PackageWriter._write_content_types_stream(phys_writer, parts)
        PackageWriter._write_pkg_rels(phys_writer, pkg_rels)
        PackageWriter._write_parts(phys_writer, parts)  # 将 part 对象写入文件
        phys_writer.close()

    @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 属性返回值是 CT_CoreProperties 元素转为 XML 后的字节流 而 Part.blob 则是初始化时传入的 blob 并且不支持更改。当通过 docx.opc.coreprops.Coreproperties 代理类修改文档 core 属性时 内部的 docx.oxml.coreprops.CT_Coreproperties 发生改变将其转为 xml 作为 blob 属性值并写入文件 则最终文档的 core 属性的变化写入了文件。但是如果是 “/docProps/app.xml” 默认情况下会创建一个 Part 类 对于读操作还好但是写操作发生变化的属性信息无法再赋值回 blob 中因为 Part 类的 blob 属性是一个仅读属性。解决办法的核心就是“使用 docx.opc.part.XmlPart 编排 /docProps/app.xml”。

 

python-docx 实现 app properties 的读写功能

    import docx
    from docx.opc.part import PartFactory, XmlPart
    from docx.opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT

	# 使用 docx.opc.part.XmlPart 来编排 /docProps/app.xml
    PartFactory.part_type_for[CT.OFC_EXTENDED_PROPERTIES] = XmlPart

    document = docx.api.Document()
    app_properties_part = document._part._package.part_related_by(RT.EXTENDED_PROPERTIES)
    app_properties_element = app_properties_part._element

	# 以 Company 字段值的读写为例
    for child in app_properties_element:
        if child.tag == "{http://schemas.openxmlformats.org/officeDocument/2006/extended-properties}Company":
            print(child.text)  # 读取源单位名称
            child.text = "123456"  # 写入新的单位名称
            break

    document.save("temporary.docx")

 

待完成

定义AppPropertiesPart、CT_AppProperties、AppProperties 实现目标功能。

[元素tag 前缀为 ap](https://learn.microsoft.com/zh-cn/dotnet/api/documentformat.openxml.extendedproperties?view=openxml-2.8.1)
命名空间http://schemas.openxmlformats.org/officeDocument/2006/extended-properties
与package的关系类型http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties
媒体类型application/vnd.openxmlformats-officedocument.extended-properties+xml
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: python