尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python实现按户合并:数据自动填充到固定Word/Excel文档

Python实现按户合并:数据自动填充到固定Word/Excel文档 按户合并这组词放在办公自动化场景里通常指的是把明细数据按“户”维度归并后填入已经定好版式的固定文档一户生成一份文件。比如物业催缴单、客户一户一档、家庭信息登记表、供应商资料归档都属于这类需求。很多做数据或行政工作的人第一次接到这个任务时第一反应是复制粘贴但数据一多复制粘贴基本不可用尤其是一户多条明细、需要按户汇总、模板又完全不能乱动的情况纯手工操作非常容易漏。这篇文章要讲的就是怎么把“数据填充到固定文档”从手工操作变成脚本操作重点处理按户合并的逻辑。适合刚接触办公自动化、需要批量生成 Word 或 Excel 文档以及想把重复性工作沉淀成脚本的人。下面按实际跑过一轮的流程来拆。1. 先搞清楚按户合并到底要解决什么问题1.1 现实中哪些任务属于按户合并我接触最多的场景有三类物业催缴Excel 里有房号、业主姓名、欠费金额Word 模板是催缴通知书需要一户一张直接打印寄送。客户档案一张主表存客户信息一张联系人表存多个联系人最后要生成一户一档的档案文件。补贴名单公示村或社区的名单一个家庭多个人需要按户归并成员信息再填入固定的公示表。这些场景的共同点是数据是明细化的但文档以“户”为单位。如果不先按户合并直接把明细行填进单元格一个户会占好几行文档版面就会乱也会破坏固定文档的结构。1.2 人表和户表差在一行数据的含义明细表里每一行通常是一个人、一笔费用或一条记录。比如张三一家三口在家庭成员表里是三条记录每条记录有姓名、与户主关系、身份证号、手机号。但固定文档里可能只有“户主姓名”“家庭成员”“家庭人数”几个展示位所以必须先把三条记录揉成一行户级数据。这里的关键是按户合并不等于简单去重而是明确到底以哪个字段作为“户”的唯一标识。常见的有户号、客户编号、档案号、房间号。如果这个字段本身有重复或者有空值后续填充一定会出问题。1.3 三个关键词背后是四个规则数据填充、固定文档、按户合并这三个词拆开看分别对应不同的要求数据填充不是人工录入而是程序把数据写到模板里目的是减少手误保证每份文档格式一致。固定文档模板已经设计好可能带公章、页眉页脚、打印区域脚本只改数据区不能动整体结构。按户合并核心是按“户”分组把明细数据聚合到户再逐户生成文档。这里还要补一个容易被忽略的规则分组键怎么定、聚合规则是什么、模板字段和数据列怎么对应、输出文件名怎么生成这四个问题必须先想清楚。很多项目做了一半才发现字段对应不上返工成本特别高。2. 别急着写代码文档载体和方案选型要提前确认2.1 先看模板是 Word、Excel 还是 PDF实现方案不是一开始就定的先看固定文档的载体。Word 模板用 python-docx 处理比较常见可以替换占位符也可以改表格单元格。适合红头文件、通知书、档案登记表。Excel 模板用 openpyxl 处理更方便适合财务统计表、汇总表、复核表。PDF 模板不建议直接用脚本去改 PDF 页面布局。除非模板里自带表单字段否则排版复杂脚本很容易把位置改乱。如果业务方拿来的模板是 PDF我一般会先反问一句能不能提供生成这个 PDF 的 Word 原稿能拿到 Word 原稿后面省事很多。2.2 Word 自带邮件合并的限制在哪里很多人第一反应是 Word 自带的“邮件合并”。邮件合并适合数据简单、一张表正好对应一条记录的场景。但按户合并如果要处理“一户多个成员”邮件合并就需要用“目录”类型或者提前在 Excel 里把家庭成员拼接成一列。实际操作中很多人对 Excel 预处理不熟练家庭成员顺序不稳定、拼接分隔符不统一最后生成的文档还得人工检查。我不是说邮件合并完全不能用。如果数据量小、模板简单、使用人只熟悉 Office邮件合并够用。但如果数据来自多个表还要做分组、求和、关联脚本方案会更可控。脚本最大的优势是可重复执行改一次数据跑一遍就能重新生成全部文件。2.3 Python 环境准备与依赖确认我一般会准备 Python 3.8 以上版本核心依赖是 pandas、openpyxl、python-docx按需安装pip install pandas openpyxl python-docx需要特别注意openpyxl 只支持 .xlsx不支持 .xlspython-docx 支持 .docx不支持 .doc。如果你的模板还在用 .doc 或 .xls最好先统一转成 .docx 和 .xlsx。这一步看起来小实际很多人卡在这里。版本兼容也要留意。pandas 读取 Excel 时会调用 openpyxl 或 xlrd不同版本对旧格式支持不一样。如果读文件报错不要急着怀疑代码先确认文件扩展名和依赖版本是否匹配。3. 数据准备把明细表揉成户级表再谈填充3.1 分组键不能有歧义也不能有空值拿到数据后第一步不是写填充代码而是先做按户合并。我的流程是读入明细表检查分组键是否存在空值按分组键和展示顺序对明细排序聚合成员信息或金额字段输出户级表人工抽查几户。用 pandas 的 groupby 可以完成大部分工作。比如家庭成员明细表按户号分组把姓名和关系拼成成员清单同时计算家庭人数import pandas as pd df pd.read_excel( 家庭成员明细.xlsx, dtype{户号: str, 身份证号: str} ) df df.sort_values([户号, 成员序号]) grouped df.groupby(户号, as_indexFalse).agg( 户主姓名(姓名, first), 家庭成员(姓名, lambda x: 、.join(x)), 家庭人数(姓名, count) )这段代码里我用“成员序号”排序用“姓名”的第一个值作为户主姓名。实际项目中户主认定规则不一定这么简单有些表里“与户主关系”为“户主”的那一行才是户主。写代码前先确认清楚户主是固定取第一条还是按关系字段过滤。3.2 把家庭成员拼到一起聚合规则要有依据成员字段的拼接方式直接影响文档呈现效果。如果你希望家庭成员在文档里显示成多行不要在 pandas 里拼成长字符串而是考虑在 Word 表格里循环插入多行。如果只是展示用用顿号或换行拼接更合适。拼接时要注意排序。不能直接按 Excel 原始顺序最好有一个“成员序号”列。没有这个列就要按姓名、年龄、与户主关系等字段排序。比如默认户主排在第一位之后是配偶、子女、其他亲属。这个规则如果写错生成的文档顺序看起来就乱。除了成员信息按户合并还可能要聚合费用。比如一户多条欠费记录要汇总后填到催缴单里。这时候用 groupby 对金额字段做 sum但要注意金额精度float 累加可能出现 0.1 变 0.10000000000000003 的问题最好在聚合后统一 round 到两位小数。3.3 读取时顺手处理合并单元格和科学计数法Excel 表里经常有合并单元格尤其是表头或分组列。pandas 读进来时合并单元格的下方或右侧单元格会变成 NaN。比如“户号”列做了纵向合并第一行有值第二行是空。直接 groupby 会把第二行当成另一户导致户号缺失。处理方式有两种一是读入后对分组键做空值填充把上一行有效值往下填二是在生成 Excel 之前先把源数据里的合并单元格取消。我更推荐后者因为数据源标准化之后脚本逻辑会简单很多。如果无法修改源文件可以这样处理df[户号] df[户号].ffill()但要注意这种操作只适用于确认合并方向是“向下填充”的情况。如果表头也有合并单元格表头区域要单独处理不能直接对整列 ffill。长数字字段也要提前处理。身份证号、手机号、银行卡号在 Excel 里可能已经被显示成科学计数法。这种情况靠脚本救不回来最好是让业务人员提供文本格式的数字列或者在读取时强制指定 dtype。4. 填充到固定文档占位符和表格单元格两条路4.1 占位符替换法适合段落型模板这是最常用的一种方式。模板里放一个占位符比如致{{户主姓名}}{{房号}}生成文档时程序把占位符替换成真实数据。用 python-docx 读取段落遍历所有段落找到包含占位符的段落把整段文字替换成真实文本。这里有一个重要细节Word 会把一个段落拆成多个 run占位符可能被拆在两个 run 里。直接遍历 run 替换会删不干净。稳妥做法是先把段落的 run 文本合并再赋值给第一个 run最后清空其他 run。示例from docx import Document doc Document(模板.docx) for para in doc.paragraphs: if {{户主姓名}} not in para.text: continue full_text .join(run.text for run in para.runs) para.runs[0].text full_text.replace({{户主姓名}}, row[户主姓名]).replace({{房号}}, row[房号]) for run in para.runs[1:]: run.text 不过要注意合并 run 会破坏段落内部的字体、颜色等局部格式。如果占位符周围有复杂格式先拿一份样例测试看看替换后样式是否还能接受。4.2 表格单元格填充法适合表格式模板有些模板本身就是一张表格比如“入户信息表”户主、家庭住址、成员清单都放在表格单元格里。这时候可以用 python-docx 操作表格table doc.tables[0] table.cell(1, 0).text row[户主姓名] table.cell(1, 1).text row[家庭住址]需要注意Word 表格如果有合并单元格行列索引不完全是视觉上的行列。cell 坐标以文档结构为准而不是肉眼看到的行号列号。如果模板里合并了表头建议先写一段临时代码打印表格行数和列数再确定写入坐标。如果担心坐标漂移可以在模板里用“书签”定位单元格然后通过书签找到对应位置。这个方案更稳但需要做模板的人配合提前在 Word 里插入书签。4.3 先跑一户再跑批量验证标准要具体不要等全部生成完再检查。拿 1 户跑一遍确认以下几点文档能正常打开不是损坏文件。全文搜索不到“{{”说明占位符已经替换干净。中文不乱码日期、金额格式正确。表格里的行列没有错位。输出文件名与户号严格对应。我习惯写一个简单检查脚本遍历输出目录统计仍然包含占位符的文件数量。如果发现遗漏先调整替换逻辑再重新生成。5. 批量生成时文件命名、日志和失败重试怎么设计5.1 文件命名规则先定好避免覆盖和乱序批量生成最怕的是文件覆盖和找不到对应关系。命名规则建议用“户号_姓名_日期”。户号适合做唯一键因为不会重复。但户号是数字时要注意转成字符串后可能丢失前导零比如“001”变成“1”。读取时指定 dtypestr命名时才不会出错。还要避开 Windows 文件名的非法字符\ / : * ? |。姓名或地址里如果包含这些字符直接替换成下划线。路径也不要太长否则文件可能无法保存。5.2 输出目录和日志字段要能支撑回溯输出目录建议按日期分开output/ 20250101/ 001_张三.docx 002_李四.docx这样后续回查很方便。日志建议单独写一个 CSV记录每户的生成状态、耗时、错误信息。比如字段设计为字段说明house_id户号statussuccess 或 failedoutput_file输出文件名error异常信息create_time生成时间有了这个日志即使有一户失败也知道是哪一户不会因为报错把所有任务中断。5.3 失败重试先收集失败清单再单独处理我的习惯是不要在一个大循环里把几百户全部跑完而是先跑一小批用于验证文件命名、目录、格式。全部没问题之后再跑完整数据。如果中途有失败先把失败户号收集起来fail_list [] for house_id in house_ids: try: generate_one(house_id) except Exception as e: fail_list.append((house_id, str(e))) print(失败数量:, len(fail_list))然后针对 fail_list 单独重试。重试时先看日志里的异常类型再决定要不要改参数。如果是因为某个字段内容特殊导致程序报错全部重试大概率还会失败要先修正数据或代码。注意不要把模板文件放在输出目录里。模板只在读取时打开保存时写入输出目录否则一旦保存路径写错模板就被覆盖了。6. 最容易翻车的细节格式、空值和合并单元格6.1 金额、日期和长数字格式问题不能拖金额字段如果用 float 直接填充可能出现 33.300000000000004。处理方式是先 round再用格式化字符串保留两位小数。日期字段如果来自 Excelpandas 读出的是 Timestamp需要先转成字符串再填不然 Word 里会出现一串 UTC 时间戳。示例if pd.notna(row[入住日期]): date_str row[入住日期].strftime(%Y-%m-%d) else: date_str 身份证号、账号等长数字在源文件里就可能已经被转成科学计数法。读入时用 dtype 强制转成字符串能避免一部分问题但如果源文件已经损坏脚本救不回来。6.2 空值字段要分类型处理不能一刀切有人习惯用 fillna() 把所有空值都填成空字符串。这样做的风险是如果某个必填字段为空文档会留白但业务人员不一定看得出来。我建议分字段处理金额字段默认填 0 或空根据业务要求定。联系方式字段填“无”或“不详”。姓名等必填字段为空时把该户标记为异常不直接生成或者单独输出到一个待人工核对列表。在聚合之前还可以对必填字段做一次空值统计看看哪些户存在缺失提前反馈不要等到生成文档后才暴露。6.3 合并单元格的读取和写入都要小心如果输出的是 Excel 模板而且模板里有合并单元格openpyxl 写入合并区域时只能给左上角单元格赋值。如果试图给合并区域里的其他单元格赋值有的版本会报错有的版本会直接忽略。所以在写 Excel 模板之前先检查模板的 merged_cells 范围from openpyxl import load_workbook wb load_workbook(模板.xlsx) ws wb.active print(ws.merged_cells.ranges)先打印再写代码。这一步能省很多调试时间。读取一侧也是一样合并单元格的非左上角位置读出来是 None要对这些位置做特殊处理。7. 从跑通脚本到团队能用的工具还差这几步7.1 把路径、分组键、字段映射做成配置数据源路径、模板路径、输出目录、分组键、字段映射最好不要写死在代码里。最简单的方法是用一个 config.py 存字典config { input_file: data/20250101/家庭成员明细.xlsx, template_file: templates/入户信息表_v1.docx, output_dir: output/20250101, group_key: 户号, field_map: { 户主姓名: 户主姓名, 家庭住址: 家庭住址, } }业务人员换一批数据时只需要改配置不用进代码。等需求更复杂可以把配置放到 Excel 表或 yaml 文件里让非技术同事也能维护。7.2 大数量时先增量再考虑并发几万户的数据单线程跑可能要十几分钟甚至更久。如果只是临时任务耐心等待就行。如果要经常跑需要做增量处理已经生成过的户跳过。增量处理可以从输出目录中读取已有文件名与本次任务列表做差集。只处理还没生成过的户避免重复劳动。不要盲目开多线程。python-docx 和 openpyxl 对并发写同一个文件并不安全。如果资源够可以用多进程把任务列表切成几份每份独立进程去跑如果机器配置一般还是建议串行避免卡死。7.3 抽样核验和模板版本管理批量生成之后至少抽 5% 的样本人工核对。核对内容不只是内容对不对还包括页眉页脚是否还在、字体字号是否变化、表格是否错位。模板一旦改过生成结果可能全变所以模板文件也要做版本管理比如模板_v1.docx、模板_v2.docx。如果团队要长期用还要保留输入数据快照。后面如果发现某份文档错了可以回溯是哪一版数据、哪一版模板生成的。这个习惯能省掉大量扯皮。8. 排查链路填充结果不对时先查什么8.1 按现象、数据、模板、代码、依赖逐层排查遇到填充问题我先看现象再逐层缩小范围现象是报错还是文档生成成功但内容不对。输入数据分组键是否有空值字段名是否变化编码是否乱码。模板占位符是否与代码一致是否包含多余空格或换行。代码逻辑是否每户都重新打开模板还是不小心共用了同一个 Document 对象。依赖版本python-docx、openpyxl 版本是否兼容。如果文档能打开只有部分内容错误优先检查数据和字段映射。如果程序直接报错先看异常信息里的行号再往输入数据和模板方向查。8.2 所有文件内容都一样先查 Document 对象复用有个很常见的现象生成的所有文档都显示同一个人的名字。很多人会怀疑是数据读取错误实际很可能是循环里开了 Document但没有每次重新加载模板。错误示范# 错误doc 在循环外只加载一次 doc Document(模板.docx) for row in rows: doc.paragraphs[0].text row[姓名] doc.save(f{house_id}.docx)这样每次保存的都是同一个 doc最后所有文件内容都是最后一次循环的值。正确做法是把Document(模板.docx)放在循环里面每处理一户都重新加载for row in rows: doc Document(模板.docx) doc.paragraphs[0].text row[姓名] doc.save(f{house_id}.docx)这类问题用日志也能发现如果所有文件大小几乎一样内容也相同基本就是这个原因。8.3 每次改动后都回归一遍问题会少很多模板升级后先用一户数据回归测试不要直接跑全量。数据表结构变化后先检查字段名再跑全量。代码尽量拆成“读取数据”“按户合并”“填充文档”“保存输出”四个阶段这样排查问题时不用从头看到尾。按户合并这类任务真正难的不是写代码而是把数据口径、模板结构、字段格式提前梳理清楚。只要这三件套稳了脚本写起来很快这三件里有任何一件没确认批量生成的返工成本会非常高。
返回列表