ARTICLE · 1180101
python办公自动化:六份表、六份文档、三张发票,一个脚本搞定
办公自动化:六份表、六份文档、三张发票,一个脚本搞定
每个月末,你要干三件事。
六个区域交来的销售表,合并成一张总表;按区域生成六份对账单,发给各区域经理签字;报上去的发票,并成一份 PDF 附件。
每件事都不难,就是重复。一页页复制粘贴、一份份改文件名,一下午就没了。
这三件事,Python 都能替你干。而且用的是三个很小的库,每一个的核心用法都不超过五行。
| openpyxl | ||
| python-docx | ||
| pypdf |
注意中间那个:装的时候叫 python-docx,代码里却要写 import docx。 名字不一样,第一次用很容易卡在这儿。

01 这些文件,其实不是黑盒
在动手之前,先看一件可能会改变你想法的事。
一个 .xlsx 文件,用压缩包的方式打开,里面是这样的:
你天天点的 Excel,就是把这些 XML 渲染成格子的一个界面。 文件本身没有任何"只能用手点"的地方。
.docx 也一样,同样是压缩包加一堆 XML。知道这一点,后面的事就顺理成章了——读文件就是读里面的数据,写文件就是拼出这些数据再存回去。

02 Excel · 读:六份表合成一张

先看看手上有什么。
六份,结构一样。打开其中一份看看:
三行代码对应三个动作:load_workbook() 打开文件,.active 拿到当前那张工作表,ws[1] 拿第一行。
ws[1] 和 ws["A1"] 两种写法都行,前者给整行,后者给单个格子。
要批量处理,就得一行行过,用 iter_rows():
min_row=2 跳过表头。values_only=True 是个好习惯——不加它,拿到的是单元格对象,取值还要补一句 .value。
现在把六份合起来:
(f.stem,) + row 那个逗号别漏了——它把区域名和原来的六列拼成七列,区域名在前,来源一目了然。同一件事手工做要复制粘贴 180 次。
一个不看代码看不出来的坑
你"看一眼"某个单元格,openpyxl 就会把它创建出来。
这份表本来只有 31 行,因为我访问了一次 Z99,max_row 就变成了 99——中间那 68 行是空的。
为什么?openpyxl 打开文件时不会把所有格子都建出来(那样太占内存),访问到谁,谁才存在。
官方文档专门提醒过这件事:滚动遍历会白白创建一堆单元格。
实际影响:如果你先访问了远处的格子,再拿 max_row 当循环边界,就会多处理一串空行。
解法是别随手访问不该碰的格子,遍历时按数据的实际长度来。
另一个坑:老式 .xls 打不开
如果表是别的同事用老版 Excel 存的 .xls,openpyxl 会直接拒绝:
好消息是它把两条路都写在报错里了:装 xlrd 读老格式,或者在 Excel 里另存为 .xlsx。
顺手提一个细节:它连文件扩展名都会检查。 把 .xlsx 改名成 .xls 再打开,同样报这个错——即使里面其实还是新格式。
03 Excel · 写:让它能直接发出去
读出来的数字,得变成一张能发给别人的表。
写文件三个动作:Workbook() 新建工作簿(不用先在硬盘上建文件),append([...]) 往末尾追加一行,save() 存盘。
样式看着参数多,其实就四类:
fgColor 写的是十六进制颜色,前面那两位(alpha)可以省。
上面控制台打印的数字没有千分位,这不是 bug。number_format 管的是 Excel 打开后怎么显示,跟控制台输出无关。
两件关于样式的事
第一,列宽的单位是"字符",而且按标准字符算。 宽度 10 只能放下 5 个汉字——中文宽度算两倍,设的时候心里乘个二。
第二,样式是贴在格子上的,不是贴在行上的。 我给第一行设了底色,append() 进来的第二行不会有;给 B2 设的数字格式,B3 也不会继承。
所以别指望"设一次管一片"。 每一格想要什么样式,就明确地给它设。
04 Word:一份模板,一批文档
对账单要发六份,内容只差区域名和数字。这活儿最适合交给代码。
Document() 新建文档,add_heading、add_paragraph、add_table 依次往里加,最后 save()。
表格是"先建几行几列,再一格一格填":
先搞清段落和 run 的关系
这是 python-docx 最该先弄明白的一件事。
一个段落(paragraph)管的是整块格式——对齐、缩进、行距。
段落里的文字,被切成一个或多个 run。加粗、斜体、字体、字号这些"字级别"的格式,全设在 run 上。
add_paragraph("一句话") 会帮你建一个包含单个 run 的段落。要让句子中间几个字加粗,就得自己拆:
加粗不是段落的属性,是 run 的。 记住这一句,Word 这边的其他问题都会好懂很多。
中文字体没那么好设
现在来设字体。你写下这一句,会以为搞定了:
中文不会变。 因为 Word 把字体分成好几套:西文一套、东亚文字一套、复杂文种又一套。font.name 只动了西文那套,中文走的是另一套属性。
实测对照,两行设置生成的 XML 差别很明显:
补上 w:eastAsia,中文才跟着变:
为什么非要分这么细? 因为中西文本来就不该用同一个字体——英文用微软雅黑会显得很怪,Word 一直是分开存的。上面这一行不太好记,抄下来就行。
既然每份文档都要设,包成函数更省事:

还有个容易漏的地方:表格里的格子也是"段落套 run"。 想让表头加粗,得往下钻两层:
05 PDF · 读:把发票上的字抠出来
三张发票要报销,先把关键信息提出来。
PdfReader 打开,.pages 是页面列表,.pages[0].extract_text() 拿这一页的文字。
想知道总页数,用 len(reader.pages) 就行。
它给你的是带换行的整段文字,要变成能进表格的数据,还得自己按冒号切一刀。
同样是 PDF,有的一个字都提不出来
原因很简单:扫描件里根本没有文字,只有一张图片。 PDF 里存的是"把这张图贴在这个位置"。pypdf 不干 OCR 的活,它只能读文件里真的有的东西。

怎么判断手上的是哪一种? 在 PDF 阅读器里试着选中文字——能选中、能复制的是文字型;选不中的就是图片型。图片型得交给 OCR 工具。
加密的 PDF 先解密
一行 reader.decrypt("密码") 就能解。不解就碰页面,报的是上面那个 FileNotDecryptedError。
还有一种情况值得单独说:有些 PDF 打开不用密码,但被设置成"禁止复制"。
这类文件 decrypt("")(传空字符串)通常就能解除。这是 PDF 的两套密码机制——用户密码管"能不能打开",所有者密码管"打开后能干什么"。
提出来的是位置顺序,不是阅读顺序
官方文档把这件事讲得很直白:PDF 格式是为打印设计的,不是为了被机器读。它里面没有"段落""表格""页眉"这些概念,只有一堆各自标好坐标的文字。
所以 extract_text() 是按文字在页面上的位置拼出来的。简单的公文没问题,复杂的表格就容易串行——两栏文字交错、同一行散成几段,都常见。
真要做严谨的表格提取,得用专门处理版面的工具。但发票、通知、单一栏的公文这类,extract_text() 足够用。
06 PDF · 写:合并、拆页、盖水印
花时间最多的其实是合并。
写法很直白:PdfWriter() 起一个写入器,循环 append() 把文件并进来,最后 write() 落盘。
append() 收文件名、PdfReader 对象、或者打开的文件都可以。
sorted() 那句别省——glob 不保证顺序,不排序你的发票可能前后颠倒。
拆页换 add_page(),一次一页:
水印:不是加字,是盖一页上去
水印的思路和前面都不太一样:不做新文字,而是先做一页"只有水印字"的 PDF,再把它盖到每一页上。
水印页用 reportlab 画出来,关键是那个透明度:
不设透明度,就是一块灰板子盖住正文。 调淡之后才是水印该有的样子。
还有一点要留意:merge_page() 是就地改掉那一页,不是生成新页。后面还要用原件的话,别在原对象上直接叠。
07 动手:一个脚本跑完月末三件事
三件事串起来,八十来行。
跑完,这些文件都落在当前目录:
| 区域汇总.xlsx | |
| 对账单/ | |
| 报销附件.pdf | |
| 发票明细.csv |

第二件和第三件都复用了第一件的结果——by_region 和 detail 只算一次,后面直接拿来用。
留个练习:现在报销单里是三张发票。如果变成 50 张,脚本要改哪里?
答案是一行都不用改,直接把发票都丢进 发票/ 目录就行。
Path("发票").glob("*.pdf") 认得所有的 PDF。这就是写一次和每次重做的区别。
08 最后
三条建议:
第一,先想清楚人是怎么做的。 打开一张表、复制哪几列、贴到哪里去——把这个流程写下来,代码就是照着流程翻译。这一步想不明白,API 再熟也写不出来。
第二,三个库的 API 都很浅,难的是方向。
读和写是两个入口:load_workbook 对 Workbook,PdfReader 对 PdfWriter。
搞不清方向的时候,先问自己一句:我是要把它打开看,还是要另存一份?
第三,动手前先看一眼文件本身。 表头在第几行、有没有合并单元格、PDF 是文字型还是扫描件——这三件事决定了程序怎么写。
速查表:
















































































