☰
用Python批量处理Word格式:python-docx脚本实战
2026/10/16 8:10:50 网站建设 项目流程

批量处理Word格式这种需求,做过办公室工作的人都能秒懂——每天面对一堆格式乱七八糟的文档,标题字体不一样、正文行距不统一、首行缩进有深有浅,手动调整一份可能一两分钟,一百份就是两三个小时,而且这种纯机械操作最容易疲劳漏改。我写过一个Python脚本,用python-docx批量处理,实测100份文档全流程跑完不到30秒,调整内容包括标题、正文、行距、缩进、表格内文字。这篇文章就把这个脚本从设计思路、核心代码到踩坑修复完整拆开讲一遍,主要用到的工具是python-docx库,适合遇到批量文档排版需求的行政、运营、实习生和一切被Word折磨过的朋友。不管你是学过一点Python还是完全没接触过代码,照着下面抄都能用起来。

1. 为什么是Python脚本?手动处理的成本到底有多高

1.1 传统处理方式有多折磨人

先还原一下手动调整Word格式的流程:拿到一份文档,先打开,Ctrl+A全选,把正文改为宋体小四,然后右键段落设置行距、首行缩进,再找到标题挨个改黑体三号,如果是表格还得进表格设置字体。单份文档只要格式规范,动作熟练的人可能一分多钟搞定,但实际情况往往不是这样:有的文档段落用了“Tab键+空格”模拟首行缩进,有的标题看起来是标题其实只是加粗正文,有的直接从网页复制过来带着内联样式。

100份文档意味着重复这个流程100次。就算每份只要2分钟,也是200分钟的纯机械劳动,过程中只要走神一次就可能漏改一两个标题。我自己有一次改到第70多份的时候,眼睛已经看花了,结果第78份整个小标题忘了改,最后被负责人挑出来返工,那种感觉实在不想体验第二次。

1.2 Python处理Word的底层逻辑

python-docx这个库做的事情,本质上不是模拟人打开Word操作,而是直接读取docx文件中的XML结构。Word文档本质上是一个打包的XML文件集合,段落、字体、样式、表格都对应到结构化的节点上。python-docx把这些节点封装成Paragraph、Run、Table之类的对象,脚本可以遍历它们,批量修改属性,改完另存为新的docx。

打个比方:手动调整是拿着一堆纸质文件用笔改,脚本处理是直接找到打印机的打印源文件,按规则批量替换里面的字体名、字号、对齐方式。这就是为什么它能做到几十秒处理一百份——不需要启动Word程序,也不需要在界面上渲染,纯文本层面的修改,速度自然快得多。

1.3 这个脚本适合谁用

经过我自己的实践,最需要这类脚本的是这几类人:一是公司行政和文员,经常要收各部门交上来的周报、总结,格式五花八门,需要统一后才能归档;二是学校里的助教、社团负责人,收上来的作业和活动方案经常字号行距全随缘;三是律所、咨询公司的初级员工,合同和投标文件的格式规范极其严格,人工调整容易出错;四是任何偶尔需要处理几十份以上文档的非技术岗位。

这个脚本用到的python-docx库安装很简单,不依赖Office软件本身,纯命令行环境下也能跑。Windows、Mac、Linux通用,只要有Python环境就行。

2. 核心代码实现:从零开始批量调整格式

2.1 环境准备和依赖安装

先装Python环境,然后安装python-docx库。在命令行里执行:

pip install python-docx

如果网络慢,可以用清华镜像源:

pip install python-docx -i https://pypi.tuna.tsinghua.edu.cn/simple

这里有个重要提醒:python-docx只能处理.docx格式,不能处理老版的.doc格式。如果文件夹里混着.doc文件,脚本会报错或直接跳过。遇到这种情况,要么在Word里手动另存为docx,要么用Office的批量转换功能,或者用LibreOffice命令行做转换。我一般会先把.doc文件筛出来单独处理。

安装完之后,在Python里验证一下能不能导入:

import docx print(docx.__version__)

能正常输出版本号就说明环境没问题。

2.2 遍历文件夹里所有Word文档

处理多份文档的第一步是找到它们。用glob模块可以轻松地列出指定文件夹下所有以.docx结尾的文件:

import glob import os folder_path = "./待处理文档" # 改成你自己的文件夹路径 docx_files = glob.glob(os.path.join(folder_path, "*.docx")) docx_files.sort() # 排序,保证处理顺序稳定 print(f"共找到 {len(docx_files)} 份待处理文档")

实操时建议先跑一遍这段代码,确认文件数量对不对,避免后期改完发现漏了子文件夹里的文件。如果文档分布在多个子文件夹中,可以把glob改成递归模式:

docx_files = glob.glob(os.path.join(folder_path, "**", "*.docx"), recursive=True)

2.3 全文档正文格式统一的两种思路

这是整个脚本的核心。需要知道一个关键概念:Word文档里每个段落都是由一个或多个Run组成的,字体、字号这些细粒度格式挂在Run上,而段落对齐、缩进、行距这些挂在Paragraph上。

思路A:修改文档的Normal样式。适用于那些没有太多内联格式覆盖的“干净”文档。代码非常简短:

from docx import Document from docx.shared import Pt from docx.enum.text import WD_LINE_SPACING def set_normal_style(doc, font_name='宋体', font_size=12, line_spacing=1.5): style = doc.styles['Normal'] style.font.name = font_name style.font.size = Pt(font_size) style.paragraph_format.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE

思路B:逐段落、逐Run修改。如果你处理的文档来源很杂,网上下载的、别人机器上写的、从PDF转过来的,里面到处都是内联格式和硬设置,只改Normal样式往往没效果,因为段落的直接格式优先级高于样式。这时候就得手动遍历:

def set_paragraph_font(doc, font_name='宋体', font_size=12, line_spacing=1.5): for para in doc.paragraphs: # 跳过空段落,避免多余操作 if not para.text.strip(): continue # 处理段落间距和行距 pf = para.paragraph_format pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE pf.space_before = Pt(0) pf.space_after = Pt(6) # 处理每个Run的字体 for run in para.runs: run.font.name = font_name run.font.size = Pt(font_size) # 中文字体需要单独设置,这个坑后面细说 from docx.oxml.ns import qn r = run._element r.rPr.rFonts.set(qn('w:eastAsia'), font_name)

我实际测试下来,思路B更稳。很多Word文档的段落样式是“正文缩进+手动空格”,或者“直接设置了宋体五号又叠加了主题字体”,这些情况下只有逐Run设置才覆盖得掉。

2.4 标题级别的统一调整

标题和正文的字体规范通常不一样。比如正文是宋体小四,标题需要黑体三号加粗。python-docx里判断一个段落是不是标题,最简单的方法是看它的样式名:

def set_headings(doc): for para in doc.paragraphs: style_name = para.style.name.lower() # 匹配以 heading 1、heading 2 开头的样式名 is_h1 = style_name.startswith('heading 1') is_h2 = style_name.startswith('heading 2') if not (is_h1 or is_h2): continue # 设置标题字体 for run in para.runs: run.font.name = '黑体' run.font.bold = True if is_h1: run.font.size = Pt(15) # 三号 elif is_h2: run.font.size = Pt(14) # 四号 # 同步设置中文字体 run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') # 段落间距:标题前后适当留白 pf = para.paragraph_format pf.space_before = Pt(12) pf.space_after = Pt(6)

这里要注意的是,很多从网页复制过来的文档,标题看起来是“黑体、二号”,但样式名其实叫“正文”或“紧凑”,因为复制时丢失了Word样式定义。这种情况靠样式名判断会漏掉。我加了一层兜底逻辑:如果段落字数小于20并且整段加粗,我就把它当作小标题处理。这个逻辑不是100%准确,但遇到不规范的文档时能救回大部分漏网之鱼。

2.5 表格内文字的处理

Word文档里经常有表格,表格里的字体和正文又不一样。python-docx遍历表格的层级关系是:document.tables -> table.rows -> row.cells -> cell.paragraphs -> paragraph.runs。代码可以这样写:

def set_table_font(doc, font_name='宋体', font_size=12): for table in doc.tables: for row in table.rows: for cell in row.cells: for para in cell.paragraphs: for run in para.runs: run.font.name = font_name run.font.size = Pt(font_size) run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)

注意:同一个单元格在行列交错的大合并表格里可能被重复访问,这是python-docx的已知现象,不需要额外处理,重复设置同样格式没有副作用。

2.6 完整脚本拼装

把上面几段组合在一起,加上文件遍历和保存,就得到了一个可以实际使用的脚本。我贴一个简化版的完整代码,实际用的时候再按需改造:

import glob import os import time from docx import Document from docx.shared import Pt from docx.enum.text import WD_LINE_SPACING from docx.oxml.ns import qn def process_docx(file_path, output_path=None): doc = Document(file_path) # 1. 正文统一格式 for para in doc.paragraphs: if not para.text.strip(): continue pf = para.paragraph_format pf.line_spacing_rule = WD_LINE_SPACING.ONE_POINT_FIVE pf.space_before = Pt(0) pf.space_after = Pt(6) pf.first_line_indent = Pt(24) # 首行缩进2字符 for run in para.runs: run.font.name = '宋体' run.font.size = Pt(12) run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') # 2. 标题统一格式 for para in doc.paragraphs: style_name = para.style.name.lower() if style_name.startswith('heading 1') or style_name.startswith('heading 2'): for run in para.runs: run.font.name = '黑体' run.font.bold = True run.font.size = Pt(15) run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体') para.paragraph_format.space_before = Pt(12) para.paragraph_format.space_after = Pt(6) # 3. 表格内文字统一格式 for table in doc.tables: for row in table.rows: for cell in row.cells: for para in cell.paragraphs: for run in para.runs: run.font.name = '宋体' run.font.size = Pt(12) run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体') # 4. 保存 if output_path: doc.save(output_path) else: doc.save(file_path) return True def main(folder_path='./待处理文档'): docx_files = glob.glob(os.path.join(folder_path, "*.docx")) docx_files.sort() start_time = time.time() success_count = 0 for idx, file_path in enumerate(docx_files, 1): try: process_docx(file_path) success_count += 1 print(f"[{idx}/{len(docx_files)}] 处理成功: {os.path.basename(file_path)}") except Exception as e: print(f"[{idx}/{len(docx_files)}] 处理失败: {os.path.basename(file_path)},错误:{e}") elapsed = time.time() - start_time print(f"全部处理完成,成功 {success_count} 份,耗时 {elapsed:.2f} 秒") if __name__ == '__main__': main()

第一次跑的时候建议把输出路径改成新文件夹,不要直接覆盖原文件,万一格式调整得不符合要求还能回退。等确认没有问题,再改成原地覆盖或者另存为覆盖。

3. 从“能用”到“好用”:把脚本封装成通用批处理工具

3.1 命令行参数化改造

写死的脚本只能自己用,一旦要给别人用,或者不同批次的文档需要不同的参数,就得改代码,很麻烦。我建议把核心参数提取出来,用argparse做成命令行参数:

import argparse def parse_args(): parser = argparse.ArgumentParser(description='批量格式化Word文档') parser.add_argument('--folder', default='./待处理文档', help='文档所在文件夹路径') parser.add_argument('--font', default='宋体', help='正文字体') parser.add_argument('--size', type=int, default=12, help='正文字号(pt)') parser.add_argument('--line-spacing', type=float, default=1.5, help='行距倍数') parser.add_argument('--backup', action='store_true', help='是否备份原文件') return parser.parse_args()

这样改完之后,命令行执行就变成:

python word_formatter.py --folder "./周报汇总" --font "仿宋" --size 14 --line-spacing 1.25

好处是不同项目有不同格式规范时,不需要改动任何代码,只要换参数就行。

3.2 增加异常隔离和逐文件进度反馈

处理100份文档时最怕的是跑到中间某一份突然报错,整个程序崩溃。我的经验是每个文件用try/except包起来,单个文件失败不影响后续文件,并且把失败的文件记录下来,最后统一查看。上面2.6的main函数已经做了这个事,这里再补充一点:失败信息尽量记入日志文件,而不是只打印在控制台。控制台内容滚动后就看不到了,日志文件可以随时翻看。

with open('处理日志.txt', 'a', encoding='utf-8') as f: f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} 失败文件: {file_path}, 错误: {e}\n")

3.3 备份策略与输出目录管理

直接原地覆盖原文件有风险:脚本有bug、误判了某些段落、或者格式规范理解有偏差,100份文档全被改坏,那是大事故。我的做法是在处理前先建立一个backup文件夹,把原始文件复制过去:

import shutil backup_dir = os.path.join(folder_path, 'backup') os.makedirs(backup_dir, exist_ok=True) for file_path in docx_files: shutil.copy2(file_path, os.path.join(backup_dir, os.path.basename(file_path)))

另外也建议把处理后的文件输出到另一个目录,比如“已处理_格式化后”,这样原文件保持不动,对比效果也方便。确定满意后再手动替换。

3.4 做一个简单的图形界面给非技术同事用

有的人看到命令行就头疼。如果这个脚本要给完全不懂技术的同事用,可以套一个tkinter的壳子,做一个最简单的文件选择器和按钮。核心逻辑不复杂,就是多了一层界面封装:

import tkinter as tk from tkinter import filedialog, messagebox def select_folder(): folder_selected = filedialog.askdirectory() entry_folder.delete(0, tk.END) entry_folder.insert(0, folder_selected) def run(): folder = entry_folder.get() if not folder: messagebox.showwarning('提示', '请先选择文件夹') return try: main(folder) messagebox.showinfo('完成', '全部处理完成') except Exception as e: messagebox.showerror('出错', str(e))

界面最多放一个路径输入框、一个浏览按钮、一个开始按钮就够了。我当时给办公室同事做了这样一个工具,她们只需要选文件夹、点开始,完全不需要知道背后是Python。

4. 实测记录与常见问题排查实录

4.1 实际处理100份文档的耗时数据

拿我手上真实处理过的一批文档来举例:100份周报,平均每份15页左右,包含正文、一两个小表格、图片若干。脚本处理的总耗时是28.7秒,平均每份0.3秒不到。这个速度受磁盘性能影响不大,主要看CPU和文件大小。文件越大、表格越复杂,耗时会增加,但即使200份文档,脚本跑完也基本在一分钟以内。

对比之下,纯手动处理100份周报,就算每份只花2分钟,至少也要200分钟。用脚本节省了至少170分钟,而且操作全程不需要人盯着,喝杯水的功夫就完成了。

不要忽视这个时间差背后的意义。办公场景里的重复劳动,人做久了会疲劳、会出错,机器做不会。批量格式调整这类任务,天然适合自动化。

4.2 高频问题对照表

问题现象根本原因解决方案
中文变成宋体英文却是Calibri只设置了ASCII字体,没有设置东亚字体必须用qn('w:eastAsia')设置中文字体,run.font.name只管西文
首行缩进没有效果文档用的是“空格/全角空格”模拟缩进脚本里加正则把行首的全角空格删除,再设置first_line_indent参数
表格边框样式错乱表格有自己的样式系统,段落格式和表格样式独立不要动table.style,只遍历cell里的paragraph格式
处理后的文档用WPS打开乱码中文字体名设置方式不对确认字体名称是“宋体”“黑体”而不是“SimSun”“SimHei”,并保持eastAsia同步
报错“File is not a zip file”把.doc文件改后缀名成.docx,或文件本身已损坏先验证文件真实格式,用file命令或直接查看二进制文件头
所有段落都被误判成了标题判断标题的条件过于宽松收紧判断条件,加上字数限制、格局限制,避免匹配普通短段落
文档打开后图片全部丢失save方式有问题,或读写的对象不是同一个doc实例确保没有同时删除body下的元素,不要手动操作element树

4.3 中文环境的隐藏知识点:eastAsia字体设置

很多第一次用python-docx处理中文文档的人都会踩一个坑:设置了run.font.name = '宋体',也设置了size,结果打开文档一看,中文还是原来的字体,只有英文和数字变了。

原因在于Word的XML结构里,字体分为ascii、hAnsi、eastAsia三类。run.font.name只设置ascii和hAnsi字体,而中文字符走的是eastAsia通道。必须同时设置欧亚字体字段才行。我在上面代码里已经写了这段,再单独强调一下:

from docx.oxml.ns import qn run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')

这里rPr是run properties的缩写,rFonts是字体定义节点。如果直接操作rPr会报错,因为空白的rPr可能没有rFonts子节点,需要先判断或先用run.font.name赋值一次触发rPr创建。我实际测试中,先设置run.font.name再设置rFonts,顺序不会变。

4.4 遇到WPS编辑锁文件和只读文件的处理

如果文档正被WPS或Word打开着,python-docx读取时可能报权限错误。批量处理前最好让同事先关闭相关文档,或者在脚本里加一个检测逻辑,尝试以写模式打开失败时就跳过:

def is_file_locked(file_path): try: with open(file_path, 'a', encoding='utf-8'): return False except IOError: return True

只读文件同理,直接str替换文件名后缀保存到新位置,不要原地保存。

另外还要注意,Windows路径上的中文文件夹名不会导致编码错误,但终端输出时如果遇到GBK编码问题,可以在脚本开头加一句:

import sys sys.stdout.reconfigure(encoding='utf-8')

否则print中文日志会报UnicodeEncodeError。

4.5 处理前先做的三件小事

第一,先挑一份文档做试验。哪怕脚本写得再完美,也不建议直接全量运行,先拿一份有代表性的文档试跑,打开看效果对不对,确认标题、正文、表格都符合预期再全量跑。

第二,整理文档清单。把要处理的文档放到一个单独的文件夹里,不要和无关文件混在一起,避免误处理。

第三,检查文档格式统一性。如果文件夹里既有docx又有doc,脚本只会处理docx,导致结果不完整。先统一格式再跑脚本。

我给自己的流程是这样的:拿到任务 → 试跑1份 → 查看效果 → 备份原文件 → 全量处理 → 抽查最后几份 → 归档。这套流程很稳,从来没出过大问题。

4.6 一些只有实测才能发现的细节

有些坑是文档书上没有的,全靠实际操作碰出来的。

段落空行的问题:批量设置行距为1.5倍以后,原本空行的间距会变得很大。如果文档里空行很多,整体观感会很差。处理时可以考虑把纯空白的段落直接删除,或者把空行的字号设为最小号。我在脚本里加了一个判断:如果段落文本为空,就跳过格式设置,保持默认。

自动编号变乱的问题:Word的自动编号不是以普通文本形式存在的,它在XML里是numbering属性。如果强行改变了段落的样式,可能导致自动编号失效或者重启序号。遇到带自动编号的列表,尽量不要改变段落样式,只改字体和字号。

页眉页脚的处理:页眉页脚不在doc.paragraphs里,如果需要调整页眉页脚文字,需要单独遍历section.header。如果只是统一正文字体,不需要管页眉页脚。但要注意,修改Normal样式会影响页眉页脚默认字体,所以用逐段落方式比改样式更安全。

前后空格的问题:很多文档的标题行末尾带着一个多余的空格,批量转为黑体后,这个空格会影响对齐。建议在设置标题格式前先strip一下段落文字,但注意不要直接改paragraph.text,需要通过修改run.text来安全清理。

这些细节每一条都是实际踩出来的,不跑一遍根本不知道。

结束语

批量处理Word格式这个需求,本质上不是技术问题,而是劳动效率问题。脚本的价值不在于写代码本身有多高大上,而在于把重复、机械、容易出错的操作完全自动化,让时间和精力留在更有创造性的事情上。以我个人经验来说,现在碰到几十份甚至上百份的文档格式调整,第一反应就是打开终端跑一遍脚本,几十秒等结果出来,基本已经不太愿意手动处理了。如果你平时也经常被这类批量任务烦扰,不妨照着上面的代码搭一个自己的版本,从一份文档开始试,慢慢加上你的特殊需求。最后再提醒一句:第一次跑的时候,务必先备份,这是所有自动化脚本操作文档的底线。

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

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

立即咨询