前面四篇都是 "处理已有的 PDF",今天换个方向:从零生成 PDF。
职场中最痛苦的事情之一,就是每个月 / 每周写业务报告。数据从 Excel 里复制出来,粘贴到 Word,调整格式,插入图表,导出 PDF…… 一套流程下来两小时没了,而且每个月都要重复。
如果用 Python 自动生成呢?数据读取→表格生成→图表绘制→排版导出,全流程自动化。每月跑一次脚本,报告自动生成,你只需要检查一下就能发。
01 reportlab 是什么
reportlab 是 Python 最强大的 PDF 生成库,没有之一。它能做的事情包括:
安装:
reportlab 有两种使用方式:
低层 API(Canvas):像画笔一样在 PDF 页面上精确绘制每个元素,灵活但繁琐,适合精确排版。
高层 API(Platypus):用 "流式布局" 的方式,把段落、表格、图片作为 "元素" 依次放入文档,自动分页和排版,适合生成报告。
本文主要用 Platypus 高层 API,生成业务报告更高效。
02 第一个 PDF:Hello World
from reportlab.lib.pagesizes import A4from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacerfrom reportlab.lib.styles import getSampleStyleSheetfrom reportlab.lib.units import cm# 创建文档doc = SimpleDocTemplate("hello.pdf", pagesize=A4)# 获取样式styles = getSampleStyleSheet()# 构建内容列表story = []# 添加标题story.append(Paragraph("2024年Q3业务报告", styles["Title"]))story.append(Spacer(1, 0.5 * cm))# 添加正文段落story.append(Paragraph("本报告汇总了2024年第三季度的核心业务数据,包括销售业绩、客户增长和运营效率等关键指标。", styles["Normal"]))story.append(Spacer(1, 0.3 * cm))story.append(Paragraph("报告数据来源于公司CRM系统和财务系统,统计周期为2024年7月1日至9月30日。", styles["Normal"]))# 生成PDFdoc.build(story)print("PDF生成成功: hello.pdf")
核心概念:
SimpleDocTemplate:文档模板,负责页面设置和整体布局。story:内容列表,按顺序存放所有元素(段落、表格、图片等),最后用 doc.build(story) 一次性生成。Paragraph:段落元素,支持 HTML 标签(如 <b>加粗</b>、<i>斜体</i>)。styles:样式集合,reportlab 自带了 Title、Heading1、Normal 等预设样式。
03 中文支持:注册中文字体
reportlab 默认不支持中文,直接写中文会显示为方框。需要注册中文字体:
from reportlab.lib.pagesizes import A4from reportlab.platypus import SimpleDocTemplate, Paragraphfrom reportlab.lib.styles import getSampleStyleSheet, ParagraphStylefrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.ttfonts import TTFont# 注册中文字体(Windows示例)pdfmetrics.registerFont(TTFont("SimSun", "C:/Windows/Fonts/simsun.ttc"))pdfmetrics.registerFont(TTFont("SimHei", "C:/Windows/Fonts/simhei.ttf"))# 创建自定义样式styles = getSampleStyleSheet()title_style = ParagraphStyle( "ChineseTitle", parent=styles["Title"], fontName="SimHei", fontSize=20, leading=28, alignment=1, # 居中)body_style = ParagraphStyle( "ChineseBody", parent=styles["Normal"], fontName="SimSun", fontSize=11, leading=18, firstLineIndent=22, # 首行缩进)# 使用doc = SimpleDocTemplate("chinese.pdf", pagesize=A4)story = [ Paragraph("2024年第三季度业务报告", title_style), Paragraph("本报告汇总了2024年第三季度的核心业务数据……", body_style),]doc.build(story)print("中文PDF生成成功")
各系统字体路径:
- Windows:
C:/Windows/Fonts/simsun.ttc(宋体)、simhei.ttf(黑体) - macOS:
/System/Library/Fonts/PingFang.ttc(苹方) - Linux:
/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc(文泉驿,需先安装)
04 绘制表格
表格是业务报告的核心元素。reportlab 的 Table 功能非常强大:
from reportlab.lib.pagesizes import A4from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacerfrom reportlab.lib.styles import getSampleStyleSheet, ParagraphStylefrom reportlab.lib import colorsfrom reportlab.lib.units import cmfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.ttfonts import TTFont# 注册字体(同上,省略)pdfmetrics.registerFont(TTFont("SimHei", "C:/Windows/Fonts/simhei.ttf"))pdfmetrics.registerFont(TTFont("SimSun", "C:/Windows/Fonts/simsun.ttc"))styles = getSampleStyleSheet()cell_style = ParagraphStyle("Cell", fontName="SimSun", fontSize=10, leading=14, alignment=1)header_style = ParagraphStyle("Header", fontName="SimHei", fontSize=10, leading=14, alignment=1, textColor=colors.white)# 表格数据data = [ [Paragraph("指标", header_style), Paragraph("Q1", header_style), Paragraph("Q2", header_style), Paragraph("Q3", header_style), Paragraph("环比", header_style)], [Paragraph("销售额(万元)", cell_style), Paragraph("1,250", cell_style), Paragraph("1,480", cell_style), Paragraph("1,720", cell_style), Paragraph("+16.2%", cell_style)], [Paragraph("客户数", cell_style), Paragraph("320", cell_style), Paragraph("385", cell_style), Paragraph("452", cell_style), Paragraph("+17.4%", cell_style)], [Paragraph("客单价(元)", cell_style), Paragraph("390", cell_style), Paragraph("384", cell_style), Paragraph("380", cell_style), Paragraph("-1.0%", cell_style)], [Paragraph("复购率", cell_style), Paragraph("28%", cell_style), Paragraph("31%", cell_style), Paragraph("35%", cell_style), Paragraph("+4pp", cell_style)],]# 创建表格table = Table(data, colWidths=[4*cm, 2.5*cm, 2.5*cm, 2.5*cm, 2.5*cm])# 设置样式table.setStyle(TableStyle([ # 表头背景色 ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2E5BFF")), # 表格边框 ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), # 交替行背景色 ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F7FA")]), # 内边距 ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8), # 垂直居中 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),]))# 生成PDFdoc = SimpleDocTemplate("table_report.pdf", pagesize=A4)story = [ Paragraph("核心指标汇总", ParagraphStyle("H", fontName="SimHei", fontSize=14, leading=20)), Spacer(1, 0.5*cm), table,]doc.build(story)print("表格PDF生成成功")
TableStyle 常用参数:
BACKGROUND:背景色,参数为 (起始列,起始行,结束列,结束行,颜色)GRIDROWBACKGROUNDSSPAN:合并单元格,如 ("SPAN", (0,0), (1,0)) 合并前两列的第一行FONTNAMEALIGN
05 插入图表
推荐方案:用 matplotlib 生成图表再插入 PDF
reportlab 自带的图表功能有限,样式不够美观。更实用的方案是用 matplotlib 生成精美的图表,保存为图片,再插入 PDF:
import matplotlib.pyplot as pltimport matplotlibmatplotlib.rcParams["font.sans-serif"] = ["SimHei"]matplotlib.rcParams["axes.unicode_minus"] = False# 生成柱状图quarters = ["Q1", "Q2", "Q3", "Q4"]sales = [1250, 1480, 1720, 1890]plt.figure(figsize=(8, 4))plt.bar(quarters, sales, color="#2E5BFF")plt.title("季度销售额趋势", fontsize=14)plt.ylabel("销售额(万元)")plt.grid(axis="y", alpha=0.3)plt.tight_layout()plt.savefig("sales_chart.png", dpi=150)plt.close()# 插入PDFfrom reportlab.platypus import Imagefrom reportlab.lib.units import cmimg = Image("sales_chart.png", width=14*cm, height=7*cm)story.append(img)
这个方案的优势是 matplotlib 的图表样式更丰富、更美观,而且你可以完全控制图表的每一个细节。
06 完整业务报告生成示例
现在把上面的知识点整合起来,生成一份完整的业务报告:
from reportlab.lib.pagesizes import A4from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreakfrom reportlab.lib.styles import ParagraphStylefrom reportlab.lib import colorsfrom reportlab.lib.units import cmfrom reportlab.pdfbase import pdfmetricsfrom reportlab.pdfbase.ttfonts import TTFont# 注册字体pdfmetrics.registerFont(TTFont("SimHei", "C:/Windows/Fonts/simhei.ttf"))pdfmetrics.registerFont(TTFont("SimSun", "C:/Windows/Fonts/simsun.ttc"))# 样式定义title_style = ParagraphStyle("Title", fontName="SimHei", fontSize=22, leading=30, alignment=1, spaceAfter=20)h1_style = ParagraphStyle("H1", fontName="SimHei", fontSize=16, leading=24, spaceBefore=15, spaceAfter=10)h2_style = ParagraphStyle("H2", fontName="SimHei", fontSize=13, leading=20, spaceBefore=10, spaceAfter=8)body_style = ParagraphStyle("Body", fontName="SimSun", fontSize=11, leading=18, firstLineIndent=22)cell_style = ParagraphStyle("Cell", fontName="SimSun", fontSize=10, leading=14, alignment=1)header_style = ParagraphStyle("Header", fontName="SimHei", fontSize=10, leading=14, alignment=1, textColor=colors.white)# 构建报告doc = SimpleDocTemplate("business_report.pdf", pagesize=A4, topMargin=2*cm, bottomMargin=2*cm)story = []# 封面标题story.append(Spacer(1, 3*cm))story.append(Paragraph("2024年第三季度业务报告", title_style))story.append(Spacer(1, 1*cm))story.append(Paragraph("报告周期:2024年7月1日 - 9月30日", ParagraphStyle("Sub", fontName="SimSun", fontSize=12, alignment=1)))story.append(PageBreak())# 第一章:概述story.append(Paragraph("一、报告概述", h1_style))story.append(Paragraph("2024年第三季度,公司整体业务保持稳健增长。核心指标方面,销售额达到1,720万元,环比增长16.2%;客户总数达到452家,环比增长17.4%;复购率提升至35%,客户粘性持续增强。", body_style))story.append(Spacer(1, 0.3*cm))# 第二章:核心指标story.append(Paragraph("二、核心指标汇总", h1_style))story.append(Spacer(1, 0.3*cm))data = [ [Paragraph("指标", header_style), Paragraph("Q1", header_style), Paragraph("Q2", header_style), Paragraph("Q3", header_style), Paragraph("环比", header_style)], [Paragraph("销售额(万元)", cell_style), Paragraph("1,250", cell_style), Paragraph("1,480", cell_style), Paragraph("1,720", cell_style), Paragraph("+16.2%", cell_style)], [Paragraph("客户数", cell_style), Paragraph("320", cell_style), Paragraph("385", cell_style), Paragraph("452", cell_style), Paragraph("+17.4%", cell_style)], [Paragraph("客单价(元)", cell_style), Paragraph("390", cell_style), Paragraph("384", cell_style), Paragraph("380", cell_style), Paragraph("-1.0%", cell_style)], [Paragraph("复购率", cell_style), Paragraph("28%", cell_style), Paragraph("31%", cell_style), Paragraph("35%", cell_style), Paragraph("+4pp", cell_style)],]table = Table(data, colWidths=[4*cm, 2.5*cm, 2.5*cm, 2.5*cm, 2.5*cm])table.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#2E5BFF")), ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#F5F7FA")]), ("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),]))story.append(table)# 插入图表(假设已用matplotlib生成)story.append(Spacer(1, 0.5*cm))story.append(Paragraph("季度销售额趋势图", h2_style))story.append(Image("sales_chart.png", width=14*cm, height=7*cm))# 生成doc.build(story)print("完整业务报告生成成功: business_report.pdf")
07 页眉页脚和页码
专业报告通常需要页眉页脚和页码。用 onFirstPage 和 onLaterPages 回调函数实现:
from reportlab.lib.pagesizes import A4from reportlab.platypus import SimpleDocTemplate, Paragraphfrom reportlab.lib.units import cmdef add_header_footer(canvas, doc): canvas.saveState() # 页眉 canvas.setFont("SimHei", 9) canvas.drawString(2*cm, A4[1] - 1.5*cm, "2024年Q3业务报告") canvas.drawRightString(A4[0] - 2*cm, A4[1] - 1.5*cm, "机密文件") # 页眉线 canvas.setStrokeColorRGB(0.5, 0.5, 0.5) canvas.line(2*cm, A4[1] - 1.7*cm, A4[0] - 2*cm, A4[1] - 1.7*cm) # 页脚页码 canvas.setFont("SimSun", 9) page_num = canvas.getPageNumber() canvas.drawCentredString(A4[0]/2, 1.2*cm, f"- {page_num} -") canvas.restoreState()doc = SimpleDocTemplate( "report_with_header.pdf", pagesize=A4, topMargin=2.5*cm, bottomMargin=2*cm,)doc.build(story, onFirstPage=add_header_footer, onLaterPages=add_header_footer)
08 常见坑与解决方案
坑 1:中文显示为方框
必须注册中文字体,且所有用到中文的样式都要指定 fontName 为注册的中文字体。别忘了图表中的中文也要设置字体。
坑 2:表格内容溢出
如果单元格内容太长,会溢出表格。解决方法:1)用 Paragraph 包裹单元格内容,支持自动换行;2)调整列宽;3)缩小字号。
坑 3:图片模糊
插入的图片分辨率不够会模糊。用 matplotlib 生成图表时设置 dpi=150 或更高。插入图片时按比例缩放,不要拉伸变形。
坑 4:分页时表格被截断
长表格跨页时,reportlab 默认会把表格整体放在一页,放不下就会报错或留白。解决方法:使用 LongTable 替代 Table,它支持自动跨页。或者把大表格拆成多个小表格。
写在最后
用 reportlab 生成 PDF 业务报告,前期需要花时间搭建模板和样式,但一旦模板建好,后续每月只需要更新数据,跑一下脚本就能生成一份格式统一、排版精美的报告。这就是自动化的价值:把重复劳动交给代码,把创造性工作留给自己。