销售月报、验收单、项目周报……几十个 docx,每个里面嵌着几张表,领导要的是一张汇总表。手动复制粘贴是体力刑具。下面这份脚本本次完整实跑过:2 个多表 docx,4 张表 12 行数据全部正确抽出,带「源文件 + 表序号」溯源列,重跑幂等。全程只用 Python 标准库,目标机器不用 pip install 任何东西。
.docx 本质是 zip 包,表格全在 word/document.xml 里。解压读 XML,遍历 <w:tbl> 即可——所以不依赖 python-docx,服务器、旧电脑都能跑。
import glob, os, csv, zipfile
from xml.etree import ElementTree as ET
W = lambda t: '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}' + t
def cell_text(tc):
# 坑1:只取单元格「直接子级」的 w:p,防止嵌套表格的文字混进来
parts = [''.join(t.text or '' for t in p.iter(W('t')))
for p in tc if p.tag == W('p')]
return '\n'.join(parts)
all_rows = []
for f in sorted(glob.glob('*.docx')):
with zipfile.ZipFile(f) as z:
root = ET.fromstring(z.read('word/document.xml'))
tbl_idx = [0]
def rec(el):
for ch in el:
if ch.tag == W('tbl'):
tbl_idx[0] += 1
for tr in ch.findall(W('tr')): # 坑1:findall 而非 iter
cells = []
for tc in tr.findall(W('tc')):
gs = tc.find(f'{W("tcPr")}/{W("gridSpan")}')
span = int(gs.get(W('val'))) if gs is not None else 1
cells.append(cell_text(tc))
cells.extend([''] * (span - 1)) # 坑3:见下文
if any(c.strip() for c in cells):
all_rows.append([os.path.basename(f), tbl_idx[0]] + cells)
rec(ch) # 坑1:递归抓嵌套表
rec(root)
with open('word_tables_all.csv', 'w', newline='', encoding='utf-8-sig') as fo:
w = csv.writer(fo)
w.writerow(['源文件', '表序号', '列1', '列2', '列3'])
w.writerows(all_rows)
print(f'{len(all_rows)} rows')
构造 2 个 docx(华东月报含 2 张表:主表 + 备注表,备注表有空单元格;华南月报含主表 + 事项表),共 4 张表 12 行,全部正确提取:
销售月报-华东.docx, 1, 月份, 销售额, 负责人 销售月报-华东.docx, 1, 1月, 152,000, 张三 销售月报-华东.docx, 1, 2月, 183,500, 李四 销售月报-华东.docx, 1, 2月, 97,200, 张三 销售月报-华东.docx, 2, 备注区(空单元格正确输出为空串)... 销售月报-华南.docx, 1, 月份, 销售额, 负责人 销售月报-华南.docx, 1, 1月, 210,300, 王五 销售月报-华南.docx, 2, 日期, 事项 ... ... 共 12 行,输出 word_tables_all.csv(utf-8-sig)
输出到独立 CSV、源文件只读,重跑幂等。
tc.iter(w:tbl 内的 w:t)iter('w:tbl') 重复提取一遍(同一行数据出两次)。修法:抓 w:tr/w:tc 一律用 findall(只看直接子级),单元格文字只取直接子级 w:p,然后递归遍历让嵌套表自己作为独立表出现。t.text or '' 必须加,否则 ''.join 直接 TypeError。看起来低级,但报错点离真正的坑很远,第一眼容易怀疑编码。w:tc 数量比表头少,直接按位置拼进 CSV 会整行左移。读 w:tcPr/w:gridSpan 的 w:val,用空串补齐到正确列数。纵向合并(vMerge)同理,被合并的行会缺单元格,按同样思路补。python-docx 确实更省事(table.rows[i].cells[j].text),但它对嵌套表格和 gridSpan 的处理同样要手写补丁,且目标机器必须装包。对「在别人电脑上跑」的场景(详见 脚本在别人电脑上报错怎么办),纯 stdlib 是更稳的交付形态。要输出真正的 .xlsx 而不是 CSV,拿到汇总 CSV 后用 openpyxl 一次性转格式即可。