简介:户口办理委托书是一份可直接套用的实用法律文书模板,主要面向因工作、学习等原因无法亲自回户籍所在地办理手续的人群,也适用于需要代他人处理户籍事务的读者。资源包共含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_name | CSV 表头用中文时,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_ok=True) tpl_bytes = TPL.read_bytes() # 模板读一次,循环里反复复用 # utf-8-sig 兼容 Excel 另存的带 BOM 的 CSV with open('名单.csv', newline='', encoding='utf-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, west='Times New Roman', east='宋体', size_pt=None): 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那行不能省。rPr(run 属性节点)在新建 run 上可能是None,直接访问rFonts会抛异常,先设一次西文字体把它创建出来,再往里塞eastAsia。这个顺序反过来就报错。
| 参数 | 常见取值 | 作用与踩坑点 |
|---|---|---|
section.top_margin | Cm(2.54) | 上下边距太大会把签字栏挤到第二页,改完要重新数页数 |
run.font.size | Pt(12) | 只对当前 run 生效,整段统一要遍历所有 run |
w: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之上,而页眉、页脚、文本框里的内容不在这两条路径里。占位符只要被写进页眉,替换就静默失效,肉眼还很难发现。验证环节直接解包扫 XML:
import re, zipfile def scan_leftover(path, pattern=r'\{\{\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模板哈希、当次名单快照和成品目录名一起归档。哪天有人拿着某份委托书问是哪一批印的,比对一下哈希,就能定位到当时用的是哪一版模板、哪一份名单,不用去翻聊天记录猜。
本文还有配套的精品资源,点击获取