查看: 243|回复: 3

Python批量合并Excel并生成Word定时任务脚本

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
Python 自动化办公最常见的入口,不是学完语法再找场景,而是把每周重复的表格合并、清洗、拆发、生成文档和发邮件串成一条可运行流水线。原文给出的判断标准很实用:规则是否固定、频次是否够高、出错代价是否够大。三条都满足就值得写脚本;自动化真正的价值除了省时间,还在于消除人工操作带来的随机错误。

【工具选型与能力边界】
办公自动化的常见需求可以拆成读取、处理、产出、调度四层。pandas 适合表格读取、清洗、合并和统计;openpyxl 适合读写 .xlsx 并控制单元格样式、列宽和公式;python-docx 适合基于模板生成 Word;pdfplumber 适合提取 PDF 中的文字和表格;pypdf 适合 PDF 拆分、合并和元信息处理;pathlib 与 shutil 负责文件整理;smtplib 与 email 负责邮件;schedule 适合单机常驻脚本的周期执行。

需要避免两个误区。第一,不要用 openpyxl 读取几万行大表做数据分析,它的设计目标是精细控制单元格,读大文件慢且吃内存;更合理的分工是 pandas 做数据处理,openpyxl 只负责最终美化。第二,pandas 不能替代 openpyxl 做复杂格式,报表要颜色、边框、列宽和冻结表头时,仍需 openpyxl 收尾。

能力边界也要清楚:扫描版 PDF 不接 OCR 无法稳定提取文字;没有开放接口的桌面客户端不适合用界面自动化强行操作;需要大量主观判断的任务应先保留人工环节。

【环境准备与目录约定】
原文建议 Python 3.9 及以上,具体以项目依赖兼容性为准。开发工具可用 PyCharm 社区版或 VS Code 加 Python 插件。依赖不要全局安装,在项目目录建虚拟环境更稳:
  1. python -m venv .venv
  2. source .venv/bin/activate
  3. python -m pip install --upgrade pip
  4. pip install pandas openpyxl python-docx pdfplumber pypdf schedule
复制代码

国内网络安装慢时可指定镜像源:
  1. pip install pandas openpyxl python-docx pdfplumber pypdf schedule -i https://pypi.tuna.tsinghua.edu.cn/simple
复制代码

建议目录按原始文件、输出、归档、模板、日志、脚本、配置分离。两个关键约定要长期坚持:原始文件只读,脚本只往 output 写,处理成功后再移动到 archive;所有路径集中放在 config.py,避免换机器时逐个脚本改路径。依赖锁定用 pip freeze > requirements.txt,部署时用 pip install -r requirements.txt 还原。

涉及公司数据时,脚本处理的文件必须是工作中合法有权访问的;个人信息、薪酬、合同等敏感数据本地处理完要清理临时文件;连接公司邮箱或业务系统前先获得授权,使用专门账号。任何会修改或删除原始文件的操作,都先在测试目录预演。

【Excel 合并:先把数据正确合到一起】
最核心的第一步不是做漂亮报表,而是确保多表能正确合并。原文示例把 data/raw 下多个门店销售表合并,统一保留列,清洗空行和脏数据,按部门汇总并输出两个 sheet。关键点包括:用 dtype={'门店': str} 防止 001 变成 1;过滤 Excel 打开时生成的 ~$ 临时文件;列名 strip 去空格;缺必需列时直接退出而不是带错误继续跑。
  1. from pathlib import Path
  2. import pandas as pd
  3. BASE_DIR = Path(__file__).resolve().parent.parent
  4. RAW_DIR = BASE_DIR / 'data' / 'raw'
  5. OUTPUT_DIR = BASE_DIR / 'data' / 'output'
  6. OUTPUT_FILE = OUTPUT_DIR / '销售汇总.xlsx'
  7. KEEP_COLUMNS = ['门店', '部门', '日期', '商品', '数量', '单价', '金额']
  8. def read_one(file_path: Path) -> pd.DataFrame:
  9.     df = pd.read_excel(file_path, dtype={'门店': str})
  10.     df = df.dropna(how='all').dropna(axis=1, how='all')
  11.     df.columns = [str(c).strip() for c in df.columns]
  12.     df['来源文件'] = file_path.name
  13.     return df
  14. def main() -> None:
  15.     OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
  16.     files = sorted(f for f in RAW_DIR.glob('*.xlsx') if not f.name.startswith('~$'))
  17.     if not files:
  18.         raise SystemExit(f'在 {RAW_DIR} 下没有找到任何 xlsx 文件,请检查路径')
  19.     frames = []
  20.     for f in files:
  21.         try:
  22.             frames.append(read_one(f))
  23.         except Exception as exc:
  24.             print(f'[跳过] {f.name} 读取失败:{exc}')
  25.     if not frames:
  26.         raise SystemExit('所有文件都读取失败,请检查表结构')
  27.     merged = pd.concat(frames, ignore_index=True)
  28.     missing = [c for c in KEEP_COLUMNS if c not in merged.columns]
  29.     if missing:
  30.         raise SystemExit(f'以下必需列缺失,请先统一表头:{missing}')
  31.     merged = merged[KEEP_COLUMNS].copy()
  32.     merged['门店'] = merged['门店'].astype(str).str.strip()
  33.     merged['部门'] = merged['部门'].astype(str).str.strip()
  34.     merged['日期'] = pd.to_datetime(merged['日期'], errors='coerce')
  35.     for col in ['数量', '单价', '金额']:
  36.         merged[col] = pd.to_numeric(merged[col], errors='coerce')
  37.     before = len(merged)
  38.     merged = merged.dropna(subset=['日期', '金额'], how='all')
  39.     print(f'清洗前 {before} 行,清洗后 {len(merged)} 行')
  40.     mask = merged['金额'].isna() & merged['数量'].notna() & merged['单价'].notna()
  41.     merged.loc[mask, '金额'] = merged.loc[mask, '数量'] * merged.loc[mask, '单价']
  42.     summary = (
  43.         merged.groupby('部门', as_index=False)
  44.         .agg(订单数=('金额', 'size'), 销售金额=('金额', 'sum'))
  45.         .sort_values('销售金额', ascending=False)
  46.     )
  47.     summary['销售金额'] = summary['销售金额'].round(2)
  48.     with pd.ExcelWriter(OUTPUT_FILE, engine='openpyxl') as writer:
  49.         merged.to_excel(writer, sheet_name='明细', index=False)
  50.         summary.to_excel(writer, sheet_name='部门汇总', index=False)
  51.     print(f'完成,输出文件:{OUTPUT_FILE}')
  52. if __name__ == '__main__':
  53.     main()
复制代码

运行命令是 python scripts/merge_excel.py。如果报必需列缺失,先打开报错文件看表头是否被合并单元格或标题行挤到第二行;这种情况可给 read_excel 加 skiprows=1 之类参数先跳过标题行。

【报表格式化与按部门拆分】
数据正确后再处理列宽、表头颜色、数字格式和冻结表头。freeze_panes='A2' 让滚动时表头固定,列宽按内容估算,金额列加千分位。格式化脚本读取销售汇总,对明细和部门汇总分别处理:
  1. from pathlib import Path
  2. from openpyxl import load_workbook
  3. from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
  4. from openpyxl.utils import get_column_letter
  5. BASE_DIR = Path(__file__).resolve().parent.parent
  6. REPORT = BASE_DIR / 'data' / 'output' / '销售汇总.xlsx'
  7. HEADER_FILL = PatternFill('solid', fgColor='1F4E78')
  8. HEADER_FONT = Font(color='FFFFFF', bold=True, size=11)
  9. THIN = Side(style='thin', color='BFBFBF')
  10. BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
  11. def style_sheet(ws, money_cols=()) -> None:
  12.     for cell in ws[1]:
  13.         cell.fill = HEADER_FILL
  14.         cell.font = HEADER_FONT
  15.         cell.alignment = Alignment(horizontal='center', vertical='center')
  16.         cell.border = BORDER
  17.     for idx, column in enumerate(ws.columns, start=1):
  18.         max_len = max(len(str(c.value)) if c.value is not None else 0 for c in column)
  19.         ws.column_dimensions[get_column_letter(idx)].width = min(max_len * 2 + 4, 40)
  20.     headers = {c.value: c.column for c in ws[1]}
  21.     for name in money_cols:
  22.         if name in headers:
  23.             col = headers[name]
  24.             for row in range(2, ws.max_row + 1):
  25.                 ws.cell(row=row, column=col).number_format = '#,##0.00'
  26.     ws.freeze_panes = 'A2'
  27. def main() -> None:
  28.     wb = load_workbook(REPORT)
  29.     style_sheet(wb['明细'], money_cols=('金额', '单价'))
  30.     style_sheet(wb['汇总'] if '汇总' in wb.sheetnames else wb['部门汇总'], money_cols=('销售金额',))
  31.     wb.save(REPORT)
  32.     print(f'格式已应用到:{REPORT}')
  33. if __name__ == '__main__':
  34.     main()
复制代码

按部门拆分时用 groupby 循环生成单独文件,空部门跳过。真实数据里部门名可能包含斜杠,文件名中要替换掉,否则会被文件系统当成路径分隔符导致报错。
  1. from pathlib import Path
  2. import pandas as pd
  3. BASE_DIR = Path(__file__).resolve().parent.parent
  4. SOURCE = BASE_DIR / 'data' / 'output' / '销售汇总.xlsx'
  5. SPLIT_DIR = BASE_DIR / 'data' / 'output' / '按部门拆分'
  6. def main() -> None:
  7.     SPLIT_DIR.mkdir(parents=True, exist_ok=True)
  8.     detail = pd.read_excel(SOURCE, sheet_name='明细')
  9.     for dept, group in detail.groupby('部门'):
  10.         if pd.isna(dept) or str(dept).strip() == '':
  11.             continue
  12.         safe_name = str(dept).replace('/', '_').strip()
  13.         target = SPLIT_DIR / f'{safe_name}_销售明细.xlsx'
  14.         group.to_excel(target, index=False)
  15.         print(f'已生成:{target.name}({len(group)} 行)')
  16. if __name__ == '__main__':
  17.     main()
复制代码

【Word 模板批量生成】
Word 批量生成不建议从零拼排版,更省事的是先做模板,把变化内容写成 {{姓名}}、{{部门}}、{{金额}} 这类占位符,再用 python-docx 替换。替换时要注意保留原有格式:先把段落内多个 run 的文本合并,完成替换后写回第一个 run,其余 run 清空,避免格式错乱。原文给出的段落替换函数如下:
  1. def replace_in_paragraph(paragraph, mapping: dict) -> None:
  2.     full_text = ''.join(run.text for run in paragraph.runs)
  3.     replaced = full_text
  4.     for key, value in mapping.items():
  5.         replaced = replaced.replace(f'{{{{{key}}}}}', str(value))
  6.     if replaced == full_text:
  7.         return
  8.     paragraph.runs[0].text = replaced
  9.     for run in paragraph.runs[1:]:
  10.         run.text = ''
复制代码

主流程读取人员名单,逐行构造 mapping,打开模板文档,遍历 doc.paragraphs 调用替换函数;表格内的单元格段落也用同一函数处理。这样格式由 Word 模板保证,脚本只负责填内容。

【文件整理、邮件与定时任务】
原文将文件重命名归档、带附件邮件和 schedule 定时执行放在同一条流水线中。实现上可用 pathlib 和 shutil 做重命名与分类归档,用 smtplib 与 email 构造带附件邮件,用 schedule 做单机常驻脚本的周期执行。这里要强调工程习惯:任何批量改名或删除操作先预演再执行;邮件发送使用授权账号并控制收件范围;定时任务要写日志,失败时能定位到具体文件和步骤。

【排查思路与工程建议】
常见问题包括:~$ 临时文件导致读取失败;门店编号被识别成数字;列名前后空格导致合并后列对不上;表头被合并单元格挤到第二行;部门名含斜杠导致文件名报错;空行和备注行混入数据。排查时先看报错文件的实际表头,再看脚本的列名约定和路径配置。

工程化上建议配置与代码分离,日志要写清输入文件、行数变化、输出路径和异常原因;脚本要尽量幂等和可重跑,输出目录可覆盖,原始文件只读并归档;依赖锁定版本,权限最小化,数据边界清晰。做到这些,脚本才从一次性玩具变成可长期维护的办公工具。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python批量合并Excel并生成Word定时任务脚本

这个流程拆得很清楚,尤其是“原始文件只读、脚本只写 output、成功后再归档”这条约定,我之前就是没坚持,结果有次脚本跑一半报错,原始表被覆盖了一部分,只能从备份恢复。现在我也按原始/输出/归档分目录,路径全收进 config,换机器确实省心很多。 几个细节特别有共鸣:dtype 指定门店为字符串防止 001 变 1 这个坑太真实了;过滤 ~$ 临时文件也是必须的,不然 pandas 读到一半报错;列名 strip 和缺列直接退出比带错误继续跑靠谱,宁可早失败也别产出脏数据。 另外 pandas 做数据处理、openpyxl 只负责最终格式这个分工我完全认同,之前用 openpyxl 读几万行大表,慢到以为程序卡死了。期待看到后面 Word 模板生成和 schedule 定时那部分,尤其是任务异常了怎么通知、日志怎么落地,这块想参考一下你的做法。
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python批量合并Excel并生成Word定时任务脚本

楼主这篇总结得很实在,尤其是“规则固定、频次高、出错代价大”这三条判断标准,比单纯讲技术点更有指导意义。很多人学自动化容易陷入为了写脚本而写脚本,结果维护成本比手动还高。 工具分工那块也说到了点子上。pandas 做数据处理、openpyxl 只管最后美化,这个边界划清楚能少踩很多坑。我见过不少同事用 openpyxl 一行行读大表,跑到内存爆掉才回头找原因。 代码里几个细节处理得很到位:dtype 指定门店为字符串防止 001 变 1、过滤 ~$ 临时文件、列名 strip、缺必需列直接退出。这些看起来是小地方,但实际跑批量任务时恰恰是这些地方最容易出问题。尤其是“缺列直接退出而不是带错误继续跑”,能避免生成一份看似正常实则残缺的汇总表。 另外想补充一点实际经验:合并前最好先做一次表头一致性检查,把所有文件的列名收集起来对比,比合并后才发现缺列要省事。还有日期列格式不统一的情况很常见,有的文件是文本有的文件是日期,pd.to_datetime 加 errors=‘coerce’ 后最好再统计一下转换失败的行数,避免脏数据被静默丢掉。 目录约定里“原始文件只读、只往 output 写、成功后再 archive”这条建议值得贴在工位上。定时任务部分虽然帖子后面截断了,但 schedule 确实适合单机常驻场景,如果是多人共用或者需要失败重试和告警,可能还得考虑更重一点的调度方案。整体
回复 支持 反对

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: Python批量合并Excel并生成Word定时任务脚本

这个帖子的思路很实在,尤其是“规则固定、频次高、出错代价大”这三个判断标准,确实比单纯堆技术点更有指导意义。很多教程一上来就讲库怎么用,但真到工作里,最难的是判断哪些流程值得自动化、哪些应该保留人工确认,楼主把这条线划得比较清楚。 工具分工那段也说到点子上了。pandas 和 openpyxl 的边界很容易被搞混,实际用起来就是 pandas 负责算、openpyxl 负责好看,硬拿 openpyxl 去读大表或者用 pandas 硬做复杂格式,最后都是自己给自己找麻烦。还有 `dtype={'门店': str}` 这种细节,看着不起眼,但真碰上门店编号带前导零的数据,不设置就等着报表对不上吧。 目录约定和原始文件只读这两条我觉得比代码本身还重要。脚本可以慢慢改,但数据被写坏了就麻烦了。把路径统一收到 config.py 里,后面换机器或者换目录确实省心很多,这个习惯值得从一开始就坚持。 另外想补充一点,定时任务如果跑在 Windows 上,schedule 单机常驻脚本要注意别让机器休眠或者用户注销把进程杀了,日志也要写清楚每次跑了哪些文件、跳过哪些、耗时多久,不然后面排查问题只能靠猜。合并 Excel 那部分先校验列、缺列直接退出而不是硬跑,也是对的,宁可失败得明显,也不要悄悄产出一份错数据。 整体看下来,这个帖子更像是一份能直接落地的工作流清单,不是只教语法,适合拿来对照自己
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-14 07:51 , Processed in 0.024765 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部