python-docx-源码浅析-创建Document

Docx介绍

在介绍Docx之前,先了解一下Office Open XML, 参考:wikipadia

Office Open XML是Microsoft Office的默认格式,其文档就是docx文件。

我们如果用zip打开docx,可以看到一系列文件夹,如下图

$ tree word
word
├── [Content_Types].xml
├── _rels
├── customXml
│   ├── _rels
│   │   └── item1.xml.rels
│   ├── item1.xml
│   └── itemProps1.xml
├── docProps
│   ├── app.xml
│   └── core.xml
└── word
    ├── _rels
    │   └── document.xml.rels
    ├── document.xml
    ├── endnotes.xml
    ├── fontTable.xml
    ├── footnotes.xml
    ├── media
    │   ├── image1.png
    │   ├── image2.png
    ├── numbering.xml
    ├── settings.xml
    ├── styles.xml
    ├── theme
    │   └── theme1.xml
    └── webSettings.xml

我们有非常多的手段去解析xml文件,但分析OOXML仍然不是一件简单事情,而python-docx是一个可以用于解析docx的库。

Document构建

Working with Documents : 参考

我们可以构建一个新的document

from docx import Document
document = Document()

可以通过文件名打开一个docx文件

from docx import Document
document = Document("filename.docx")

也可以通过文件流打开一个docx文件

from docx import Document
with open("filename.docx", "rb") as f:
    stream = StringIO(f.read())
doucment = Document(stream)
stream.close()

Document构建流程

构建一个新的document

Document

源代码:GitHub

def Document(docx=None):
    docx = _default_docx_path() if docx is None else docx
    document_part = Package.open(docx).main_document_part
    if document_part.content_type != CT.WML_DOCUMENT_MAIN:
        tmpl = "file '%s' is not a Word file, content type is '%s'"
        raise ValueError(tmpl % (docx, document_part.content_type))
    return document_part.document


def _default_docx_path():
    _thisdir = os.path.split(__file__)[0]
    return os.path.join(_thisdir, 'templates', 'default.docx')

可以看出如果没有提供docx的话,会加载模板,也就是docx/templates/default.docx文件

然后会调用Package.open将docx加载到内存

接着会判断文件类型是不是CT.WML_DOCUMENT_MAIN

然后就把document返回给调用者。

Package.open

源代码:GitHub

@classmethod
def open(cls, pkg_file):
    pkg_reader = PackageReader.from_file(pkg_file)
    package = cls()
    Unmarshaller.unmarshal(pkg_reader, package, PartFactory)
    return package

这是一个类方法,通过PackageReader.from_file可以获得一个解析器,然后利用这个解析器去对pkg_file进行解析

pkgreader

源代码:GitHub

@staticmethod
def from_file(pkg_file):
    """
    Return a |PackageReader| instance loaded with contents of *pkg_file*.
    """
    phys_reader = PhysPkgReader(pkg_file)
    content_types = _ContentTypeMap.from_xml(phys_reader.content_types_xml)
    pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI)
    sparts = PackageReader._load_serialized_parts(
        phys_reader, pkg_srels, content_types
    )
    phys_reader.close()
    return PackageReader(content_types, pkg_srels, sparts)

PhysPkgReader

源代码:GitHub

class PhysPkgReader(object):
    def __new__(cls, pkg_file):
        if is_string(pkg_file):
            if os.path.isdir(pkg_file):
                reader_cls = _DirPkgReader
            elif is_zipfile(pkg_file):
                reader_cls = _ZipPkgReader
            else:
                raise PackageNotFoundError(
                    "Package not found at '%s'" % pkg_file
                )
        else:
            reader_cls = _ZipPkgReader

        return super(PhysPkgReader, cls).__new__(reader_cls)

PhysPkgReader将返回一个解析器,根据情况而言:

如果pkg_file是字符串,那么 判断pkg_file,是文件路径的话使用_DirPkgReader,是zip文件使用_ZipPkgReader, 其他抛出异常

如果不是字符串,一律使用_ZipPkgReader

注意到

super(PhysPkgReader, cls).__new__(reader_cls)

这句话其实等价于

object.__new__(reader_cls)

等价于利用reader_cls对象作为本类的实例,这是一个非常有趣的点。

不过这里无论是DirPkgReader、还是ZipPkgReader都是PhysPkgReader的子类。