资讯详情

资讯详情

Python 批量生成 Word 委托书:docx 模板占位符替换与校验

简介户口办理委托书是一份可直接套用的实用法律文书模板主要面向因工作、学习等原因无法亲自回户籍所在地办理手续的人群也适用于需要代他人处理户籍事务的读者。资源包共含1个doc文档文件类型为Word格式压缩包约15KB体积极小、下载后即可在常用办公软件中打开编辑。文档围绕委托关系与代理权限展开包含委托人与被委托人双方身份信息填写栏位明确授权被委托人代为办理户口迁移、信息更新、新生儿入户、婚姻状况变更等事项并声明委托人对被委托人签署文件的认可与法律责任承担。文中还预留了委托期限条款通常约定自签字之日起至事项办结为止同时提醒委托人亲笔签名并按红色手印、准确填写日期以增强文书的法律效力。已有94人学习参考适合需要规范拟定授权文书、规避代理风险的读者直接修改使用。1. 从一份《户口办理委托书.doc》说起为什么模板化生成比手改 Word 更靠谱行政岗每个月要替几十号人开同一份《户口办理委托书》字段只有委托人、受托人、身份证号、委托事项、日期这几项看着简单手改起来全是坑上一份的身份证号没换、两个受托人姓名串了行、日期还是去年的、签字栏被顶到第二页。一份改错就得重排版式人工核对几乎等于逐字重读一遍。工程化的做法是把这份 doc 当模板把可变字段抽成占位符用脚本从名单表里读数据、批量填充、批量导出最后再做一轮字段与格式自检。下面按「看懂 docx 结构 → 写模板替换 → 校验与导出 → 全链路验证」推一遍代码可以直接抄去改。2. doc 与 docx 的结构差异以及用 python-docx 定位委托书字段2.1 docx 是 OOXML 压缩包doc 是另一套二进制把.docx的后缀改成.zip直接解压会看到word/document.xml、word/styles.xml、word/header1.xml、word/footer1.xml、word/media/这些条目。正文、页眉页脚、图片各占一个 XML 部件格式信息全写在 XML 属性里所以 python-docx 这类库才能按段落和 run 去操作它。而老的.doc是私有二进制结构没有公开的节点树Python 侧基本没有可靠的直接解析方案。常见做法是先转格式再处理LibreOffice 的无头模式是跨平台里最省事的# 老式 .doc 转 docx输出到当前目录下的 out 文件夹 soffice --headless --convert-to docx --outdir ./out 户口办理委托书.doc # 版本较新的发行版命令名可能是 libreoffice参数完全一致 libreoffice --headless --convert-to docx --outdir ./out 户口办理委托书.doc参数含义--headless不启动图形界面适合服务器和 CI--convert-to docx指定目标格式冒号后面还能跟过滤器名--outdir决定输出目录不写就落在当前目录。批量转换时把多个文件名依次跟在后面即可--outdir之后不要混入其他参数。注意转换不是无损的。文本框、艺术字、域代码、分栏、页眉里的图片在转换后有可能移位或丢失。模板只转一次转完人工比对一页确认签字栏位置和表格边框没跑偏再进入下一步别在批量环节才发现版式崩了。2.2 按文档真实顺序遍历段落和表格python-docx 的doc.paragraphs只返回正文层的段落表格单元格里的段落拿不到doc.tables又完全不包含正文段落。委托书里的「委托人信息」「受托人信息」通常就是表格只走一条路必然漏字段。按 body 子元素的真实顺序遍历是两个都覆盖的写法from docx import Document from docx.table import Table from docx.text.paragraph import Paragraph from docx.oxml.ns import qn def iter_block_items(parent): 按 body 里的真实顺序产出段落和表格顺序和 Word 里看到的一致 for child in parent.element.body.iterchildren(): if child.tag qn(w:p): yield Paragraph(child, parent) elif child.tag qn(w:tbl): yield Table(child, parent) doc Document(户口办理委托书.docx) for i, block in enumerate(iter_block_items(doc)): if isinstance(block, Paragraph): print(f[P{i}] style{block.style.name!r} text{block.text!r}) else: print(f[T{i}] rows{len(block.rows)} cols{len(block.columns)}) for r in block.rows: print( |, | .join(c.text.strip() for c in r.cells))qn(w:p)把w:p前缀展开成完整的命名空间 URI直接拿字符串比较标签名会因为命名空间前缀不同而失败。这段输出来就是模板的字段地图哪一段是固定文案哪一个单元格是可变字段一目了然。2.2.1 用 run 拆分情况判断模板好不好改Word 会因为拼写检查、输入法、复制粘贴把一句连续的文字切成多个 run。{{委托人姓名}}很可能被切成{{委托人、姓名}}两段甚至三段这时对p.text做替换是白做的——p.text是只读拼接结果回写不到文档里。for p in doc.paragraphs: if {{ in p.text: print(repr(p.text)) for j, r in enumerate(p.runs): print( run, j, repr(r.text))如果占位符被切得很碎最省事的办法不是写复杂的合并逻辑而是回到模板里把占位符整段删掉、用纯键盘重新输入一遍再存盘。重新输入后通常就是一个 run后面的替换代码能少一半分支。这一步花五分钟比事后调 bug 划算。2.3 模板改造的三条命名约定模板改造阶段先定规矩后面写脚本才不会反复返工。约定做法原因定界符统一用半角{{key}}全角花括号在部分输入法下会被转成中文标点正则要额外兼容key 命名用拼音或英文如weituoren_nameCSV 表头用中文时Excel 另存容易带 BOM 和空格字段粒度一个占位符只放一个值不写{{姓名及身份证号}}拆开才能单独校验身份证号需要独立跑校验位固定文案「本人因故无法亲自办理特委托……」这类留在模板里不进数据字典。数据字典只装会变的东西名单表有几个字段字典就有几个 key。3. 用占位符加数据字典批量生成户口办理委托书3.1 跨 run 替换的实现与参数说明核心思路是先把整段所有 run 的文本拼起来做正则替换再把结果写回第一个 run其余 run 清空。这样能绕开 run 被切碎的问题同时保留第一个 run 的字体、字号、加粗等格式。import re from docx import Document # \w 覆盖字母数字下划线要支持中文 key 就换成 [\w\u4e00-\u9fa5] PLACEHOLDER re.compile(r\{\{\s*(\w)\s*\}\}) def fill_runs(paragraph, data): 整段合并后替换把结果写回首个 run full .join(run.text for run in paragraph.runs) if {{ not in full: return new_text PLACEHOLDER.sub( lambda m: str(data.get(m.group(1), m.group(0))), full) if not paragraph.runs: return paragraph.runs[0].text new_text for run in paragraph.runs[1:]: run.text data.get(key, m.group(0))这个默认值很关键字段在名单里缺失时原样保留{{key}}而不是替换成空字符串。生成完的文件里一眼就能看出哪个字段没喂上数据比默默留一片空白可靠得多。\s*允许{{ 委托人姓名 }}这种带空格的写法模板作者不用记格式。副作用要说清楚替换后整段统一成第一个 run 的格式。如果模板里占位符前后字体不一致结果会以第一个 run 为准。规避办法是让占位符独占一个段落或独占一个单元格别和固定文案混排在同一行。3.2 表格单元格和嵌套表格的填充正文段落和表格要分开走嵌套表格再递归一层def fill_tables(tables, data): for table in tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: fill_runs(p, data) fill_tables(cell.tables, data) # 嵌套表递归 def fill_doc(doc, data): for p in doc.paragraphs: fill_runs(p, data) for table in doc.tables: for row in table.rows: for cell in row.cells: for p in cell.paragraphs: fill_runs(p, data) fill_tables(cell.tables, data)cell.tables是这个单元格内部的嵌套表格集合委托书里如果用了「表格套表格」来画签字框不递归就会漏掉内层。另一个坑是合并单元格同一个_tc元素会被row.cells多次返回同一个占位符被替换多遍。替换本身是幂等的不会出错但如果要统计「一共替换了多少处」得用id(cell._tc)去重否则数字会虚高。3.3 批量生成的主循环import csv from io import BytesIO from pathlib import Path from docx import Document TPL Path(户口办理委托书.docx) OUT Path(out); OUT.mkdir(exist_okTrue) tpl_bytes TPL.read_bytes() # 模板读一次循环里反复复用 # utf-8-sig 兼容 Excel 另存的带 BOM 的 CSV with open(名单.csv, newline, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) for idx, row in enumerate(rows, 1): doc Document(BytesIO(tpl_bytes)) # 每份都从模板字节重新构造 fill_doc(doc, {k.strip(): (v or ).strip() for k, v in row.items()}) name row[weituoren_name].strip() doc.save(OUT / f{idx:03d}_{name}_户口办理委托书.docx)三个动作值得单独说。BytesIO(tpl_bytes)让每份文档都从干净的模板字节重新构造绝不能在外面建一个Document对象循环复用那样上一份的数据会残留到下一份。k.strip()和v.strip()处理表头尾随空格和单元格里的不可见字符Excel 导出几乎必带。文件名前缀补零序号一是保证目录排序与名单顺序一致二是同名委托人出现在两行时不会互相覆盖。3.4 中文字体、页边距这三个必调参数python-docx 的run.font.name只作用于西文中文走的是w:eastAsia属性不显式设置服务器上生成的文档会掉回默认字体from docx.oxml.ns import qn def set_run_font(run, westTimes New Roman, east宋体, size_ptNone): run.font.name west # 先赋值触发 rPr 节点创建 run._element.rPr.rFonts.set(qn(w:eastAsia), east) if size_pt: run.font.size Pt(size_pt)run.font.name那行不能省。rPrrun 属性节点在新建 run 上可能是None直接访问rFonts会抛异常先设一次西文字体把它创建出来再往里塞eastAsia。这个顺序反过来就报错。参数常见取值作用与踩坑点section.top_marginCm(2.54)上下边距太大会把签字栏挤到第二页改完要重新数页数run.font.sizePt(12)只对当前 run 生效整段统一要遍历所有 runw:eastAsia字体宋体 / 仿宋服务器没装对应字体时 PDF 会变方框装fonts-noto-cjk兜底4. 字段校验、签字栏与导出让生成结果能直接打印4.1 生成前的字段级校验替换之前先跑一遍校验不合格的行直接拦下来记日志不要让它生成出一份错误文件混进目录。字段校验规则不合格处理身份证号18 位前 17 位数字 校验位跳过该行并打印行号姓名非空长度 2–15无空格去空格后仍为空则拦截委托日期能被四种格式之一解析解析失败打印原值委托事项非空长度不超过 100超长提示可能撑破版式身份证校验位的实现不复杂加上它能挡住手抄错一位这类最常见的错误import re def check_id(id_no: str) - bool: id_no id_no.strip().upper() if not re.fullmatch(r\d{17}[\dX], id_no): return False weights [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] codes 10X98765432 total sum(int(c) * w for c, w in zip(id_no[:17], weights)) return codes[total % 11] id_no[-1]weights是加权因子codes是模 11 之后对应的校验字符表10X98765432的第 0 位到第 10 位。zip只取前 17 位最后一位单独比对。注意末位可能是字母 X所以先upper()再匹配[\dX]。4.2 日期规范化与委托事项截断名单表里的日期格式通常是五花八门的先归一再填模板from datetime import datetime def norm_date(s: str) - str: for fmt in (%Y-%m-%d, %Y.%m.%d, %Y/%m/%d, %Y年%m月%d日): try: d datetime.strptime(s.strip(), fmt) return f{d.year}年{d.month}月{d.day}日 except ValueError: continue raise ValueError(f无法识别的日期格式: {s!r})strptime逐个格式试命中就返回。%m和%d能自动接受1和01两种写法所以2024.1.5这类不带前导零的输入不用单独处理。抛出异常而不是返回空串是为了让调用方在批量循环里能精确定位到是哪一行数据有问题。委托事项如果来自自由文本输入填进去之前截断到 100 字并补省略号避免一整段话把签字栏顶到下一页。4.3 签字栏排版与「不跨页」控制签字栏最常见的崩法是跑到第二页去。三个动作组合起来基本能压住from docx.shared import Cm, Pt sec doc.sections[0] sec.bottom_margin Cm(2.0) tail doc.add_paragraph( 委托人签字____________ 受托人签字____________) tail.paragraph_format.space_before Pt(24) tail.paragraph_format.keep_with_next True tail.paragraph_format.keep_together Truekeep_together保证这一段自身不被拆到两页keep_with_next让它和下一段通常是日期行绑在一起。space_before用段前距拉开与正文的距离比插入若干空段落靠谱——空段落会随内容长度变化把版面顶乱而且计数页数时会误导。注意如果正文本身已经接近满页keep_together会把整段推到下一页结果反而多出一页空白。改完边距和段距后一定随机抽三份打开数页数不要只看第一份。4.4 导出 PDF 与 doc 兼容格式多数接收方要 PDF个别场景仍要老.doc。两种都用同一条命令模式# 批量转 PDF soffice --headless --convert-to pdf --outdir ./pdf ./out/*.docx # 需要老格式时转 doc soffice --headless --convert-to doc --outdir ./doc ./out/*.docx # 指定导出过滤器避免默认设置丢字体嵌入 soffice --headless --convert-to pdf:writer_pdf_Export --outdir ./pdf ./out/*.docx无头转换依赖系统已安装字体。服务器只装了西文字体时生成的 PDF 里中文会显示成方框判断依据是 PDF 体积明显偏小且文字无法搜索。装一套中文字体后重跑即可。另外转换是逐文件起进程几百份的量级建议分批跑或用脚本控制并发数一次性丢几千个文件进去容易超时。5. 进阶验证扫 XML 找残留占位符再对成品做断言比对前面所有替换都建立在doc.paragraphs和doc.tables之上而页眉、页脚、文本框里的内容不在这两条路径里。占位符只要被写进页眉替换就静默失效肉眼还很难发现。验证环节直接解包扫 XMLimport re, zipfile def scan_leftover(path, patternr\{\{\w\}\}): hits [] with zipfile.ZipFile(path) as z: for name in z.namelist(): if name.endswith(.xml) and (header in name or footer in name or name.startswith(word/document)): xml z.read(name).decode(utf-8) hits [(name, m.group(0)) for m in re.finditer(pattern, xml)] return hitszipfile直接读的是磁盘上的成品绕过 python-docx 的对象模型覆盖到页眉页脚这些边角部件。返回空列表才算干净。decode(utf-8)对 OOXML 部件是安全的所有部件都按 UTF-8 编码。再把成品读回来做值的存在性断言比逐份打开目测快得多from docx import Document def assert_filled(path, expect: dict): doc Document(path) text \n.join(p.text for p in doc.paragraphs) \n \n.join( c.text for t in doc.tables for r in t.rows for c in r.cells) for key, val in expect.items(): assert val in text, f{path.name} 缺少 {key}{val} assert {{ not in text, f{path.name} 存在未替换占位符最后补一道模板层面的保险把模板文件的哈希记下来模板一改就重跑一遍全量生成和断言。sha256sum 户口办理委托书.docx tpl.sha256模板哈希、当次名单快照和成品目录名一起归档。哪天有人拿着某份委托书问是哪一批印的比对一下哈希就能定位到当时用的是哪一版模板、哪一份名单不用去翻聊天记录猜。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →