☰
python-docx 源码剖析:CT_Body(w:body)——Word 文档正文容器的 Schema 定义与 oxml 实现
2026/10/12 2:21:42 网站建设 项目流程
  • 后端

【免费下载链接】python-docx

Create and modify Word documents with Python

项目地址:https://gitcode.com/gh_mirrors/py/python-docx
点击查看免费下载

导读

w:body是 OOXML WordprocessingML 文档中承载“正文编辑面”的容器元素,也是 python-docx 所有Document.add_paragraph()、add_table()、iter_inner_content()等操作最终落脚的 XML 位置。本文以仓库中 docs/dev/analysis/schema/ct_body.rst 这一份 schema 分析笔记为骨架,先还原 ISO/IEC 29500 规范中CT_Body的完整定义(含两种 XSD 表达形式与EG_BlockLevelElts元素组体系),再深入到 src/docx/oxml/document.py 的CT_Body自定义元素类、src/docx/oxml/xmlchemy.py 的声明式子元素机制以及w:body/w:sectPr的分节哨兵逻辑。读完本文,你将理解 python-docx 为什么把段落、表格当作“块级元素”统一处理、新增内容为何总是被插入到sectPr之前、以及文档正文与页眉页脚/单元格在容器模型上的异同。

CT_Body 一览:规范定位

CT_Body是 WordprocessingML 主文档部件(document.xml)中正文容器的复杂类型。先看它在规范中的元信息:

项目值
Schema NameCT_Body
Spec NameDocument Body
Tag(s)w:body
Namespacewordprocessingml(wml.xsd)
Spec Section17.2.2
  • Schema Name:W3C XML Schema 中定义的 complexType 名称,对应wml.xsd里的<xsd:complexType name="CT_Body">;
  • Spec Name:ECMA-376 / ISO/IEC 29500 规范正文中给该元素起的语义名称,即 “Document Body”;
  • Tag(s):序列化到document.xml时使用的元素标签,即<w:body>;
  • Namespace:该元素属于 WordprocessingML 命名空间,其 XSD 定义位于仓库 ref/xsd/wml.xsd(同时有 ref/rnc/wml.rnc 的 Relax NG 版本);
  • Spec Section:规范中描述该元素的具体章节号 17.2.2,对应 ISO/IEC 29500-1 的 “Main Document” 部分(仓库 ref/ISO-IEC-29500-1.pdf 可查原文)。

python-docx 把CT_Body与w:body标签绑定起来,注册点在 src/docx/oxml/init.py#L100:register_element_cls("w:body", CT_Body),与之配套的是同文件的register_element_cls("w:document", CT_Document)。也就是说,lxml 在解析document.xml时遇到<w:body>会自动实例化CT_Body类,从而获得 python-docx 定义的一系列属性与方法。

规范原文:Document Body 的语义

ISO/IEC 29500 对CT_Body的规范描述(Spec text)如下:

This element specifies the contents of the body of the document -- the main document editing surface.

The document body contains what is referred to asblock-level markup-- markup which can exist as a sibling element to paragraphs in a WordprocessingML document.

翻译并拆解为三点:

  1. w:body定义的是文档正文本体(document body)的内容,也就是用户编辑文档时面对的主编辑区;
  2. 正文容纳的是“块级标记”(block-level markup)——即可以在一篇 WordprocessingML 文档中与段落(w:p)互为兄弟节点的那些标记。这一点是理解CT_Body结构的关键:正文的直接子节点不是 Run 级别的文本碎片,而是段落、表格这类“块”;
  3. 内容一旦出现在<w:body>内,就属于主文档故事(main document story)。

规范给出的最小示例是一个只含单个段落的文档:

<w:document> <w:body> <w:p/> </w:body> </w:document>

这里<w:p/>是空段落(对应 python-docx 中一个无文本的Paragraph)。<w:p/>之所以属于主文档故事,正是因为它位于<w:body>元素内部——这是“故事”(story)概念的体现:WordprocessingML 中一段叙事内容(主文档、页眉、页脚、脚注、文本框等)分别由对应的容器元素承载,w:body是主文档故事的容器。

CT_Document(w:document,规范章节 17.2.3,参见姊妹篇笔记 docs/dev/analysis/schema/ct_document.rst)是document.xml的根元素,它最多包含一个CT_Body。在 src/docx/oxml/document.py#L15-L32 中,这一关系被声明为body: CT_Body = ZeroOrOne("w:body")——ZeroOrOne正是对minOccurs="0" maxOccurs="1"的 Python 化表达。

Schema 深度解读:CT_Body 的两种表达形式

wml.xsd中CT_Body存在两种等价写法。第一种是完全展开(denormalized)形式,把正文允许出现的所有子元素逐一列出;第二种是分组引用形式,通过EG_BlockLevelElts等元素组(group)把“块级元素”按语义归类。两种形式描述的是同一套内容模型,python-docx 分析笔记将其同时收录,便于对照。

完全展开形式:直接列出全部子元素

<xsd:complexType name="CT_Body"> <xsd:sequence> <xsd:choice minOccurs="0" maxOccurs="unbounded"> <xsd:element name="p" type="CT_P"/> <xsd:element name="tbl" type="CT_Tbl"/> <xsd:element name="customXml" type="CT_CustomXmlBlock"/> <xsd:element name="sdt" type="CT_SdtBlock"/> <xsd:element name="proofErr" type="CT_ProofErr"/> <xsd:element name="permStart" type="CT_PermStart"/> <xsd:element name="permEnd" type="CT_Perm"/> <xsd:element name="ins" type="CT_RunTrackChange"/> <xsd:element name="del" type="CT_RunTrackChange"/> <xsd:element name="moveFrom" type="CT_RunTrackChange"/> <xsd:element name="moveTo" type="CT_RunTrackChange"/> <xsd:element ref="m:oMathPara" type="CT_OMathPara"/> <xsd:element ref="m:oMath" type="CT_OMath"/> <xsd:element name="bookmarkStart" type="CT_Bookmark"/> <xsd:element name="bookmarkEnd" type="CT_MarkupRange"/> <xsd:element name="moveFromRangeStart" type="CT_MoveBookmark"/> <xsd:element name="moveFromRangeEnd" type="CT_MarkupRange"/> <xsd:element name="moveToRangeStart" type="CT_MoveBookmark"/> <xsd:element name="moveToRangeEnd" type="CT_MarkupRange"/> <xsd:element name="commentRangeStart" type="CT_MarkupRange"/> <xsd:element name="commentRangeEnd" type="CT_MarkupRange"/> <xsd:element name="customXmlInsRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlInsRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlDelRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlDelRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlMoveFromRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlMoveFromRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlMoveToRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlMoveToRangeEnd" type="CT_Markup"/> <xsd:element name="altChunk" type="CT_AltChunk"/> </xsd:choice> <xsd:element name="sectPr" type="CT_SectPr" minOccurs="0" maxOccurs="1"/> </xsd:sequence> </xsd:complexType>

结构要点:

  • 外层是xsd:sequence:先是一组可重复的内容(choice,minOccurs="0" maxOccurs="unbounded"),最后是至多一个w:sectPr。这意味着在文档顺序上,w:sectPr永远是w:body的最后一个子元素;
  • choice内部是正文块级元素的完整候选集,任意顺序、任意数量、可以缺省;每个元素都使用maxOccurs="unbounded",即段落、表格、标记等可以无限交错出现;
  • 其中m:oMathPara、m:oMath两个元素使用ref=引用m:(math)命名空间中的类型,说明正文允许直接内嵌 Office Math 公式;
  • 与CT_Document的展开形式(见 docs/dev/analysis/schema/ct_document.rst)相比,正文的候选集还多出proofErr、permStart/permEnd、各类customXml*Range*以及ins/del/moveFrom/moveTo等修订类元素。

分组引用形式:块级元素组体系

<xsd:complexType name="CT_Body"> <xsd:sequence> <xsd:group ref="EG_BlockLevelElts" minOccurs="0" maxOccurs="unbounded"/> <xsd:element name="sectPr" type="CT_SectPr" minOccurs="0" maxOccurs="1"/> </xsd:sequence> </xsd:complexType> <xsd:group name="EG_BlockLevelElts"> <xsd:choice> <xsd:group ref="EG_BlockLevelChunkElts"/> <xsd:element name="altChunk" type="CT_AltChunk"/> </xsd:choice> </xsd:group> <xsd:group name="EG_BlockLevelChunkElts"> <xsd:choice> <xsd:group ref="EG_ContentBlockContent"/> </xsd:choice> </xsd:group> <xsd:group name="EG_ContentBlockContent"> <xsd:choice> <xsd:element name="customXml" type="CT_CustomXmlBlock"/> <xsd:element name="sdt" type="CT_SdtBlock"/> <xsd:element name="p" type="CT_P"/> <xsd:element name="tbl" type="CT_Tbl"/> <xsd:group ref="EG_RunLevelElts"/> </xsd:choice> </xsd:group> <xsd:group name="EG_RunLevelElts"> <xsd:choice> <xsd:element name="proofErr" type="CT_ProofErr"/> <xsd:element name="permStart" type="CT_PermStart"/> <xsd:element name="permEnd" type="CT_Perm"/> <xsd:element name="ins" type="CT_RunTrackChange"/> <xsd:element name="del" type="CT_RunTrackChange"/> <xsd:element name="moveFrom" type="CT_RunTrackChange"/> <xsd:element name="moveTo" type="CT_RunTrackChange"/> <xsd:group ref="EG_MathContent"/> <xsd:group ref="EG_RangeMarkupElements"/> </xsd:choice> </xsd:group> <xsd:group name="EG_MathContent"> <xsd:choice> <xsd:element ref="m:oMathPara" type="CT_OMathPara"/> <xsd:element ref="m:oMath" type="CT_OMath"/> </xsd:choice> </xsd:group> <xsd:group name="EG_RangeMarkupElements"> <xsd:choice> <xsd:element name="bookmarkStart" type="CT_Bookmark"/> <xsd:element name="bookmarkEnd" type="CT_MarkupRange"/> <xsd:element name="moveFromRangeStart" type="CT_MoveBookmark"/> <xsd:element name="moveFromRangeEnd" type="CT_MarkupRange"/> <xsd:element name="moveToRangeStart" type="CT_MoveBookmark"/> <xsd:element name="moveToRangeEnd" type="CT_MarkupRange"/> <xsd:element name="commentRangeStart" type="CT_MarkupRange"/> <xsd:element name="commentRangeEnd" type="CT_MarkupRange"/> <xsd:element name="customXmlInsRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlInsRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlDelRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlDelRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlMoveFromRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlMoveFromRangeEnd" type="CT_Markup"/> <xsd:element name="customXmlMoveToRangeStart" type="CT_TrackChange"/> <xsd:element name="customXmlMoveToRangeEnd" type="CT_Markup"/> </xsd:choice> </xsd:group>

这个嵌套 group 结构揭示了几层语义分类:

元素组语义成员
EG_BlockLevelElts正文一级的“块”altChunk(外部文档导入块)+EG_BlockLevelChunkElts
EG_BlockLevelChunkElts内容块EG_ContentBlockContent
EG_ContentBlockContent真正的块级内容customXml、sdt、p、tbl+EG_RunLevelElts
EG_RunLevelElts允许出现在块之间的“运行级”杂项元素proofErr、permStart/permEnd、ins/del/moveFrom/moveTo+EG_MathContent+EG_RangeMarkupElements
EG_MathContent数学公式m:oMathPara、m:oMath
EG_RangeMarkupElements范围标记书签(bookmarkStart/bookmarkEnd)、移动范围(moveFromRange*/moveToRange*)、批注范围(commentRangeStart/commentRangeEnd)、customXml 修订范围(customXml*Range*)

这个分组设计的实际含义是:w:body的直接子元素并不只有“内容块”——在段落、表格之间,Word 还会写入书签、批注范围标记、拼写校对标记(proofErr)、权限范围(permStart/permEnd)以及修订跟踪元素(ins/del/moveFrom/moveTo)。这些元素与w:p、w:tbl平级共存。这也是 python-docx 中Document.paragraphs、Document.tables只返回顶层w:p/w:tbl的原因——它需要在层级上“过滤”掉这些标记元素。

正文子元素的角色分类

把上面两种形式合并归纳,w:body的子元素可归为五类:

  1. 内容块(真正渲染的内容):p(段落,CT_P,规范章节 17.3.1.22,见 docs/dev/analysis/schema/ct_p.rst)、tbl(表格,CT_Tbl);
  2. 结构化/自定义内容包装:customXml(自定义 XML 块)、sdt(结构化文档标签,如内容控件)、altChunk(导入的外部文档片段);
  3. 批注与书签等范围标记:commentRangeStart/commentRangeEnd(批注高亮范围)、bookmarkStart/bookmarkEnd(书签)、moveFromRange*/moveToRange*(修订移动范围)、customXml*Range*(customXml 修订范围);
  4. 修订跟踪:ins(插入)、del(删除)、moveFrom(移动源)、moveTo(移动目标),以及proofErr(校对错误标记)、permStart/permEnd(编辑权限范围);
  5. 数学内容:m:oMathPara、m:oMath(Office Math 公式)。

值得一提的是批注范围:python-docx 的Document.add_comment()(见 src/docx/document.py#L41-L88)正是通过在被批注 Run 的前后放置w:commentRangeStart与w:commentRangeEnd来界定高亮范围的,其 docstring 明确写了 “The comment reference range is delimited by placing aw:commentRangeStartelement before the first run and aw:commentRangeEndelement after the last run.”——这与本 schema 中EG_RangeMarkupElements的定义一一对应。

sectPr:正文的收尾分节元素

CT_Body序列的最后是w:sectPr(minOccurs="0" maxOccurs="1"):

<xsd:complexType name="CT_SectPr"> <xsd:sequence> <xsd:group ref="EG_HdrFtrReferences" minOccurs="0" maxOccurs="6"/> <xsd:group ref="EG_SectPrContents" minOccurs="0"/> <xsd:element name="sectPrChange" type="CT_SectPrChange" minOccurs="0"/> </xsd:sequence> <xsd:attributeGroup ref="AG_SectPrAttributes"/> </xsd:complexType>

CT_SectPr的组成:至多 6 个页眉/页脚引用(EG_HdrFtrReferences,对应奇偶页 + 首页 + 默认页眉/页脚的组合)、分节属性内容(EG_SectPrContents,如pgSz页面尺寸、pgMar页边距、cols分栏等)、修订记录sectPrChange,以及分节属性组AG_SectPrAttributes。

w:body/w:sectPr的特殊地位在于:它是文档最后一个分节(section)的“哨兵”属性——凡是w:p/w:pPr/w:sectPr(段内分节)之后的正文,其分节属性都由它决定。python-docx 的Sections集合正是把所有分节收集起来的:见 src/docx/oxml/document.py#L20-L32,其 XPath 为"./w:body/w:p/w:pPr/w:sectPr | ./w:body/w:sectPr",并且代码注释明确指出 “The last one is alwaysw:body/w:sectPr, all preceding arew:p/w:pPr/w:sectPr”。再结合 src/docx/section.py 中Sections.__getitem__/__iter__/__len__都是基于self._document_elm.sectPr_lst实现,可以推断:python-docx 的“分节”模型本质就是遍历w:body内的这些sectPr元素。

从 Schema 到 Python 类:oxml 层的声明式映射

schema 是纸面契约,python-docx 则把CT_Body落地为 lxml 自定义元素类。核心实现在 src/docx/oxml/document.py#L35-L88:

class CT_Body(BaseOxmlElement): """`w:body`, the container element for the main document story in `document.xml`.""" add_p: Callable[[], CT_P] get_or_add_sectPr: Callable[[], CT_SectPr] p_lst: List[CT_P] tbl_lst: List[CT_Tbl] _insert_tbl: Callable[[CT_Tbl], CT_Tbl] p = ZeroOrMore("w:p", successors=("w:sectPr",)) tbl = ZeroOrMore("w:tbl", successors=("w:sectPr",)) sectPr: CT_SectPr | None = ZeroOrOne( # pyright: ignore[reportAssignmentType] "w:sectPr", successors=() )

对照 XSD 可以看到 python-docx 的取舍策略:它没有为 schema 中全部 27 类子元素建模,而是只声明自己用得到的子集——p、tbl和sectPr,其余标记类元素(书签、批注范围、修订等)在 oxml 层不声明属性,但 lxml 解析时不会丢数据,因为未声明的子元素仍以通用_Element形式保留在树中。

几个值得深挖的实现细节:

1.ZeroOrMore/ZeroOrOne声明式描述符

ZeroOrMore与ZeroOrOne定义在 src/docx/oxml/xmlchemy.py#L526-L576,它们负责在类上动态生成一组方法:

  • ZeroOrMore:生成p_lst/tbl_lst列表属性、_new_p()工厂、_insert_p()/_insert_tbl()插入器、add_p()公开追加方法;
  • ZeroOrOne:额外生成sectPr属性(无则返回None)、get_or_add_sectPr()、_remove_sectPr()。

这正好对应 XSD 的基数约束:ZeroOrMore↔minOccurs="0" maxOccurs="unbounded",ZeroOrOne↔minOccurs="0" maxOccurs="1"。

2.successors参数:保证sectPr永远在最后

ZeroOrMore("w:p", successors=("w:sectPr",))中的successors是 python-docx 实现“sectPr 必须是最后一个子元素”这一 schema 约束的手段。生成的插入器最终调用 src/docx/oxml/xmlchemy.py#L664-L670 的BaseOxmlElement.insert_element_before():

def insert_element_before(self, elm: ElementBase, *tagnames: str): successor = self.first_child_found_in(*tagnames) if successor is not None: successor.addprevious(elm) else: self.append(elm) return elm

即:新插入的w:p/w:tbl会自动定位到第一个w:sectPr之前;如果正文中还没有sectPr,则直接 append 到末尾。因此无论用户往正文里加多少段落、表格,w:body/w:sectPr始终保持在序列尾部,与 XSD 的 sequence 顺序完全一致。

3.inner_content_elements:只返回块级内容

src/docx/oxml/document.py#L81-L88 定义:

@property def inner_content_elements(self) -> List[CT_P | CT_Tbl]: """Generate all `w:p` and `w:tbl` elements in this document-body.""" return self.xpath("./w:p | ./w:tbl")

它用 XPath 只挑出w:body直接子级中的w:p与w:tbl,刻意跳过书签、批注范围、proofErr、sdt等“非内容”子元素。这一点有单元测试背书:tests/oxml/test_document.py#L17-L19:

def it_knows_its_inner_content_block_item_elements(self): body = cast(CT_Body, element("w:body/(w:tbl, w:p,w:p)")) assert [type(e) for e in body.inner_content_elements] == [CT_Tbl, CT_P, CT_P]

测试用紧凑 XML 语法构造了一个含“表格、段落、段落”的w:body,断言inner_content_elements按文档顺序返回[CT_Tbl, CT_P, CT_P]。docstring 还特别注明:嵌套在w:ins等“包装”元素内部的块不会包含在内——这与Document.paragraphs、Document.tables的文档行为一致(见 src/docx/document.py#L184-L230,其中明确写着修订标记内的段落/表格不出现)。

4.clear_content():清空但保留分节

src/docx/oxml/document.py#L73-L79:

def clear_content(self): """Remove all content child elements from this <w:body> element. Leave the <w:sectPr> element if it is present. """ for content_elm in self.xpath("./*[not(self::w:sectPr)]"): self.remove(content_elm)

它删除除w:sectPr外的所有子元素——即“清空正文但保留分节设置”。上层Document通过 src/docx/document.py#L259-L265 的_Body.clear_content()暴露这一能力。这个行为与 schema 中sectPr的独立地位完全对应:分节属性不属于“内容”,清内容时应当保留。

上层 API:Document / _Body / BlockItemContainer 如何驱动 w:body

从用户视角看,w:body的操作都被封装在DocumentAPI 之后。调用链如下:

Document.add_paragraph(text, style) └─> self._body.add_paragraph(text, style) # document.py:119 └─> BlockItemContainer._add_paragraph() # blkcntnr.py:99-101 └─> Paragraph(self._element.add_p(), self) └─> CT_Body.add_p() # oxml/document.py,ZeroOrMore 生成

_Body定义在 src/docx/document.py#L249-L265,它继承 src/docx/blkcntnr.py#L33-L101 的BlockItemContainer——一个“可以包含块级项目的容器”基类。值得注意的类型别名 src/docx/blkcntnr.py#L30:

BlockItemElement: TypeAlias = "CT_Body | CT_Comment | CT_HdrFtr | CT_Tc"

这说明w:body与批注(w:comment)、页眉/页脚(w:hdr/w:ftr)、表格单元格(w:tc)在 python-docx 的容器模型中地位相同——它们都是BlockItemContainer的宿主元素。这与 schema 的设计哲学呼应:CT_Comment、CT_HdrFtr、CT_Tc的内容模型同样是“块级元素组”(EG_BlockLevelElts或其变体)。因此_Cell.add_paragraph()、header.add_paragraph()与Document.add_paragraph()走的是同一条BlockItemContainer代码路径,只是底层元素不同。

BlockItemContainer提供的方法(src/docx/blkcntnr.py):

方法/属性作用底层 CT_Body 对应
add_paragraph(text, style)正文末尾追加段落CT_Body.add_p()
add_table(rows, cols, width)正文末尾追加表格CT_Tbl.new_tbl()+CT_Body._insert_tbl()
iter_inner_content()按文档顺序产出Paragraph/TableCT_Body.inner_content_elements
paragraphs顶层段落列表CT_Body.p_lst
tables顶层表格列表CT_Body.tbl_lst
_add_paragraph()追加空段落CT_Body.add_p()

其中add_table通过_insert_tbl(tbl)插入,正是上一节提到的successors=("w:sectPr",)机制,保证表格永远排在sectPr之前。Document.add_table()还会用 src/docx/document.py#L232-L239 的_block_width(最后分节的页宽减左右边距)作为表格宽度——这是w:body/w:sectPr与内容块之间联动的一个例子。

Document.add_section()(src/docx/document.py#L140-L148)则直接落到self._element.body.add_section_break(),即调用CT_Body.add_section_break()。

w:body 与分节:sectPr 的“哨兵”与克隆机制

CT_Body.add_section_break()是理解 python-docx 分节模型的最佳窗口(src/docx/oxml/document.py#L51-L71):

def add_section_break(self) -> CT_SectPr: # ---get the sectPr at file-end, which controls last section (sections[-1])--- sentinel_sectPr = self.get_or_add_sectPr() # ---add exact copy to new `w:p` element; that is now second-to last section--- self.add_p().set_sectPr(sentinel_sectPr.clone()) # ---remove any header or footer references from "new" last section--- for hdrftr_ref in sentinel_sectPr.xpath("w:headerReference|w:footerReference"): sentinel_sectPr.remove(hdrftr_ref) # ---the sentinel `w:sectPr` now controls the new last section--- return sentinel_sectPr

其过程与 schema 结构严格对应:

  1. 取w:body末尾的哨兵sectPr(即控制“最后一个分节”的那个,对应 XSD 中w:body序列尾部的sectPr);
  2. 向w:body追加一个新段落w:p,并把哨兵sectPr的完整克隆放进该段落的w:pPr/w:sectPr——这个段内sectPr从此成为“倒数第二个分节”的属性;
  3. 从原哨兵中移除所有页眉/页脚引用,使其变成新最后一个分节(页眉页脚因此“继承”自前一节,直到用户另行设置);
  4. 返回该哨兵sectPr,Document.add_section()随即设置其start_type(分节起始方式,如WD_SECTION.NEW_PAGE)。

结果是w:body中出现w:p/w:pPr/w:sectPr与w:body/w:sectPr并存的结构——这正是 src/docx/oxml/document.py#L31 那条 XPath 同时匹配两种位置的原因。分节行为测试见 features/sct-section.feature(如Section.start_type、页眉页脚、页面尺寸、边距等场景)。分节在文档模型中的意义可进一步参考 docs/user/sections.rst。

实操观察:新建文档的 w:body 长什么样

python-docx 自带的默认模板 src/docx/templates/default-docx-template/word/document.xml 展示了一个“空正文”的真实样例:

<w:document ...> <w:body> <w:sectPr w:rsidR="00FC693F" w:rsidRPr="0006063C" w:rsidSect="00034616"> <w:pgSz w:w="12240" w:h="15840"/> <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="720" w:footer="720" w:gutter="0"/> <w:cols w:space="720"/> <w:docGrid w:linePitch="360"/> </w:sectPr> </w:body> </w:document>

注意这里w:body只有w:sectPr而没有内容块——完全符合 XSD 中choice允许minOccurs="0"(零个内容)且sectPr独立出现的定义。pgSz(12240×15840 twips = Letter 竖版)、pgMar(上下 1440 twips、左右 1800 twips、页眉页脚 720 twips)、cols、docGrid都属于EG_SectPrContents组的内容。

你可以通过以下步骤亲手验证正文容器的结构变化:

from docx import Document doc = Document() doc.add_paragraph("第一段") doc.add_paragraph("第二段") doc.add_table(rows=2, cols=2) doc.save("demo.docx")

然后把demo.docx当作 ZIP 解压(unzip demo.docx或任意解压工具),查看word/document.xml,会看到w:body变为:

<w:body> <w:p>...第一段...</w:p> <w:p>...第二段...</w:p> <w:tbl>...2x2 表格...</w:tbl> <w:sectPr>...</w:sectPr> </w:body>

其中两个w:p和一个w:tbl按调用顺序排列,w:sectPr依旧殿后——successors机制保证了这一点。再调用一次doc.add_section(),则倒数第二个分节的w:sectPr会出现在新增空段落的w:pPr中。

进一步探索

  • 姊妹篇 schema 笔记:CT_Document(w:document)、CT_P(w:p),可对比根元素与段落的内容模型;
  • oxml 元素类实现:src/docx/oxml/document.py、声明式描述符 src/docx/oxml/xmlchemy.py;
  • 元素注册表:src/docx/oxml/init.py(register_element_cls("w:body", CT_Body)等约 200 条注册);
  • 上层 API:src/docx/document.py(Document与_Body)、src/docx/blkcntnr.py(BlockItemContainer)、src/docx/section.py(Section/Sections);
  • 单元测试:tests/oxml/test_document.py;行为级测试:features/blk-add-paragraph.feature、features/blk-add-table.feature、features/blk-iter-inner-content.feature、features/sct-section.feature;
  • 规范原文:wml.xsd见 ref/xsd/wml.xsd,Relax NG 版见 ref/rnc/wml.rnc,规范章节 17.2.2 见 ref/ISO-IEC-29500-1.pdf。

小结

CT_Body(w:body)是 WordprocessingML 主文档故事的容器:规范(章节 17.2.2)定义它为“主文档编辑面”,内容模型是“任意数量的块级元素 + 至多一个收尾的sectPr”;XSD 用EG_BlockLevelElts→EG_ContentBlockContent→EG_RunLevelElts→EG_MathContent/EG_RangeMarkupElements的嵌套分组,把段落、表格、书签、批注范围、修订、公式、customXml 等元素组织成层次清晰的候选集。python-docx 在 oxml 层通过ZeroOrMore("w:p", successors=("w:sectPr",))等声明式描述符与insert_element_before机制,精确复刻了“块内容在前、sectPr 殿后”的顺序约束;在上层则用_Body/BlockItemContainer把add_paragraph、add_table、iter_inner_content、clear_content、add_section全部收口到w:body这一个元素上。理解CT_Body,就等于拿到了解读 python-docx 文档对象模型与 OOXML 正文存储格式的钥匙。

  • 后端

【免费下载链接】python-docx

Create and modify Word documents with Python

项目地址:https://gitcode.com/gh_mirrors/py/python-docx
点击查看免费下载
上一篇:EcoPaste 剪贴板管线深度解析:从 OS 监听、去重入库到写回抑制的完整实现
下一篇:disktree 目录分类引擎剖析:8 种数据类型识别与可回收空间判定逻辑全解读

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询