☰
Python办公文档自动化:Word与Excel创建解析实战
2026/10/16 0:05:35 网站建设 项目流程

1. 为什么办公文档自动化是绕不开的一课

做 Python 自动化的人,早晚会撞上 Office 文档这堵墙。你可能一开始只是想批量改几个 Word 里的日期,或者从一堆 Excel 里把某几列数据抠出来汇总,结果搜着搜着就掉进了python-docx、openpyxl、xlrd、pandas这一堆库的坑里,光选型就耗掉半天。更别提那些"看起来能跑、换个文件就报错"的诡异情况——合并单元格读出来是 None、Word 里的表格跨页后行数对不上、生成的文档打开提示"内容有问题需要修复"。

这篇是 Python 系列的第一篇,专门聊创建和解析 Office 文档这件事。所谓"创建",就是让程序从零生成一份 Word、Excel 或 PowerPoint;所谓"解析",就是把已有的文档拆开,把里面的文字、表格、样式、公式读出来变成程序能处理的数据。这两件事合起来,基本覆盖了日常办公自动化的八成场景:批量生成合同、自动填报表、从报告里提取关键指标、把数据表转成图文并茂的汇报材料。

适合谁看?如果你已经会写基本的 Python 循环和函数,但一碰到.docx、.xlsx就不知道从哪下手,这篇就是给你准备的。我会把选型逻辑、核心 API 的用法、以及那些文档里不会写的踩坑经验都摊开讲。全程用从业者之间交流的口吻,不绕弯子,能直接抄的代码我都给出来。

先说一个反直觉的结论:处理 Office 文档,最难的从来不是写代码,而是理解文档本身的格式规范。.docx和.xlsx本质上都是 ZIP 压缩包,里面装着一堆 XML。你用的库只是帮你把这层 XML 包装成了 Python 对象。理解这一点,后面遇到各种"莫名其妙"的问题时,你就能顺着 XML 结构去排查,而不是对着报错干瞪眼。

2. 选型之前先搞清楚:你到底在跟什么格式打交道

2.1 老格式和新格式是两套完全不同的东西

很多人栽的第一个跟头,就是没分清.doc和.docx、.xls和.xlsx。这两组看起来只差一个字母,底层却是天壤之别。

.doc、.xls、.ppt是微软早期的二进制格式,文件内部是一段紧凑的二进制流,没有公开的、友好的解析规范。Python 想读它们,基本只能靠xlrd(仅限 xls)、pywin32调用本机 Office 组件这类方式,跨平台性差,还经常因为环境没装 Office 而失败。

.docx、.xlsx、.pptx是 2007 年之后基于 Office Open XML 规范的格式,本质是 ZIP 包加 XML。你可以直接把一个.docx的后缀改成.zip,解压后能看到word/document.xml、word/styles.xml这些文件。这种开放性让第三方库可以纯 Python 解析,不依赖 Office 软件本身。

所以选型的第一条铁律:新项目一律只支持新格式。如果业务方丢给你一堆.doc,正确做法是先批量转成.docx再处理,而不是硬着头皮去啃二进制。转换可以用 LibreOffice 的命令行模式批量做,这个后面会讲。

2.2 三大件对应的主力库

文档类型创建解析说明
Word (.docx)python-docxpython-docx读写一体,API 直观
Excel (.xlsx)openpyxlopenpyxl读写一体,支持公式和样式
Excel 数据分析pandaspandas适合纯数据场景,不碰样式
PowerPoint (.pptx)python-pptxpython-pptx读写一体
老格式 (.doc/.xls)不推荐xlrd(仅xls)建议先转换

这里有个常见的误区:很多人一上来就用 pandas 读 Excel,觉得一行pd.read_excel()就搞定,何必学 openpyxl。但 pandas 的定位是数据分析,它读 Excel 时会把所有内容当成数据表,样式、合并单元格、公式、批注这些信息全部丢掉。如果你只是想把表格数据拿来做统计,pandas 确实快;但如果你要保留格式、要写回带样式的报表、要处理合并单元格,就必须用 openpyxl。

我的经验是:数据进 pandas,格式进 openpyxl。两者可以配合,用 openpyxl 读带格式的原始文件,把纯数据部分转成 DataFrame 做计算,算完再用 openpyxl 写回带样式的模板。

2.3 安装与环境准备

pip install python-docx openpyxl python-pptx pandas

装完之后建议先跑一个最小验证,确认库能正常导入:

import docx import openpyxl import pptx print(docx.__version__ if hasattr(docx, '__version__') else 'docx ok') print(openpyxl.__version__) print(pptx.__version__)

注意:python-docx的包名是python-docx,但导入时写import docx;python-pptx导入时写import pptx。这个命名不一致坑过不少人,装的时候别装成docx那个同名的老包,那个是另一个项目。

3. Word 文档:从生成一份合同到拆解一份报告

3.1 用 python-docx 从零生成文档

先看创建。假设你要批量生成一批通知书,每份的姓名、日期、金额不同,但格式完全一样。最朴素的做法是准备一个模板,然后替换占位符。但 python-docx 本身没有"查找替换"这种高级功能,你得自己遍历段落和表格里的 run。

先理解 python-docx 的对象模型:一个 Document 包含若干 Paragraph(段落),每个 Paragraph 包含若干 Run(文本片段)。Run 是样式的最小单位,同一段里不同字体、不同加粗的文本会被拆成多个 Run。这个模型很关键,因为替换文字时如果直接改 Paragraph.text,会把整段的样式全冲掉。

from docx import Document from docx.shared import Pt, RGBColor from docx.enum.text import WD_ALIGN_PARAGRAPH doc = Document() # 添加标题 title = doc.add_heading('项目结项通知书', level=1) title.alignment = WD_ALIGN_PARAGRAPH.CENTER # 添加正文段落 p = doc.add_paragraph('兹有') run = p.add_run('张三') run.bold = True run.font.color.rgb = RGBColor(0xC0, 0x00, 0x00) p.add_run('同志,您参与的项目已通过验收。') # 添加表格 table = doc.add_table(rows=3, cols=2) table.style = 'Table Grid' table.cell(0, 0).text = '项目名称' table.cell(0, 1).text = '某跨平台系统' table.cell(1, 0).text = '结项日期' table.cell(1, 1).text = '2024-06-01' table.cell(2, 0).text = '负责人' table.cell(2, 1).text = '张三' doc.save('notice.docx')

这段代码里几个点值得展开。add_heading用的是内置样式,level=1 对应"标题 1"。如果你想要自定义字体,得去改样式对象,而不是直接设 run 的字体——因为标题样式会覆盖 run 级别的设置。table.style = 'Table Grid'是给表格加边框,不加的话默认是无框线,打印出来一片空白。

3.2 模板替换:为什么直接改 text 会翻车

批量生成场景下,更实用的做法是准备一个.docx模板,里面写好{{name}}、{{date}}这样的占位符,然后程序读模板、替换、另存。但前面说了,python-docx 没有内置替换。网上流传的写法是遍历doc.paragraphs,对每个paragraph.text做replace,然后重新赋值。这个写法有个致命问题:它会把段落里所有 Run 的样式合并成一个。

正确的做法是遍历 Run,在 Run 级别替换:

def replace_in_paragraph(paragraph, mapping): for run in paragraph.runs: for key, value in mapping.items(): if key in run.text: run.text = run.text.replace(key, value)

但这里还有个坑:占位符可能被拆到多个 Run 里。比如{{name}}在 Word 里可能因为编辑历史被拆成{{、name、}}三个 Run。这时候单个 Run 里根本找不到完整的占位符。解决办法有两种:一是替换前先把段落里所有 Run 的文本合并判断,如果占位符跨 Run,就重建整个段落;二是用更专业的库如docxtpl,它基于 jinja2 模板引擎,专门解决这个问题。

我的建议是:简单场景用 python-docx 手动替换,复杂模板直接上 docxtpl。docxtpl 的用法是doc.render(context),context 是个字典,模板里写{{ name }},还支持循环和条件判断,做批量合同、证书这类场景非常省事。

3.3 解析 Word:表格和段落要分开处理

解析比创建更容易踩坑。一个 Word 文档里,文字可能出现在三个地方:普通段落、表格单元格、页眉页脚。很多人只遍历doc.paragraphs,结果发现表格里的内容全丢了。

def extract_all_text(doc): texts = [] # 普通段落 for p in doc.paragraphs: if p.text.strip(): texts.append(p.text) # 表格 for table in doc.tables: for row in table.rows: cells = [cell.text.strip() for cell in row.cells] texts.append(' | '.join(cells)) return texts

这里有个隐蔽的坑:合并单元格会导致同一段文字被重复读取。比如一个横向合并的单元格,row.cells会返回多个指向同一个底层单元格的对象,你读到的文字就重复了。判断方法是比较 cell 的_tc属性(底层 XML 元素)是否相同:

seen = set() for cell in row.cells: if id(cell._tc) not in seen: seen.add(id(cell._tc)) # 处理这个 cell

另一个坑是跨页表格的行数。Word 里一个表格如果跨页,python-docx 读出来仍然是一个 table 对象,行数是完整的,这点不用担心。但如果文档里有两个独立的表格被排版成看起来连在一起,那就是两个 table,需要分别处理。

3.4 页眉页脚和文本框:容易被忽略的角落

页眉页脚的内容不在doc.paragraphs里,要单独访问:

for section in doc.sections: header = section.header for p in header.paragraphs: print(p.text) footer = section.footer for p in footer.paragraphs: print(p.text)

文本框(Text Box)更麻烦,python-docx 没有直接 API,得深入到 XML 层面去遍历w:txbxContent节点。如果业务文档里大量用文本框排版,解析起来会非常痛苦。我的经验是:如果解析需求涉及文本框,先评估能不能让业务方改用普通段落,实在不行再上 XML 解析。

4. Excel 文档:数据、公式、样式三条线要分开走

4.1 openpyxl 的读写基本盘

Excel 是办公自动化里出现频率最高的。openpyxl 的核心对象是 Workbook、Worksheet、Cell。读一个文件:

import openpyxl wb = openpyxl.load_workbook('data.xlsx', data_only=True) ws = wb['Sheet1'] for row in ws.iter_rows(min_row=2, values_only=True): name, age, score = row print(name, age, score)

这里data_only=True是个关键参数。Excel 单元格里存的可能是公式,比如=A1+B1。默认情况下 openpyxl 读出来的是公式字符串本身;设了data_only=True,它读的是公式上一次被 Excel 计算后缓存的结果。注意,这个缓存值只有在文件被 Excel 打开并保存过之后才存在。如果文件是程序生成的、从没被 Excel 打开过,data_only=True读出来会是 None。这个坑非常隐蔽,很多人卡在这里半天找不到原因。

写文件:

wb = openpyxl.Workbook() ws = wb.active ws.title = '汇总' ws.append(['姓名', '部门', '金额']) ws.append(['张三', '研发', 12000]) ws['D1'] = '=SUM(C2:C2)' wb.save('output.xlsx')

ws.append()按行追加,适合从列表批量写入。写公式直接写字符串,但注意公式里的函数名和参数分隔符要符合 Excel 规范,中文版 Excel 用逗号分隔参数,这个和英文版一致,不用担心。

4.2 样式、合并单元格与列宽

生成报表时,样式很重要。openpyxl 的样式通过Font、PatternFill、Border、Alignment这些对象组合:

from openpyxl.styles import Font, PatternFill, Alignment, Border, Side header_font = Font(name='微软雅黑', size=11, bold=True, color='FFFFFF') header_fill = PatternFill('solid', fgColor='4472C4') center = Alignment(horizontal='center', vertical='center') for cell in ws[1]: cell.font = header_font cell.fill = header_fill cell.alignment = center ws.merge_cells('A1:C1') ws.column_dimensions['A'].width = 15

合并单元格要注意:合并后只有左上角那个单元格能写值,其他单元格的值会被清空。读取时,合并区域的非左上角单元格返回 None。如果你要遍历读取,得先判断单元格是否在合并区域内,或者用ws.merged_cells.ranges拿到所有合并区域,自己映射回左上角。

4.3 大文件处理:为什么你的脚本越跑越慢

openpyxl 有个性能问题:它默认把整个工作簿加载到内存,几万行的表还能扛,几十万行就会吃光内存。这时候有两个选择。

一是用read_only=True模式,它用流式读取,内存占用小,但只能顺序读,不能随机访问单元格,也不能修改:

wb = openpyxl.load_workbook('big.xlsx', read_only=True) ws = wb.active for row in ws.iter_rows(values_only=True): process(row) wb.close()

二是写大文件时用write_only=True,配合ws.append()逐行写,避免在内存里维护完整的工作表对象:

wb = openpyxl.Workbook(write_only=True) ws = wb.create_sheet() for record in data_stream: ws.append(record) wb.save('big_output.xlsx')

实测下来,写 50 万行数据,普通模式内存能飙到 2GB 以上,write_only 模式稳定在几百 MB。这个差距在处理日志导出、数据备份这类场景里是决定性的。

4.4 pandas 和 openpyxl 怎么配合

前面说过,纯数据分析用 pandas 更顺手。典型流程是:用 openpyxl 读带格式的原始表,把数据区转成 DataFrame 做清洗和计算,再用 openpyxl 把结果写回带样式的模板。

import pandas as pd import openpyxl # 读数据 df = pd.read_excel('raw.xlsx', sheet_name='明细', skiprows=2) df['金额'] = df['数量'] * df['单价'] summary = df.groupby('部门')['金额'].sum().reset_index() # 写回带样式的模板 wb = openpyxl.load_workbook('template.xlsx') ws = wb['汇总'] for i, row in summary.iterrows(): ws.cell(row=i+2, column=1, value=row['部门']) ws.cell(row=i+2, column=2, value=row['金额']) wb.save('report.xlsx')

skiprows=2是因为原始表前两行是标题和说明,不是数据。这种参数要根据实际文件调整,没有通用值。pandas 读 Excel 时如果表头不在第一行,一定要用header和skiprows配合,否则列名会错位。

5. 那些文档里不会写的踩坑实录

5.1 文件被占用导致保存失败

最常见的报错之一:PermissionError: [Errno 13] Permission denied。原因几乎都是目标文件正在被 Excel 或 Word 打开。Windows 下 Office 会锁定文件,程序写不进去。解决办法不是加 try-except 硬扛,而是从流程上避免:生成的文件名带时间戳,或者先写到临时文件再移动。

import os import tempfile import shutil tmp = tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) wb.save(tmp.name) tmp.close() shutil.move(tmp.name, 'output.xlsx')

这样即使 output.xlsx 被占用,也只是最后一步 move 失败,不会丢数据。

5.2 中文乱码与字体问题

生成的文档里中文显示成方框,通常是字体没设对。python-docx 默认用 Calibri,这个字体不含中文字形。解决办法是显式设置中文字体:

from docx.oxml.ns import qn run = p.add_run('中文内容') run.font.name = '微软雅黑' run._element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')

注意那个w:eastAsia的设置,只设font.name对中文不生效,必须同时设 eastAsia 属性。这是 python-docx 的一个历史遗留问题,官方文档里提得很少,但实际项目里几乎必踩。

5.3 日期格式的坑

Excel 里的日期本质是个浮点数,整数部分是天数,小数部分是时间。openpyxl 读出来可能是 datetime 对象,也可能是数字,取决于单元格格式。如果读出来是数字,用openpyxl.utils.datetime.from_excel()转换。写日期时,直接写 datetime 对象,openpyxl 会自动处理格式;如果写字符串,Excel 会当成文本,后续没法做日期计算。

5.4 批量转换老格式

前面提到.doc转.docx,用 LibreOffice 命令行最省事:

soffice --headless --convert-to docx --outdir ./converted ./old_files/*.doc

这个命令在 Linux 和 Windows 都能用(Windows 下是soffice.exe)。转换是批量的,但要注意 LibreOffice 的排版引擎和微软 Office 有细微差异,复杂排版的文档转换后可能错位。所以转换后最好抽样检查几份。

6. 把创建和解析串成一条流水线

单点操作会了之后,真正的价值在于把创建和解析串起来,形成自动化流水线。举个典型场景:每月从多个部门收集 Excel 报表,汇总后生成一份 Word 分析报告。

流程拆解:先用 openpyxl 遍历各部门的 Excel,读取数据并校验(比如检查必填列是否为空、数值是否在合理范围);把校验通过的数据汇总到一个 DataFrame,做统计计算;然后用 python-docx 打开报告模板,把统计结果填入对应的占位符和表格;最后保存并归档。

这个流水线里,校验环节最容易被忽略但最重要。我见过太多脚本因为某个部门的表多了一行空行、或者列顺序变了就直接崩溃。健壮的做法是:读取时用列名定位而不是列号,遇到缺失列记录警告并跳过该文件,而不是让整个流程挂掉。

required_cols = ['部门', '项目', '金额'] for file in files: try: df = pd.read_excel(file) missing = [c for c in required_cols if c not in df.columns] if missing: log.warning(f'{file} 缺少列: {missing},跳过') continue # 继续处理 except Exception as e: log.error(f'{file} 读取失败: {e}') continue

这种"跳过坏数据、记录日志、继续处理"的模式,在生产环境里比"一遇错就抛异常"实用得多。因为批量任务里,个别文件有问题不应该影响整体。

再补一个经验:生成文档时保留中间产物。比如汇总的 DataFrame 存一份 CSV,填充后的 Word 存一份草稿,这样出问题时能快速定位是哪一步错了,而不是从头重跑。这个习惯在调试阶段能省大量时间。

7. 几个提升效率的实用技巧

第一个技巧是关于路径处理的。Windows 下路径用反斜杠,Linux 用正斜杠,写死任何一种都会在另一个平台翻车。统一用pathlib:

from pathlib import Path base = Path('data') for f in base.glob('*.xlsx'): print(f.name, f.stem, f.suffix)

glob支持通配符,stem是不带后缀的文件名,这些比字符串拼接靠谱得多。

第二个技巧是给文档操作加日志。批量处理几十上百个文件时,出问题在所难免,有日志才能快速定位。用标准库的 logging,配置成同时输出到控制台和文件:

import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s', handlers=[logging.FileHandler('process.log'), logging.StreamHandler()] )

第三个技巧是处理超大批量时的进度反馈。用tqdm包一层循环,能直观看到进度和预估剩余时间:

from tqdm import tqdm for f in tqdm(files, desc='处理中'): process(f)

第四个技巧,也是我个人踩坑最多的一条:永远不要相信输入文件的格式是规范的。业务方给你的 Excel,可能有隐藏行、有筛选状态、有冻结窗格、有数据验证、有批注,这些在 openpyxl 里读出来都是额外的属性。如果你的脚本只关心数据,读的时候用values_only=True忽略格式;如果关心格式,就要做好遇到各种意外属性的准备。最稳妥的做法是先用一个小脚本把文件的结构打印出来看看,再写正式处理逻辑。

wb = openpyxl.load_workbook('unknown.xlsx') for ws in wb.worksheets: print(f'Sheet: {ws.title}, 维度: {ws.dimensions}, 最大行: {ws.max_row}, 最大列: {ws.max_column}') print(f'合并区域: {list(ws.merged_cells.ranges)}') print(f'冻结窗格: {ws.freeze_panes}')

这几行打印出来的信息,能帮你快速判断这个文件"干净不干净",值不值得直接上自动化,还是需要先做一轮清洗。

Office 文档自动化这件事,库的 API 只是表层,真正决定成败的是你对文档格式的理解和对异常情况的预判。把上面这些坑都趟过一遍之后,你会发现大部分问题都有规律可循,无非是格式差异、样式覆盖、文件占用这几类。下一篇我会聊 PDF 和图片的处理,那又是另一套完全不同的逻辑了。

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

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

立即咨询