多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python Word表格提取指南:从python-docx到批量汇总Excel

Python Word表格提取指南:从python-docx到批量汇总Excel 1. 先把场景摊开为什么我要用 Python 去折腾 Word 表格在我接触过的自动化需求里Word 表格提取可能是出现频率最高、也最容易被低估的一项。很多朋友觉得“不就是把表格内容复制出来嘛”但现实是你每天收到的报价单、项目排期表、周报汇总、采购清单Word 里往往嵌着十几张表每张表的合并单元格、空行、表头格式全都不一样真靠复制粘贴汇总一上午就没了。用 Python 处理这件事最核心的价值在于把“从 Word 表格里读取数据”变成一门可重复、可批量、可校验的技术活。你可以把几十份文档中的表格内容统一抽出来经过清洗后丢进 Excel 或数据库整个过程不需要打开 Word 软件也不依赖宏更不会因为手动复制而漏行错列。先说清楚这篇指南能给你什么。读完以后你能掌握三件事第一理解 Word 文档里表格到底是怎样存储的为什么 python-docx 可以读取它第二能写出一套稳定读取普通表格、合并单元格、嵌套表格的代码第三能把单文档提取升级成多文档批量汇总顺手把常见的坑都避开。适合的读者包括办公自动化的实施者、写脚本处理报表的开发以及被 Word 表格折磨得不想再手动复制的普通办公人员。不必担心基础下面所有代码我都拆到“复制就能试”的程度同时也会告诉你每一步背后的原理。2. 工具选型拆解 Word 文件的真实结构看懂 python-docx 的边界2.1 Word 文件底层到底是什么很多教程一上来就教你怎么写代码但我觉得真正想搞明白表格提取必须先从文件格式说起。一个 .docx 文件本质上是一个 zip 压缩包里面装着一组 XML 文件。其中负责正文内容的是 word/document.xml它按照顺序记录了段落、表格、图片等所有元素。表格在 XML 里是一棵层级非常清晰的对象树最外层是w:tbl往里是代表行的w:tr再往里是代表单元格的w:tc。单元格里面又包含若干个w:p段落段落里才是真正可见的文本w:t。明白了这层结构再去理解 python-docx 的 API 就非常简单。比如 document.tables 这个方法做的事情就是扫描文档 body 里所有的w:tbl节点把它们包装成 Table 对象返回table.rows 拿到的是w:tr集合row.cells 拿到的则是w:tc集合。你手上写出来的每一行 Python 代码最后都会映射回对 XML 树的读写操作。这个认知能救你很多次尤其是遇到“python-docx 接口不够用”的情况时你就知道自己可以绕过封装直接操作cell._tc或者table._tbl来修改底层 XML。记住一句话接口只是方便底层结构才是根本。2.2 为什么优先选 python-docx 而不是其他方案做 Word 表格提取技术路线其实有好几条。有人先转 PDF 再用 pdfplumber 抽表格有人借助 win32com 调用 Word 应用还有人写 VBA 宏。每条路都有人走但我的首选一直是 python-docx原因有三个。第一功能匹配度最高。python-docx 内置对表格的完整支持读取、新增、修改、设置样式都能做不需要中转不容易丢失数据结构。第二维护活跃、社区大。遇到问题基本能搜到答案而且它对 Python 3.8 到 3.13 的兼容性都不错。第三不依赖 Word 进程。这点在服务器和批处理场景里特别重要win32com 方案要跑一个 Office 进程一旦文档崩了或者权限不够整批任务就卡住了。当然python-docx 也有边界——它只能处理 .docx不能打开旧版 .doc。关于这个问题我会在第 5 章专门讲补救方案。至于 VBA它最适合“鼠标点一下就执行”的临时操作一旦涉及到批量、定时、跨设备部署脚本的可移植性和日志追踪能力比宏要强太多了。2.3 环境准备与版本避雷开始写代码前先把环境装好。我的习惯是创建一个干净的虚拟环境避免库之间的依赖冲突。安装命令很简单三个库一次到位python-docx 用于读 Wordpandas 用于表格数据整理openpyxl 用于把结果写入 Excel。pip install python-docx pandas openpyxl装完以后我强烈建议你做一个快速验证确认文档能被正常打开。随便弄一个 .docx 文件放到当前目录执行下面这段能打印出表格数量就说明环境没问题。import docx doc docx.Document(demo.docx) print(表格数量:, len(doc.tables))这里有个新手很容易踩的坑把文件命名为 demo.docx但实际上是一个改了后缀的 .doc 老文件。Python 打开时不会直接报“格式不对”而是提示“Package not found”或者抛出 zipfile.BadZipFile。判断方法也很简单用二进制方式读文件头真正的 .docx 前面两个字节是 PK。with open(demo.docx, rb) as f: head f.read(2) print(head) # bPK 说明是正常的 docx3. 表格读取核心细节对象层级、行文遍历与单元格结构3.1 表格对象与三个最容易混淆的 APIpython-docx 里和表格打交道你绕不开这几个对象Document、Table、Row、Cell。它们之间的层级关系是 Document 包含多个 TableTable 包含多个 RowRow 包含多个 Cell。听起来不复杂但 API 的命名容易让人混淆。首先document.paragraphs 只能取到正文里的段落不包含表格单元格内的段落。你要取单元格里的文字必须通过 cell.paragraphs 来拿。其次table.rows 看起来是一堆行但每行本身不直接提供文本你要往下一层通过 row.cells 才能拿到单元格。最后table.columns 不是你想的那样方便由于合并单元格的存在column.cells 返回的单元格结果在数量上可能和行数对不上。搞清楚这三个点再去读代码你会发现很多报错根本不是逻辑问题而是对对象树的预期错了。我的建议是把对象树的层级关系记成一句话——文档里有表格表格里有行行里有单元格单元格里有段落。3.2 一段可以直接用的基础读取逻辑下面这段基础读取函数是我几乎所有 Word 表格项目的起点。它能把任意一个表格对象转成二维列表每一行 list 代表表格的一行每个元素代表一个单元格的文本。import docx def read_table_to_list(table): data [] for row in table.rows: row_data [] for cell in row.cells: # 一个单元格可能包含多个段落用换行拼接 cell_text \n.join(p.text for p in cell.paragraphs) row_data.append(cell_text) data.append(row_data) return data doc docx.Document(demo.docx) for idx, table in enumerate(doc.tables): data read_table_to_list(table) print(f表格 {idx 1}: {len(data)} 行, {len(data[0]) if data else 0} 列)几个容易被忽略的细节第一个是单元格内多段落的情况。你用 cell.text 也能拿文本但它在拼接多个段落时不一定保留完整的换行信息用 paragraphs 逐段读取会更贴近 Word 里的真实呈现效果。第二个是空单元格的处理读取出来的字符串可能是空字符串但这不代表单元格不存在处理数据时别把它丢掉。第三个细节其实是我踩过最多遍的坑不要把表格首行默认当成表头。很多业务表前面两行是大标题、合并行说明真正的数据表头在第三行。如果代码里写死data[0]当列名后面必错。这个设计上要做成可配置的。3.3 合并单元格、嵌套表格等“疑难结构”处理真实世界里的 Word 表格很少规规矩矩。最让人头疼的是合并单元格它表面上一行只有两个格子但另一行有四个格子直接按行读取会导致二维矩阵形状不一致或者同一行返回的单元格数量比可见格数多。合并单元格在 XML 里的表示方式是 gridSpan水平合并和 vMerge垂直合并底层多个w:tc可能指向同一个表格单元。用 python-docx 读取时你会发现 row.cells 返回的某些单元格其实是同一个对象的重复引用。如果只需要文本直接遍历一般不会出大问题因为重复引用的单元格内容一致。但如果要做矩阵化数据合并就要把合并单元格的值填充到所有对应位置。下面这段代码做了一件简单的事用单元格底层 tc 对象做去重标记避免重复输出。def read_table_ext(table): matrix [] for row in table.rows: row_data [] seen set() for cell in row.cells: tc cell._tc if tc not in seen: seen.add(tc) row_data.append(\n.join(p.text for p in cell.paragraphs)) else: row_data.append(None) # 表示复用之前的值 matrix.append(row_data) return matrix比合并单元格更隐蔽的是嵌套表格——单元格里又套了一个表。你用 cell.text 读文本时嵌套表格的文字会被混进外层单元格导致外层表格的数据出现多余内容。区分内外层的方法是遍历外层表格的 cell用cell.tables单独处理嵌套表不要让嵌套表的文本污染外层结构。4. 全流程实操从单表提取到批量汇入 Excel4.1 单文档多表导出为多工作表先做一个最常见需求一个 Word 文档里有十几张表要把每张表分别导出到 Excel 的独立工作表。这个场景很适合用 pandas 处理数据矩阵再用 openpyxl 写入工作簿。import docx import pandas as pd from openpyxl import Workbook def table_to_df(table, has_headerTrue): data [] for row in table.rows: row_data [] for cell in row.cells: row_data.append(\n.join(p.text for p in cell.paragraphs)) data.append(row_data) if not data: return pd.DataFrame() if has_header and len(data) 1: return pd.DataFrame(data[1:], columnsdata[0]) return pd.DataFrame(data) doc docx.Document(report.docx) wb Workbook() wb.remove(wb.active) # 删除默认的空表 for i, table in enumerate(doc.tables): df table_to_df(table) ws wb.create_sheet(titlef表格{i1}) # 用 iterrows 逐行写入保留单元格内换行效果 for row_index, row in df.iterrows(): ws.append(list(row)) wb.save(report_tables.xlsx)这里有一个操作细节openpyxl 写入时如果你单元格字符串里有\n换行符Excel 默认不会自动换行显示。要让导出结果看起来和 Word 原表一致你需要给这些单元格设置换行样式。代码里加一下Alignment(wrap_textTrue)比较稳妥不然数据没丢看着却是挤成一坨。4.2 多文档同类表格跨文件合并比单文件导出更接近“提效刚需”的是多文件汇总。举个例子你收集了 30 份项目周报每份第 5 张表是“任务清单”字段一致要把它们拼成一张总表。这里最忌讳的事情是用“第几个表格”来定位目标表因为同事可能随手在目标表前插入一张截图说明表格序号就全变了。更可靠的思路是表头语义匹配——读取表格第一行判断是否包含你期望的字段名命中才纳入汇总。这种逻辑抗干扰能力很强加一列来源文件名还能追溯。import docx import pandas as pd import glob def find_table_by_header(doc, keywords): for table in doc.tables: if not table.rows: continue first_cell_texts [c.text.strip() for c in table.rows[0].cells] joined .join(first_cell_texts) if any(kw in joined for kw in keywords): return table return None frames [] for path in glob.glob(reports/*.docx): try: doc docx.Document(path) except Exception as e: print(f跳过 {path}: {e}) continue table find_table_by_header(doc, [任务名称, 负责人, 状态]) if table is None: continue header [c.text.strip() for c in table.rows[0].cells] rows_data [] for row in table.rows[1:]: row_data [c.text.strip() for c in row.cells] if any(row_data): # 跳过完全空行 rows_data.append(row_data) df pd.DataFrame(rows_data, columnsheader) df[来源文件] path frames.append(df) if frames: result pd.concat(frames, ignore_indexTrue) result.to_excel(merged_result.xlsx, indexFalse)这段代码我特意加了“来源文件”列。做数据汇总时保留来源信息非常划算后期一旦发现某行数据对不上可以立刻回源头文件核对而不是拿着一张几百行的合并表乱猜。4.3 让提取出来的数据能直接入库从 Word 表格提取出来的数据默认是字符串但数据库或 BI 系统更希望拿到干净、符合类型的值。这里需要做字段级清洗绝对不能全列一刀切。我举一个非常典型的例子金额字段在 Word 里可能写成 “1,234.50”数字列里有千分位逗号人名、地址这种文本字段里也可能出现逗号。如果你写一个通用的“删除所有逗号”逻辑会把文本字段里的分隔符也删掉。我的做法是先通过表头语义判断哪些是数值列只对这些列做去逗号、去货币符号、转 float 的操作。numeric_keywords [金额, 数量, 价格, 单价, 总计] def clean_cell(value, is_numericFalse): text str(value).strip() if not text: return None if is_numeric else if is_numeric: text text.replace(,, ).replace(, ).replace(¥, ) try: return float(text) except ValueError: return None return text同一个单元格里如果有多个段落清洗时记得把换行符保留还是替换成空格要按业务定。比如“地址”字段跨两行入库时合并成一行没问题但“备注”字段里有编号列表换行就不能删。这些细节决定了你的自动化流程能不能真正落地而不是只能在演示环境里跑通。5. 避坑实录我在实际项目中踩过的 Word 表格雷区5.1 表格列宽读出来和界面显示不一致有朋友问过“Word 表格列宽无法拖动”我在解析时也遇到过用 python-docx 读某个表格的列宽拿到的数值跟文档里肉眼看到的效果完全对不上。这个问题的根源在于 Word 里列宽信息不是只存一份。文档里有 tblGrid 的 gridCol 定义也有每个单元格的 tcW 定义。两者一旦冲突Word 渲染时会按更具体的设置为准。python-docx 的 column.width 读取的是网格列宽但单元格实际的显示宽度可能另有数值。如果要精确获取列宽建议同时读两种并且关注单位换算。Word 内部默认长度单位是 dxa1 厘米约等于 567 dxa而 python-docx 返回的是 EMU这两者不能直接拿来比较。我的建议是除非你要做精确排版还原否则别在列宽上花太多时间。表格提取场景里列宽对数据内容没有影响把精力放在内容和结构上更划算。5.2 老 .doc 格式导致的血泪教训有一回我处理一批历史招标文件表面上看都是 .docx结果程序跑了一半崩掉报错“Package not found”。排查后才发现里面混了不少真正的老 .doc 文件改成 .docx 后缀也骗不过解析器。python-docx 对 .doc 完全无能为力所以必须先转换格式。我验证过最稳的免费方案是用 LibreOffice 命令行批量转换soffice --headless --convert-to docx --outdir converted_dir *.doc这个命令在 Windows 和 Linux 上都能用。但转换有个小风险原文档如果用了特殊字体、复杂嵌套表格转换后表格网格会有一丁点儿偏移。所以转换完最好抽查几份关键文件确认表头字段没乱。更稳妥的工程方案是在批量处理入口处做文件类型预检先读文件头判断是不是 PK 开头的 zip 包不符合的直接单独列出不让它中断整个批处理流程。def is_docx(path): with open(path, rb) as f: return f.read(2) bPK5.3 内容读取为空或内容错位这是排查最多的一类问题表现形式有两种单元格读出来是空字符串或者整行内容整体错位。第一种情况通常是因为单元格里不是纯文本而是图片、公式对象或者文本框。单元格里的图片无法用 cell.text 读取公式如果是 OLE 对象同样不在文本流里。遇到这种表格完整的提取方案需要深入 XML 关系和 media 资源非常折腾。我的建议是“抓大放小”先确保文字和数字全部提取正确图片按单元格坐标单独导出并命名最后再做人工补位检查。办公场景里这种混合内容表通常只占少数不值得为了它写一套完整解析引擎。第二种错位更隐蔽原因是单元格内有多余的空白段落导致 rows 的数量看起来比实际多。处理方法是读取前先 strip 文本遇到完全空行直接跳过。判断空行的条件别只查第一个单元格要检查整行所有单元格是否都为空否则可能误删有内容的行。5.4 宏安全与 VBA 方案的边界网上很多教程推荐用 VBA 处理 Word 表格说到底是 Word 内置能力写起来快。但部署时你就会撞上“宏安全”问题公司电脑默认禁用宏或者 IT 策略不允许运行带宏的文件。用 Python 脚本就没有这个限制脚本不需要 Word 进程参与也不涉及安全受信任位置的设置。如果只会 VBA 怎么办我一般建议小任务、一次性操作可以继续用宏涉及到批量、定时、跨机器部署必须迁移到 Python 脚本。你只需要把 .py 文件放到统一目录用任务计划程序定时执行输出文件到共享文件夹全程不需要人工点开 Word效率和可靠性都高出不少。6. 把处理能力沉淀成一个可复用的工具前面给了很多代码片段但真正到落地阶段最值钱的是把这些能力收敛成一个能反复调用的工具模块。我习惯把 Word 表格处理逻辑按照“读表、定表、清洗、导出”四层拆开每层独立成函数对外暴露几个主要入口。比如我长期维护的一个模块大概长这样extract_tables_to_excel(source_docx, output_xlsx)单文档多表导出merge_tables_from_dir(input_dir, output_xlsx)多文档同类表汇总find_table_by_header(doc, keywords)按表头语义定位目标表clean_table_data(df, numeric_columns)字段级清洗每个函数都尽量保持单一职责让调用方通过参数控制行为而不是每来一个新需求就复制一套旧代码改两行。这个习惯长期看能节省非常多的时间尤其是你的业务每隔一段时间就会新增一类报告模板的情况下。另外打包需求也值得一提。如果你想把工具交给不会装 Python 的同事用可以用 PyInstaller 打包成 exe。打包时最典型的坑是资源文件路径问题脚本里如果引用了模板文件或配置文件打包后要用 sys._MEIPASS 来定位临时解压目录否则会报文件找不到。方向确认没问题细节去查一下官方文档即可。我自己的习惯是处理任何 Word 表格之前先复制一份源文件到备份目录脚本永远只读副本。原因是提取过程中一旦发现数据异常你还能回到源文件核对原始状态直接在源文件上操作改坏了连后悔的机会都没有。这个习惯让我逃过好几次“改错表还不自知”的灾难。最后再分享一个我摸索出来的通用规律凡是你要用 Python 处理的数据只要能保证入口是 .docx 格式后面所有步骤都不太容易翻车。最耗时间的往往不是写代码而是识别那些乱七八糟的手工排版、多余空行、合并单元格。把这个抽象认知建立起来后面无论遇到多刁钻的表格你都会知道底层是 XML接口是 python-docx耐心拆结构就一定有解。
返回列表