简介:Python-docx三方库安装包面向需要在Linux、Windows或macOS上以编程方式创建、编辑和读取.docx文档的Python开发者,解决不依赖Microsoft Office即可批量处理Word文档的问题,适合从入门练习到项目集成的不同技能层级。压缩包共1209个文件,约11.6MB,以548个py源码与测试脚本、144个rst文档、118个feature测试用例、94个xml配置及78个docx示例文档为主,并包含少量图片与构建配置文件,较完整地呈现了库的代码、文档和验证素材。已有11405人学习下载,说明其在文档自动化场景中具有较高关注度。读者可从中获得可直接安装使用的库包,以及围绕标题、段落、页眉页脚、图片、表格、样式主题等操作的源码参考和示例文档,便于理解Word文档对象模型、复现常见读写流程,并对照测试用例排查跨平台使用中的依赖与兼容性问题。
1. 装完就能省下一下午:python-docx 到底替你做了什么
很多人第一次接「批量生成 Word 报告」的需求,第一反应是打开 COM 调用 Office,结果脚本跑到服务器上直接报错,因为那台机器根本没装 Office。python-docx 解决的正是这个尴尬:它是一个纯 Python 的三方库,跨平台读写 .docx,Windows、Linux、macOS 上行为一致,不依赖本机 Word 进程,也不需要 headless 环境。输入是模板文档或空文档,输出仍是标准 .docx,用 WPS、Office、在线预览打开都不会变形。
它适合三类人:要把数据库结果批量灌进固定格式合同的、要从上百份 Word 里抽取表格做汇总的、要拿 Python 直接吐一份带样式报告的。本文围绕这份安装包讲清楚装法、核心对象模型、样式与表格的实操、以及几个让人翻车的边界。注意,它只管 .docx,不碰 .doc 老格式,也做不了 Word 里的复杂排版自动化,这一点先立住,后面才不会白折腾。
2. 安装与环境核对:把包放对地方,比装完更重要
2.1 选型理由:为什么是 python-docx 而不是别的路
批量处理 Word,常见有三条路。第一条是 COM 自动化,依赖本机 Office,只能在 Windows 上跑,进程容易僵死;第二条是直接按 OOXML 规范手写 XML,灵活但成本极高,改一个样式要翻半天规范;第三条就是 python-docx,它在 OOXML 上包了一层对象模型,Document、Paragraph、Run、Table这套抽象覆盖了日常 90% 的场景。代价是它不做渲染,页面上「看起来怎样」它管不了,只负责把结构写对。
所以选型判断很简单:需求是「按模板批量生成 / 批量读取结构」,选它;需求是「精确控制分页位置、页眉首页不同、复杂域代码」,它会让你难受,得考虑别的手段。安装包这种分发形式,主要是应对内网机器不能直连软件源的情况,把 whl 或源码包带过去离线安装。装之前先确认版本环境,这是最容易偷懒也最容易出事的一步。
2.2 离线安装的操作步骤
拿到安装包后先看目录里有什么,常见是.whl文件或python-docx-x.y.z.tar.gz源码包。先核对 Python 版本,再执行安装。依赖项lxml是原生扩展,不同平台、不同 Python 版本的轮子不通用,这一步错了后面全是玄学报错。
# 先看本机 Python 版本和位数,决定用哪个轮子 python -V # 进入安装包所在目录,离线安装(不访问网络) cd ./python-docx-pkg python -m pip install --no-index --find-links=. python-docx # 如果只有源码包,先解压再本地安装 tar -xzf python-docx-*.tar.gz cd python-docx-*/ python -m pip install --no-index --find-links=.. .--no-index表示完全不查在线源,--find-links=.表示只在当前目录找包,两个参数配合才是真正的离线。少了--no-index,pip 仍可能去连源,内网环境下会卡在超时上。源码包装完要留意有没有编译告警,lxml编译失败通常意味着本机缺 C 编译工具链或 libxml2 头文件。
2.3 装完先跑一个最小验证
不要装完就往下写业务代码,先花一分钟确认库能被导入、能正常读写。
import docx # 打印库版本,确认装的不是残留的旧版 print("python-docx version:", getattr(docx, "__version__", "unknown")) # 建一个文档,写一段话,存盘并重新读回 doc = docx.Document() doc.add_paragraph("安装验证:这一行能被读回来说明链路通了") doc.save("verify.docx") back = docx.Document("verify.docx") print("段落数:", len(back.paragraphs)) print("首段内容:", back.paragraphs[0].text)判断标准很直接:版本号能打出来、verify.docx能被生成、重新读取后段落数和文本对得上。任何一步失败,问题都在环境层,不要在业务代码里找原因。我一般还会顺手pip show python-docx看安装路径,确认它装在当前解释器对应的 site-packages 下,而不是另一个 Python 环境里——用虚拟环境时这个坑出现频率很高。
3. 文档对象模型:Paragraph 和 Run 是绕不开的两层
3.1 结构先理清:文档树长什么样
python-docx 的模型是分层的。Document是根,下面挂若干Paragraph(段落)和Table(表格),段落内部再挂Run(文本片段)。关键点在于:样式的最小作用单位是 Run,不是 Paragraph。同一个段落里「前半句加粗、后半句正常」这种效果,靠的就是两个 Run 分别设属性。很多人一上手就写paragraph.bold = True,发现整段都粗了,或者干脆报错,原因就在这里。
读取方向也按这层来:先遍历doc.paragraphs,再遍历paragraph.runs取样式信息。写入方向则相反,先在段落里add_run("文本"),再对 Run 设bold、font.size、font.color.rgb等。理解这层嵌套,后面样式和表格的操作才不是背 API。
3.2 读:把一份文档的结构抽出来
下面的脚本把段落文本、样式名、以及每个 Run 的加粗状态打出来,用来摸清一份陌生文档的构成。
from docx import Document from docx.shared import Pt doc = Document("input.docx") # 只遍历顶层段落,表格里的段落不在 doc.paragraphs 中 for i, para in enumerate(doc.paragraphs): if not para.text.strip(): continue # 空段落跳过,文档里常有大量空行占位 print(f"[{i}] style={para.style.name!r} text={para.text[:40]!r}") for run in para.runs: # bold 可能是 None,表示继承样式,不等于 False print(f" run bold={run.bold} size={run.font.size} text={run.text[:20]!r}")para.style.name拿到的是段落样式名,比如Normal、Heading 1、自定义样式名,这是后续按样式筛选内容的基础。注意run.bold可能是None,含义是「继承父级样式」,不是「不加粗」,直接当布尔用会判断错。run.font.size返回的是Pt对象或None,打印时要留意,比较大小得先和Pt(12)这类对象比,别直接和数字比。
3.3 写:从零生成一篇结构化文档
写入侧的重点是「按结构建、按层级设样式」,别一段一段硬塞。
from docx import Document from docx.shared import Pt, RGBColor doc = Document() doc.add_heading("月度运行报告", level=0) # level=0 是标题样式 # 小标题用内置 Heading 1,便于后期生成目录 doc.add_heading("一、总体情况", level=1) p = doc.add_paragraph() r1 = p.add_run("本周期共处理任务 ") r2 = p.add_run("1280") r2.bold = True # 只对数字加粗 r3 = p.add_run(" 条,成功率 99.2%。") r3.font.size = Pt(10.5) # 字号用 Pt 对象,不能传浮点数 # 单独一段,设置整段样式 note = doc.add_paragraph("注:数据统计口径与上期一致。", style="Intense Quote") note.paragraph_format.space_before = Pt(6) note.paragraph_format.space_after = Pt(6) doc.save("report.docx")三个细节值得记住。第一,add_heading(level=0)生成的是文档标题样式,level=1才是正文一级标题,写反了目录会乱。第二,font.size必须传Pt对象,r3.font.size = 10.5不会报错但也不会生效,这是典型的静默失败。第三,style=传的样式名必须在文档模板里存在,传错会抛KeyError,内置样式名可以用,自定义样式名建议先用[s.name for s in doc.styles]打出来核对。
4. 样式、表格与占位符填充:真实报告场景的三板斧
4.1 模板占位符替换:别用 str.replace 硬来
最常见的需求是「拿一份模板,把里面的{{name}}换成实际值」。新手容易直接读全文再 replace 回去,但那样会丢掉所有格式,因为文本重写后 Run 的样式就没了。正确做法是遍历段落里的 Run 做替换,接受一个前提:占位符不能跨 Run 断裂。
from docx import Document def fill_placeholders(doc, mapping): """在段落和表格中替换 {{key}} 占位符,尽量保留原有 Run 样式""" def replace_in_paragraph(para): for run in para.runs: if "{{" not in run.text: continue for key, value in mapping.items(): run.text = run.text.replace("{{" + key + "}}", str(value)) for para in doc.paragraphs: replace_in_paragraph(para) # 表格里的文本不在 doc.paragraphs 中,必须单独遍历 for table in doc.tables: for row in table.rows: for cell in row.cells: for para in cell.paragraphs: replace_in_paragraph(para) doc = Document("template.docx") fill_placeholders(doc, {"name": "张三", "month": "2024-06", "total": 1280}) doc.save("filled.docx")逻辑说明:按 Run 替换的前提是单词占位符完整落在一个 Run 里。Word 在编辑过程中经常把一个词拆成多个 Run,导致{{name}}变成{{na+me}},这时按 Run 替换会失效。所以更稳的做法是先检测再合并:遍历段落,把连续 Run 的文本拼起来看是否包含占位符,命中后把这段文本整体写入第一个 Run,清空其余 Run 的文本。代价是合并范围内样式统一,但对模板填充场景通常可接受。
参数方面,mapping的 value 一律str()转型,因为replace只接受字符串;数字和日期最好在外面先格式化,别指望库帮你处理。
4.2 表格:读写两侧的重点不同
表格操作里,读的难点在合并单元格,写的难点在样式继承。
from docx import Document from docx.shared import Pt doc = Document() # 建表时给行列数,style 传内置表格样式名 table = doc.add_table(rows=1, cols=3) table.style = "Table Grid" # 带边框的样式,不加这句默认无框线 hdr = table.rows[0].cells for i, title in enumerate(["项目", "数量", "备注"]): hdr[i].text = title for para in hdr[i].paragraphs: for run in para.runs: run.bold = True # 追加数据行:add_row 返回整行,按 cell 索引赋值 for name, num, memo in [("甲", 120, "正常"), ("乙", 80, "延迟")]: cells = table.add_row().cells cells[0].text = name cells[1].text = str(num) cells[2].text = memo doc.save("table.docx")写侧的坑在样式:table.style传错名字会抛KeyError,Table Grid是最常用的内置名;设了单元格text之后,原来单元格里 Run 的格式会被重置,所以先赋文本再设格式的顺序不能反。读侧的坑在合并单元格——table.rows[i].cells在存在横向合并时会返回重复的_Cell对象,同一个合并区域会被多次读出,做统计前必须去重。常见做法是用cell._tc这个底层元素做身份判断,或者按行去重后再计数。
4.3 段落格式与分页:能做和不能做
段落级参数集中在paragraph.paragraph_format上,常用的是space_before、space_after、line_spacing、first_line_indent、alignment。
from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH doc = Document() p = doc.add_paragraph("这是首行缩进两字符、段前 6 磅、两端对齐的正文段落示例。") pf = p.paragraph_format pf.first_line_indent = Cm(0.74) # 约等于两字符,按实际字号微调 pf.space_before = Pt(6) pf.space_after = Pt(6) pf.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY # 强制分页:给段落加分页属性,而不是插一堆空行 doc.add_paragraph("下一页从这里开始").paragraph_format.page_break_before = True doc.save("format.docx")page_break_before = True是分页的正确做法,比插空行可靠得多,空行数量会随字体和页边距漂移。但要清楚它的局限:它控制的是「段落前分页」,做不到「内容超长时在指定位置自动缩排」这类排版逻辑。所有涉及渲染结果的需求,都得生成后用 Word 打开人工核一遍,库本身不负责渲染,这是它的能力边界,不是 bug。
5. 避坑清单:五个真会卡住你的问题
5.1 报 PackageNotFoundError 或找不到 docx 模块
现象是import docx直接失败,或者装完提示成功但导入报错。原因通常是解释器错位:pip 装到了系统 Python,而你在虚拟环境里跑,或者反过来。解决步骤是先用python -c "import sys; print(sys.executable)"打出当前解释器路径,再用python -m pip show -f python-docx看安装位置,两者不在同一环境就重装。全程用python -m pip前缀,能规避大部分这类问题。
5.2 离线安装卡在 lxml 编译
现象是源码包装到一半报编译错误,提示缺 libxml2 或 gcc。原因在于lxml是原生扩展,没有匹配的预编译轮子时 pip 会退化成源码编译。解决分两条路:优先在能联网的机器上按目标平台下载对应 whl(注意 Python 版本和系统架构都要对上),拷进安装包目录一起装;实在要编译,就先装好系统级的编译工具链和 libxml2 开发包。血泪经验是别在目标机上临时补依赖,离线环境里这一步经常缺东西。
5.3 中文乱码或字体不生效
现象是生成的文档中文显示成方块,或者设了font.name但中文部分没变。原因是 Word 对中西文字体分开管理,font.name只设了西文。常见做法是同时设置东亚字体,通过底层元素补上:
from docx.oxml.ns import qn run.font.name = "Times New Roman" run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")_element是底层 OOXML 节点,rPr.rFonts存在的前提是该 Run 已经有字体属性,必要时先设font.name触发节点创建。这一步属于「绕过封装」,能用但要清楚它不在公开 API 承诺范围内,升级库版本时要回归验证。
5.4 表格单元格文本设了不生效
现象是给cell.text赋值后,原本设置的加粗、字号全丢了。原因是cell.text的语义是「清空该单元格并写入新文本」,重建了段落和 Run。解决办法是先拿cell.paragraphs[0].add_run(...)逐步写,或者先赋值文本、再遍历cell.paragraphs里的 Run 补格式。顺序搞反就是白干。
5.5 遍历不到表格里的文字
现象是用doc.paragraphs数段落,结果数量明显偏少,模板里的表格内容一个都没拿到。原因是文档模型里表格是独立节点,表格内的段落不属于doc.paragraphs。解决方法是显式遍历doc.tables再进rows → cells → paragraphs。如果文档里还有嵌套表格或文本框,还需要继续往 XML 层挖,这时建议先打印doc.element.xml看清实际结构再写逻辑。
6. 进阶技巧与验证习惯:把生成结果当代码一样对待
到了能稳定生成文档的阶段,真正拉开差距的不是 API 记得多,而是有没有一套验证流程。我的习惯是:凡是脚本生成的 docx,一律先跑一遍结构断言,再人工抽看。结构断言不做渲染,只校验内容层,能挡住绝大多数低级错误。
from docx import Document def assert_doc(path, expect_keywords, min_tables=0): doc = Document(path) full_text = "\n".join(p.text for p in doc.paragraphs) for kw in expect_keywords: assert kw in full_text, f"缺少关键内容: {kw}" assert len(doc.tables) >= min_tables, f"表格数量不足: {len(doc.tables)}" # 空文档或占位符未替换的兜底检查 assert "{{" not in full_text, "存在未替换的占位符" print(f"校验通过: 段落 {len(doc.paragraphs)} 个, 表格 {len(doc.tables)} 个") assert_doc("filled.docx", ["月度运行报告", "1280"], min_tables=1)这段断言的价值在于把「人工看一眼」变成可重复执行的检查。{{残留检查尤其值得加,占位符漏替换在批量生成里非常隐蔽,几十份文档里错一份,靠人眼看根本发现不了。表格数量断言则能挡住模板变更导致的表格丢失。
再往前一步,可以把「样式核对」也纳入验证,比如检查所有一级标题的字体是否统一:
from docx import Document from collections import Counter doc = Document("report.docx") sizes = Counter() for p in doc.paragraphs: if p.style.name.startswith("Heading"): for run in p.runs: sizes[str(run.font.size)] += 1 print("标题字号分布:", dict(sizes))如果分布里出现多种字号,说明有地方漏设或继承混乱,这在用模板改出来的文档里很常见。另一种值得掌握的技巧是批量处理时统一走「读—改—写」而不是「读—改—存回原文件」,输出到新路径,原模板永远保持干净;一旦写坏了还能重来,这就是给自己留后悔药。
还有一个容易被忽略的点:Document()不带参数时用的是库内置的默认模板,很多自定义样式名在里面并不存在,所以生产环境建议始终基于一份你们自己的模板文件Document("template.docx")打开再写内容,样式名和页边距都跟着模板走,比在代码里逐个设参数稳得多。我早期吃过亏,代码里硬设了一堆字体和边距,模板一换全乱,改成基于模板后代码量少了一半。
从那以后我每次生成批量文档,都强制走一遍「结构断言 + 抽看一份 + 输出到新路径」这三步,哪怕只生成两份也不省略。希望帮到你。
本文还有配套的精品资源,点击获取