
只要满足:
python --version,显示版本号即成功。在命令提示符中执行:
pip install pywin32等待提示“Successfully installed”即可。
将文末的完整代码复制,粘贴到记事本中,保存为 word_batch_replace.py(文件名可以自定,但后缀必须是 .py)。
💡 建议保存在一个你容易找到的文件夹,比如
D:\我的工具\。
双击 word_batch_replace.py,会弹出一个窗口,这就是主界面。

在“替换规则”区域,每行一条规则,格式为:
查找内容 → 替换为
示例:
考评时间:2026年7月 → 考评时间:2026年8月考评时间:*月 → 考评时间:2026年8月可以点击“添加规则”增加多行,点击“删除最后一行”移除不需要的规则,每行右侧的 ✕ 可单独删除该行。
点击 “预览替换” 按钮,工具会打开源文件夹中的第一个文档,统计每条规则匹配到的数量,并显示一个匹配示例。如果匹配数量为 0,说明你的查找文本与文档内容不一致,请检查空格、标点符号(中英文冒号)等。
点击 “开始处理”,工具会在后台启动Word,逐文档执行替换。界面上的进度条和日志窗口会实时反馈处理情况。如果中途想停止,点击 “取消” 按钮即可(会等待当前文档处理完再退出)。
处理完成后,弹窗显示成功/失败数量。请打开目标文件夹(或原文件,如果覆盖模式)检查替换效果。
通配符是解决“空格不固定”“年月格式杂乱”的神器。以下是最常用的几个符号:
* | 考评时间:*月 | |
? | 202?年 | |
[ ] | [2026] | |
[ - ] | [0-9] | |
@ | [ ]@ | |
{n} | [ ]{2} | |
{n,} | [ ]{2,} | |
^13 |
考评时间:*月 | ||
2026[ ]@年[ ]@5[ ]@月 | [ ]@表示一个或多个 | |
7[ ]@月 | ||
*^13 |
[ ],全角空格写 [ ](方括号内放一个中文空格)。如果不确定,直接从文档中复制一个空格粘贴到方括号内。* 会匹配到该段落中最后一个“月”,如果段落中有多个“月”(比如“5月”和“6月”),可能会匹配过多。若只需匹配第一个“月”,可使用 [!^13]@ 匹配非回车字符,或改用更精确的日期规则(如 2026[ ]@年[ ]@5[ ]@月)。ModuleNotFoundError: No module named 'win32com' | pip install pywin32 | |
* | * |
将以下代码保存为 word_batch_replace.py,双击运行。
# -*- coding: utf-8 -*-"""Word 批量替换工具(最终稳定版 · 路径彻底修复)功能:- 单例 Word 进程,后台运行- 多线程处理,界面不卡死- 安全替换(避免无限循环)- 支持通配符- 覆盖 / 另存为 模式- 失败不写盘- 实时日志 + 进度条- 自动修复路径格式(解决微云同步盘路径问题)"""import osimport timeimport tkinter as tkfrom tkinter import filedialog, messagebox, ttkimport threadingimport win32com.client as win32classWordBatchReplacer:def__init__(self, root): self.root = root self.root.title("Word 批量替换(最终版)") self.root.geometry("760x700")# 控制变量 self.src_var = tk.StringVar() self.dst_var = tk.StringVar() self.overwrite_var = tk.IntVar(value=0) self.wildcard_var = tk.IntVar(value=0) self.rule_entries =[] self.is_running =False self.cancel_flag =False# ---------- 界面 ---------- frame_dir = tk.LabelFrame(root, text="文件夹设置") frame_dir.pack(fill="x", padx=10, pady=5) tk.Label(frame_dir, text="源文件夹:").grid(row=0, column=0, sticky="w") tk.Entry(frame_dir, textvariable=self.src_var, width=50).grid(row=0, column=1, padx=5) tk.Button(frame_dir, text="浏览", command=self.browse_src).grid(row=0, column=2) tk.Label(frame_dir, text="目标文件夹:").grid(row=1, column=0, sticky="w", pady=5) tk.Entry(frame_dir, textvariable=self.dst_var, width=50).grid(row=1, column=1, padx=5) tk.Button(frame_dir, text="浏览", command=self.browse_dst).grid(row=1, column=2) tk.Checkbutton(frame_dir, text="直接覆盖原文件(勾选后目标文件夹无效)", variable=self.overwrite_var).grid(row=2, column=0, columnspan=3, sticky="w", pady=5) tk.Checkbutton(frame_dir, text="启用通配符匹配", variable=self.wildcard_var).grid(row=3, column=0, columnspan=3, sticky="w") tk.Label(frame_dir, text="示例(通配符):考评时间:*月 或 7[ ]@月", fg="gray", font=("Arial",8)).grid(row=4, column=0, columnspan=3, sticky="w")# 规则列表 frame_rules = tk.LabelFrame(root, text="替换规则(每行一组)") frame_rules.pack(fill="both", expand=True, padx=10, pady=5) canvas = tk.Canvas(frame_rules) scrollbar = ttk.Scrollbar(frame_rules, orient="vertical", command=canvas.yview) self.scrollable_frame = ttk.Frame(canvas) self.scrollable_frame.bind("<Configure>",lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0,0), window=self.scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") self.add_rule_row()# 按钮 frame_btns = tk.Frame(root) frame_btns.pack(fill="x", padx=10, pady=5) tk.Button(frame_btns, text="添加规则", command=self.add_rule_row).pack(side="left", padx=5) tk.Button(frame_btns, text="删除最后一行", command=self.remove_last_rule).pack(side="left", padx=5) tk.Button(frame_btns, text="预览替换", command=self.preview_replace, bg="lightyellow").pack(side="left", padx=5) tk.Button(frame_btns, text="开始处理", command=self.start_thread, bg="lightgreen").pack(side="left", padx=5) tk.Button(frame_btns, text="取消", command=self.cancel_processing, bg="lightcoral").pack(side="left", padx=5)# 状态和进度 self.status_var = tk.StringVar(value="就绪") tk.Label(root, textvariable=self.status_var, relief="sunken", anchor="w").pack(fill="x", padx=10, pady=2) self.progress = ttk.Progressbar(root, orient="horizontal", length=400, mode="determinate") self.progress.pack(fill="x", padx=10, pady=5)# 日志 self.log_text = tk.Text(root, height=10, state='disabled', bg='#f0f0f0') self.log_text.pack(fill='x', padx=10, pady=5)# ---------- 辅助方法 ----------deflog(self, msg): self.log_text.config(state='normal') self.log_text.insert('end', msg +'\n') self.log_text.see('end') self.log_text.config(state='disabled') self.root.update_idletasks()defset_status(self, msg): self.status_var.set(msg) self.root.update_idletasks()defbrowse_src(self): folder = filedialog.askdirectory(title="选择源文件夹")if folder: self.src_var.set(os.path.normpath(folder))defbrowse_dst(self): folder = filedialog.askdirectory(title="选择目标文件夹")if folder: self.dst_var.set(os.path.normpath(folder))defadd_rule_row(self, old="", new=""): frame = tk.Frame(self.scrollable_frame) frame.pack(fill="x", pady=2) old_var = tk.StringVar(value=old) new_var = tk.StringVar(value=new) tk.Entry(frame, textvariable=old_var, width=30).pack(side="left", padx=2) tk.Label(frame, text=" → ").pack(side="left") tk.Entry(frame, textvariable=new_var, width=30).pack(side="left", padx=2) tk.Button(frame, text="✕", command=lambda f=frame: self.delete_row(f)).pack(side="left", padx=5) self.rule_entries.append((old_var, new_var, frame))defdelete_row(self, frame):iflen(self.rule_entries)<=1: messagebox.showwarning("警告","至少保留一行规则。")returnfor i,(_, _, f)inenumerate(self.rule_entries):if f == frame: f.destroy()del self.rule_entries[i]breakdefremove_last_rule(self):iflen(self.rule_entries)<=1: messagebox.showwarning("警告","至少保留一行规则。")return self.rule_entries.pop()[2].destroy()defget_rules(self):return[(old.get().strip(), new.get())for old, new, _ in self.rule_entries if old.get().strip()]# ---------- 预览 ----------defpreview_replace(self): src_dir = os.path.normpath(self.src_var.get().strip())ifnot src_dir: messagebox.showerror("错误","请先选择源文件夹")return rules = self.get_rules()ifnot rules: messagebox.showerror("错误","请定义至少一条规则")return use_wildcard = self.wildcard_var.get()==1 first_doc =Nonefor root, _, files in os.walk(src_dir):for f in files:if f.lower().endswith(('.doc','.docx')): first_doc = os.path.normpath(os.path.join(root, f))breakif first_doc:breakifnot first_doc: messagebox.showinfo("提示","未找到Word文档")returntry: word = win32.Dispatch("Word.Application") word.Visible =False word.DisplayAlerts =False doc = word.Documents.Open(first_doc) preview_text =[]for old, new in rules: rng = doc.Range() find = rng.Find find.ClearFormatting() find.Text = old find.Forward =True find.Wrap =0 find.MatchWildcards = use_wildcard count =0 sample =""while find.Execute()and count <3: count +=1ifnot sample: ctx = find.Parent.Text.replace('\r',' ').replace('\n',' ')[:50] sample = ctx rng.Start = find.Parent.End find = rng.Find find.ClearFormatting() find.Text = old find.Forward =True find.Wrap =0 find.MatchWildcards = use_wildcard preview_text.append(f"规则 '{old}' → 匹配 {count} 处"+(f",示例: {sample}"if count else"")) doc.Close(False) word.Quit() messagebox.showinfo("预览结果","\n\n".join(preview_text))except Exception as e: messagebox.showerror("预览失败",str(e))# ---------- 核心安全替换(避免无限循环) ----------defprocess_document(self, word_app, src_path, dst_path, rules, use_wildcard): doc =Nonetry:# 双重标准化 src_path = os.path.normpath(src_path) dst_path = os.path.normpath(dst_path) doc = word_app.Documents.Open(src_path) total_replaced =0for old, new in rules: replaced =0 rng = doc.Range() find = rng.Find find.ClearFormatting() find.Text = old find.Forward =True find.Wrap =0# 关键:不循环 find.MatchWildcards = use_wildcardwhile find.Execute():# 直接替换匹配到的文本 find.Parent.Text = new replaced +=1# 移动范围到替换位置之后继续搜索 rng.Start = find.Parent.End find = rng.Find find.ClearFormatting() find.Text = old find.Forward =True find.Wrap =0 find.MatchWildcards = use_wildcard self.log(f" 规则 '{old}' → 替换了 {replaced} 处") total_replaced += replaced# 保存if dst_path == src_path: doc.Save()else: os.makedirs(os.path.dirname(dst_path), exist_ok=True) doc.SaveAs(dst_path) doc.Close() self.log(f" 总共替换 {total_replaced} 处")returnTrueexcept Exception as e: self.log(f" ❌ 错误:{str(e)}")if doc:try: doc.Close(SaveChanges=False)except:passreturnFalse# ---------- 批量处理 ----------defbatch_process(self):# 关键修复:在开始时立即标准化路径 src_dir = os.path.normpath(self.src_var.get().strip())ifnot src_dir: self.log("❌ 请选择源文件夹")return overwrite = self.overwrite_var.get()==1 dst_dir = os.path.normpath(self.dst_var.get().strip())ifnot overwrite else""ifnot overwrite andnot dst_dir: self.log("❌ 请选择目标文件夹或勾选覆盖")return rules = self.get_rules()ifnot rules: self.log("❌ 请至少添加一条规则")return use_wildcard = self.wildcard_var.get()==1 word_files =[]for root, _, files in os.walk(src_dir):for f in files:if f.lower().endswith(('.doc','.docx')): word_files.append(os.path.normpath(os.path.join(root, f)))ifnot word_files: self.log("❌ 未找到Word文档")return total =len(word_files) self.progress["maximum"]= total self.progress["value"]=0 self.log(f"🚀 启动Word(后台),共 {total} 个文档") self.set_status("正在启动 Word...") word_app =Nonetry: word_app = win32.Dispatch("Word.Application") word_app.Visible =False word_app.DisplayAlerts =False word_app.Options.CheckGrammarWithSpelling =False word_app.Options.CheckSpellingAsYouType =False word_app.Options.CheckGrammarAsYouType =Falseexcept Exception as e: self.log(f"❌ 启动失败:{e}")return success =0 fail =0try:for idx, src_path inenumerate(word_files,1):if self.cancel_flag: self.log("⏹ 用户取消")break self.set_status(f"处理 [{idx}/{total}] {os.path.basename(src_path)}") self.log(f"\n[{idx}/{total}] {os.path.basename(src_path)}")# 目标路径也标准化if overwrite: dst_path = src_pathelse: dst_path = os.path.normpath(os.path.join(dst_dir, os.path.relpath(src_path, src_dir))) self.log(f" 源文件: {src_path}") self.log(f" 目标文件: {dst_path}") ok =Falsefor attempt inrange(3):# 重试3次 ok = self.process_document(word_app, src_path, dst_path, rules, use_wildcard)if ok:break time.sleep(0.5)if ok: success +=1 self.log(" ✅ 成功")else: fail +=1 self.log(" ❌ 失败")ifnot overwrite and os.path.exists(dst_path):try: os.remove(dst_path)except:pass self.progress["value"]= idx time.sleep(0.1)finally:try: word_app.Quit(SaveChanges=False)except:pass self.is_running =False self.cancel_flag =False self.set_status("处理完成") self.log(f"\n✅ 全部完成:成功 {success},失败 {fail}") messagebox.showinfo("完成",f"处理完成!\n成功:{success}\n失败:{fail}")defstart_thread(self):if self.is_running: messagebox.showwarning("提示","正在处理中")return self.is_running =True self.cancel_flag =False self.log_text.config(state='normal') self.log_text.delete(1.0,'end') self.log_text.config(state='disabled') self.progress["value"]=0 threading.Thread(target=self.batch_process, daemon=True).start()defcancel_processing(self):if self.is_running: self.cancel_flag =True self.log("⏹ 正在取消...")else: messagebox.showinfo("提示","没有正在进行的任务")if __name__ =="__main__": root = tk.Tk() app = WordBatchReplacer(root) root.mainloop()有了这款工具,再多的Word文档修改任务也能轻松应对。它不仅能帮你节省大量时间,还能避免手动操作带来的错漏。代码完全开源,可根据需要自由修改。如果你觉得有用,欢迎点赞、收藏、转发,让更多朋友告别重复劳动!
关注我,获取更多办公自动化干货! 🔥