python-docx中的oxml包用于加载、处理、序列化ElementTree节点元素。例如,docx包内的xml文档都可以被加载为ElementTree。基于包内的xml文件创建ElementTree时,oxml库依赖XMLParser——定义于lxml.etree模块。
配置XMLParser需注意:1.遵循 Office XML 中命名空间的标准规范;2.将omxl中自定义的元素类对象注册到Office XML 中命名空间下。这样在使用XMLParser创建Element节点时,才会创建符合要求的元素节点对象。
?
XML中的命名空间是为了解决元素重命名的问题。在XML中命名空间使用URI:
Definition: An XML namespace is identified by a URI reference
命名空间与元素本地名称组成限定性名称,限定性名称在一个命名空间内应具有唯一性,限定性命名一般遵守{namespace_uri}%s
格式:
Definition: A qualified name is a name subject to namespace interpretation.
虽然限定性名称可以解决重命名问题,但是由于命名空间一般冗长,在实际使用中,一般使用命名空间前缀替换命名空间,并声明命名空间。声明命名空间,一般通过元素特殊属性xmlns进行声明,基本语法为xmlns:前缀=命名空间
。 经过声明后,可使用namespace_prefix:tag
的方式标识元素名。注意,通过此种方式命名空间声明的范围,包含该元素的所有子节点元素。2
?
XMLParser用于解析XML字符串,从XML字符串中创建ElementTree,并返回root节点元素。
# the code bellow origin from docx.oxml.parser.py module
from typing import cast
oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
def parse_xml(xml: str) -> "BaseOxmlElement":
"""Root lxml element obtained by parsing XML character string `xml`.
The custom parser is used, so custom element classes are produced for elements in
`xml` that have them.
"""
return cast("BaseOxmlElement", etree.fromstring(xml, oxml_parser))
实例化解析器中的“remove_blank_text”是指删除空文本节点,“resolve_entities”是指将实体对象转换为文本表示。XMLParser有两个方法需要留意。parse_xml中的cast函数将etree返回的元素节点映射为BaseOxmlElement类型。
# excute `help(etree.XMLParser)` in python console
def set_element_class_lookup(self, lookup=None):
"""Set a lookup scheme for element classes generated from this parser."""
...
def makeelement(self, _tag, attrib, nsmap, **_extra):
"""Creates a new element associated with this parser."
...
set_element_class_lookup为解析器设置元素类的检索模式。makeelement方法基于解析器、元素类的tag、属性及命名空间映射创建一个元素节点。
# the code bellow origin from docx.oxml.parser.py module
element_class_lookup = etree.ElementNamespaceClassLookup()
oxml_parser.set_element_class_lookup(element_class_lookup)
上述源码中的第一条语句用于实例化一个元素查找模式,第二条用于将自定义的查找模式替换默认的查找模式。如果仅执行以上操作,oxml_parser解析xml字符串仍然返回的是etree.ElementTree
先看源码中注册命名空间与元素类的实现逻辑:
from docx.oxml.ns import NamespacePrefixedTag, nsmap
def register_element_cls(tag: str, cls: Type["BaseOxmlElement"]):
"""Register an lxml custom element-class to use for `tag`.
A instance of `cls` to be constructed when the oxml parser encounters an element
with matching `tag`. `tag` is a string of the form `nspfx:tagroot`, e.g.
`'w:document'`.
"""
nspfx, tagroot = tag.split(":")
namespace = element_class_lookup.get_namespace(nsmap[nspfx])
namespace[tagroot] = cls
oxml_parser.makeelement(r"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p", nsmap={"w": nsmap["w"]})
from docx.oxml.text.paragraph import CT_P
register_element_cls("w:p", CT_P)
oxml_parser.makeelement(r"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p", nsmap={"w": nsmap["w"]})
配置好了XMLParser,则可以基于解析器创建自定义元素节点,源码中的逻辑如下:
from docx.oxml.ns import NamespacePrefixedTag, nsmap
def OxmlElement(
nsptag_str: str,
attrs: Dict[str, str] | None = None,
nsdecls: Dict[str, str] | None = None,
) -> BaseOxmlElement:
"""Return a 'loose' lxml element having the tag specified by `nsptag_str`.
The tag in `nsptag_str` must contain the standard namespace prefix, e.g. `a:tbl`.
The resulting element is an instance of the custom element class for this tag name
if one is defined. A dictionary of attribute values may be provided as `attrs`; they
are set if present. All namespaces defined in the dict `nsdecls` are declared in the
element using the key as the prefix and the value as the namespace name. If
`nsdecls` is not provided, a single namespace declaration is added based on the
prefix on `nsptag_str`.
"""
nsptag = NamespacePrefixedTag(nsptag_str)
if nsdecls is None:
nsdecls = nsptag.nsmap
return oxml_parser.makeelement(nsptag.clark_name, attrib=attrs, nsmap=nsdecls)