夜雨聆风学习资料网

ARTICLE · 1180101

python办公自动化:六份表、六份文档、三张发票,一个脚本搞定

python办公自动化:六份表、六份文档、三张发票,一个脚本搞定

办公自动化:六份表、六份文档、三张发票,一个脚本搞定

每个月末,你要干三件事。

六个区域交来的销售表,合并成一张总表;按区域生成六份对账单,发给各区域经理签字;报上去的发票,并成一份 PDF 附件。

每件事都不难,就是重复。一页页复制粘贴、一份份改文件名,一下午就没了。

这三件事,Python 都能替你干。而且用的是三个很小的库,每一个的核心用法都不超过五行。

要处理的文件
库
装的时候写什么
Excel(.xlsx)
openpyxl
openpyxl
Word(.docx)
python-docx
python-docx
PDF
pypdf
pypdf

注意中间那个:装的时候叫 python-docx,代码里却要写 import docx。 名字不一样,第一次用很容易卡在这儿。

pip install openpyxl python-docx pypdf

01 这些文件,其实不是黑盒

在动手之前,先看一件可能会改变你想法的事。

一个 .xlsx 文件,用压缩包的方式打开,里面是这样的:

$ python 01_inside.py把 销售数据/华北.xlsx 当压缩包打开,里面有 9 个部件:   [Content_Types].xml                   975 bytes   _rels/.rels                           531 bytes   docProps/app.xml                      205 bytes   docProps/core.xml                     555 bytes   xl/_rels/workbook.xml.rels            504 bytes   xl/styles.xml                        2832 bytes   xl/theme/theme1.xml                 10140 bytes   xl/workbook.xml                       547 bytes   xl/worksheets/sheet1.xml             9679 bytes单元格和文字,都在 xl/worksheets/sheet1.xml 里:<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetPr><outlinePr summaryBelow="1" summaryRight="1"/><pageSetUpPr/></sheetPr><dimension ref="A1:F31"/><sheetViews><sheetView workbookViewId="0"><select ……

你天天点的 Excel,就是把这些 XML 渲染成格子的一个界面。 文件本身没有任何"只能用手点"的地方。

.docx 也一样,同样是压缩包加一堆 XML。知道这一点,后面的事就顺理成章了——读文件就是读里面的数据,写文件就是拼出这些数据再存回去。

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

先看看手上有什么。

$ python 02_list.py这个文件夹里有 6 份表:   华东.xlsx       6.0 KB   华中.xlsx       6.0 KB   华北.xlsx       6.0 KB   华南.xlsx       6.0 KB   西北.xlsx       6.0 KB   西南.xlsx       6.0 KB

六份,结构一样。打开其中一份看看:

$ python 02_read.py工作表名: 华北大小: 31 行 x 6 列表头  : ['日期', '订单号', '商品', '数量', '单价', '金额']第 2 行: ['2026-01-01', 'A00001', '手机', 1, 3999, 3999]第 3 行: ['2026-01-02', 'A00002', '平板', 1, 2688, 2688]

三行代码对应三个动作:load_workbook() 打开文件,.active 拿到当前那张工作表,ws[1] 拿第一行。

ws[1] 和 ws["A1"] 两种写法都行,前者给整行,后者给单个格子。

要批量处理,就得一行行过,用 iter_rows():

for row in ws.iter_rows(min_row=2, values_only=True):    print(row)

min_row=2 跳过表头。values_only=True 是个好习惯——不加它,拿到的是单元格对象,取值还要补一句 .value。

现在把六份合起来:

$ python 02_merge.py华东   30 条华中   30 条华北   30 条华南   30 条西北   30 条西南   30 条合起来 180 条前 3 条:    ('华东', '2026-01-01', 'A00101', '耳机', 2, 399, 798)    ('华东', '2026-01-02', 'A00102', '移动电源', 1, 129, 129)    ('华东', '2026-01-03', 'A00103', '智能手表', 4, 1299, 5196)

(f.stem,) + row 那个逗号别漏了——它把区域名和原来的六列拼成七列,区域名在前,来源一目了然。同一件事手工做要复制粘贴 180 次。

一个不看代码看不出来的坑

你"看一眼"某个单元格,openpyxl 就会把它创建出来。

$ python 02_pit_maxrow.py刚打开时   max_row = 31现在只是看一眼 Z99 的值(不赋值)Z99 的值   = None但 max_row = 99

这份表本来只有 31 行,因为我访问了一次 Z99,max_row 就变成了 99——中间那 68 行是空的。

为什么?openpyxl 打开文件时不会把所有格子都建出来(那样太占内存),访问到谁,谁才存在。

官方文档专门提醒过这件事:滚动遍历会白白创建一堆单元格。

实际影响:如果你先访问了远处的格子,再拿 max_row 当循环边界,就会多处理一串空行。

解法是别随手访问不该碰的格子,遍历时按数据的实际长度来。

另一个坑:老式 .xls 打不开

如果表是别的同事用老版 Excel 存的 .xls,openpyxl 会直接拒绝:

InvalidFileException: openpyxl does not support the old .xls file format,please use xlrd to read this file, or convert it to the more recent .xlsx file

好消息是它把两条路都写在报错里了:装 xlrd 读老格式,或者在 Excel 里另存为 .xlsx。

顺手提一个细节:它连文件扩展名都会检查。 把 .xlsx 改名成 .xls 再打开,同样报这个错——即使里面其实还是新格式。

03 Excel · 写:让它能直接发出去

读出来的数字,得变成一张能发给别人的表。

$ python 03_write.py已写出 区域汇总.xlsx   区域             金额   华东         167681   华中         196565   华北         142622   华南         180430   西北         124954   西南         202227   合计        1014479B1 是否加粗: TrueA1 的底色  : 004472C4A 列宽度   : 10.0

写文件三个动作:Workbook() 新建工作簿(不用先在硬盘上建文件),append([...]) 往末尾追加一行,save() 存盘。

样式看着参数多,其实就四类:

for c in ws[1]:    c.font = Font(bold=True, color="FFFFFF")          # 白字加粗    c.fill = PatternFill("solid", fgColor="4472C4")   # 蓝底    c.alignment = Alignment(horizontal="center")      # 居中row[1].number_format = "#,##0.00"                     # 千分位 + 两位小数ws.column_dimensions["A"].width = 10                  # 列宽

fgColor 写的是十六进制颜色,前面那两位(alpha)可以省。

上面控制台打印的数字没有千分位,这不是 bug。number_format 管的是 Excel 打开后怎么显示,跟控制台输出无关。

两件关于样式的事

第一,列宽的单位是"字符",而且按标准字符算。 宽度 10 只能放下 5 个汉字——中文宽度算两倍,设的时候心里乘个二。

第二,样式是贴在格子上的,不是贴在行上的。 我给第一行设了底色,append() 进来的第二行不会有;给 B2 设的数字格式,B3 也不会继承。

第 1 行 A1 底色: 004472C4第 2 行 A2 底色: 00000000      ← 没继承第 2 行 B2 格式: '#,##0.00'第 3 行 B3 格式: 'General'     ← 没继承

所以别指望"设一次管一片"。 每一格想要什么样式,就明确地给它设。

04 Word:一份模板,一批文档

对账单要发六份,内容只差区域名和数字。这活儿最适合交给代码。

$ python 04_docx.py已写出 对账单_华北.docx段落:   [Heading 1] 2026 年 1 月区域对账单   [Normal] 区域:华北   [Normal] 本月共成交 30 笔,金额合计 142,622.00 元。请核对后回传签字页。表格: 1 个    ['商品', '数量', '金额']    ['手机', '1', '3,999.00']    ['平板', '1', '2,688.00']    ['手机', '1', '3,999.00']

Document() 新建文档,add_heading、add_paragraph、add_table 依次往里加,最后 save()。

表格是"先建几行几列,再一格一格填":

table = doc.add_table(rows=1, cols=3)table.rows[0].cells[0].text = "商品"cells = table.add_row().cells      # 加一行,拿到它的格子cells[0].text = "手机"

先搞清段落和 run 的关系

这是 python-docx 最该先弄明白的一件事。

一个段落(paragraph)管的是整块格式——对齐、缩进、行距。

段落里的文字,被切成一个或多个 run。加粗、斜体、字体、字号这些"字级别"的格式,全设在 run 上。

add_paragraph("一句话") 会帮你建一个包含单个 run 的段落。要让句子中间几个字加粗,就得自己拆:

p = doc.add_paragraph()p.add_run("本月共成交 ")p.add_run("30 笔").bold = True

加粗不是段落的属性,是 run 的。 记住这一句,Word 这边的其他问题都会好懂很多。

中文字体没那么好设

现在来设字体。你写下这一句,会以为搞定了:

run.font.name = "微软雅黑"

中文不会变。 因为 Word 把字体分成好几套:西文一套、东亚文字一套、复杂文种又一套。font.name 只动了西文那套,中文走的是另一套属性。

实测对照,两行设置生成的 XML 差别很明显:

$ python 04_font.py已写出 字体对照.docx只设 name    -> <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑"/>加了 eastAsia -> <w:rFonts w:ascii="微软雅黑" w:hAnsi="微软雅黑" w:eastAsia="微软雅黑"/>

补上 w:eastAsia,中文才跟着变:

from docx.oxml.ns import qnrun.font.name = "微软雅黑"run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")

为什么非要分这么细? 因为中西文本来就不该用同一个字体——英文用微软雅黑会显得很怪,Word 一直是分开存的。上面这一行不太好记,抄下来就行。

既然每份文档都要设,包成函数更省事:

HEI = "微软雅黑"def set_hei(run, size=None, bold=None):    run.font.name = HEI    run._element.rPr.rFonts.set(qn("w:eastAsia"), HEI)    if size is not None:        run.font.size = Pt(size)    if bold is not None:        run.font.bold = bold

还有个容易漏的地方:表格里的格子也是"段落套 run"。 想让表头加粗,得往下钻两层:

cell = table.rows[0].cells[0]cell.text = "商品"                        # 这样只是塞了纯文本cell.paragraphs[0].runs[0].bold = True    # 要加粗,得够到里面的 run

05 PDF · 读:把发票上的字抠出来

三张发票要报销,先把关键信息提出来。

$ python 05_text.py=== 发票_001.pdf | 1 页 ===    电子发票(普通发票)    发票号码:20260105001    开票日期:2026-01-05    购买方:北京云尔科技有限公司    价税合计:5,998.00 元

PdfReader 打开,.pages 是页面列表,.pages[0].extract_text() 拿这一页的文字。

想知道总页数,用 len(reader.pages) 就行。

它给你的是带换行的整段文字,要变成能进表格的数据,还得自己按冒号切一刀。

同样是 PDF,有的一个字都提不出来

$ python 05_scan.py扫描件的页数: 1提取到的文字: ''长度: 0 个字符

原因很简单:扫描件里根本没有文字,只有一张图片。 PDF 里存的是"把这张图贴在这个位置"。pypdf 不干 OCR 的活,它只能读文件里真的有的东西。

怎么判断手上的是哪一种? 在 PDF 阅读器里试着选中文字——能选中、能复制的是文字型;选不中的就是图片型。图片型得交给 OCR 工具。

加密的 PDF 先解密

$ python 05_decrypt.py是否加密: True先不输密码,直接读文字:    FileNotDecryptedError : File has not been decrypted用密码 1234 解开:    电子发票(普通发票)

一行 reader.decrypt("密码") 就能解。不解就碰页面,报的是上面那个 FileNotDecryptedError。

还有一种情况值得单独说:有些 PDF 打开不用密码,但被设置成"禁止复制"。

这类文件 decrypt("")(传空字符串)通常就能解除。这是 PDF 的两套密码机制——用户密码管"能不能打开",所有者密码管"打开后能干什么"。

提出来的是位置顺序,不是阅读顺序

官方文档把这件事讲得很直白:PDF 格式是为打印设计的,不是为了被机器读。它里面没有"段落""表格""页眉"这些概念,只有一堆各自标好坐标的文字。

所以 extract_text() 是按文字在页面上的位置拼出来的。简单的公文没问题,复杂的表格就容易串行——两栏文字交错、同一行散成几段,都常见。

真要做严谨的表格提取,得用专门处理版面的工具。但发票、通知、单一栏的公文这类,extract_text() 足够用。

06 PDF · 写:合并、拆页、盖水印

花时间最多的其实是合并。

$ python 06_merge.py已并入 发票_001.pdf已并入 发票_002.pdf已并入 发票_003.pdf报销附件.pdf 共 3 页   第 1 页: 电子发票(普通发票)   第 2 页: 电子发票(普通发票)   第 3 页: 电子发票(普通发票)

写法很直白:PdfWriter() 起一个写入器,循环 append() 把文件并进来,最后 write() 落盘。

writer = PdfWriter()for f in sorted(Path("发票").glob("*.pdf")):    writer.append(str(f))writer.write("报销附件.pdf")

append() 收文件名、PdfReader 对象、或者打开的文件都可以。

sorted() 那句别省——glob 不保证顺序,不排序你的发票可能前后颠倒。

拆页换 add_page(),一次一页:

$ python 06_split.py原件共 3 页已拆出第 2 页 -> 报销附件_第2页.pdf新文件共 1 页,内容是:    电子发票(普通发票)    发票号码:20260106002    开票日期:2026-01-06    购买方:上海明信贸易有限公司    价税合计:399.50 元

水印:不是加字,是盖一页上去

$ python 06_watermark.py已写出 报销附件_已盖章.pdf,共 3 页

水印的思路和前面都不太一样:不做新文字,而是先做一页"只有水印字"的 PDF,再把它盖到每一页上。

watermark = PdfReader("水印页.pdf").pages[0]for page in reader.pages:    page.merge_page(watermark)     # 叠上去    writer.add_page(page)

水印页用 reportlab 画出来,关键是那个透明度:

c.setFillAlpha(0.22)

不设透明度,就是一块灰板子盖住正文。 调淡之后才是水印该有的样子。

还有一点要留意:merge_page() 是就地改掉那一页,不是生成新页。后面还要用原件的话,别在原对象上直接叠。

07 动手:一个脚本跑完月末三件事

三件事串起来,八十来行。

$ python 07_month_end.py【第一件】合并销售表  写出 区域汇总.xlsx,180 条明细     华东   30 笔       167,681     华中   30 笔       196,565     华北   30 笔       142,622     华南   30 笔       180,430     西北   30 笔       124,954     西南   30 笔       202,227【第二件】生成对账单  写出 6 份,都在 对账单/ 里      对账单_华东.docx      对账单_华中.docx      对账单_华北.docx      对账单_华南.docx      对账单_西北.docx      对账单_西南.docx【第三件】合并发票  写出 报销附件.pdf,共 3 页  写出 发票明细.csv发票金额合计: 19,197.5 元三件事做完了。

跑完,这些文件都落在当前目录:

文件
是什么
区域汇总.xlsx
六个区域的笔数和金额,带样式
对账单/
六份 Word,每份带前五行明细
报销附件.pdf
三张发票并成一份
发票明细.csv
从发票里抠出来的结构化数据

第二件和第三件都复用了第一件的结果——by_region 和 detail 只算一次,后面直接拿来用。

留个练习:现在报销单里是三张发票。如果变成 50 张,脚本要改哪里?

答案是一行都不用改,直接把发票都丢进 发票/ 目录就行。

Path("发票").glob("*.pdf") 认得所有的 PDF。这就是写一次和每次重做的区别。

08 最后

三条建议:

第一,先想清楚人是怎么做的。 打开一张表、复制哪几列、贴到哪里去——把这个流程写下来,代码就是照着流程翻译。这一步想不明白,API 再熟也写不出来。

第二,三个库的 API 都很浅,难的是方向。

读和写是两个入口:load_workbook 对 Workbook,PdfReader 对 PdfWriter。

搞不清方向的时候,先问自己一句:我是要把它打开看,还是要另存一份?

第三,动手前先看一眼文件本身。 表头在第几行、有没有合并单元格、PDF 是文字型还是扫描件——这三件事决定了程序怎么写。

速查表:

下一篇讲数据可视化,让数据变成图表,大家敬请期待吧!

相关学习资料