使用python生成带目录的中文 PDF 报告
.....继承 ReportLab 的Paragraph;在drawOn时,通过获取当前页码,并调用;这样就能把每个标题段落记录到目录对象。你可以使用并放入 story;或者单独做一页作为封面,加LOGO、加背景图等都可以;把所有想放在封面上的元素(Paragraph、Image、Spacer等)依次 append 到 story 里,然后分割开。在本篇中,我们从零开始展示了如何用 Python
【从零开始】用 ReportLab 生成带目录的中文 PDF 报告:超详细实战指南
一、为什么要用 ReportLab?
ReportLab 是一个纯 Python 编写的库,可以让我们以脚本的方式灵活生成 PDF。相比手动编辑 Word/PDF,编写 Python 脚本能更好地批量化、自动化地创建报告,尤其适合以下场景:
- 自动生成安全审计或漏洞扫描报告;
- 批量输出统计、财务、数据分析报表;
- 自定义排版、插入图片、表格、图表等内容。
但是,如果要支持中文,我们必须额外注册中文字体;如果想要自动目录,就需要了解 TableOfContents 以及如何将标题和页码关联起来。这篇文章就将手把手带你完成这个过程。
二、准备工作
-
安装 Python 环境
- 需要 Python 3.x,版本最好不要太老,3.7+ 即可。
-
安装 ReportLab
- 在终端或 CMD 中执行:
等它自动下载安装完成。pip install reportlab
- 在终端或 CMD 中执行:
-
准备一份中文字体(TTF 格式)
- Windows 用户常见的“黑体”“宋体”等可能在
C:\Windows\Fonts下; - Linux/Mac 可能需要自己去下载,比如可以在网上找
SimHei.ttf(黑体) 或者思源黑体等。 - 假设我们有一个
SimHei.ttf文件,并且把它放在同一个项目文件夹下的fonts目录里。
- Windows 用户常见的“黑体”“宋体”等可能在
提示:中文字体文件通常比较大,请注意文件路径大小写一致,不要放错目录位置。
三、示例脚本:先看整体,再分步解释
下面给出的脚本名为 demo_pdf.py,它会:
- 注册中文字体(SimHei);
- 自定义段落来收集标题信息并生成目录;
- 创建 PDF 文件并写入几段示例性文本/表格;
- 在 PDF 中插入目录页(自动显示标题和对应的页码)。
请先将以下示例脚本复制到你的 Python 工程中,并在同目录下创建一个 fonts 文件夹,把 SimHei.ttf 放到里面。
# demo_pdf.py
import os
import logging
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, PageBreak, TableOfContents, Table, TableStyle
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import inch
from reportlab.lib import colors
# ========== 1. 注册中文字体函数 ==========
def register_chinese_font(font_path: str, font_name='SimHei'):
"""
使用TTFont注册中文字体,以供ReportLab显示中文。
:param font_path: 字体文件的绝对/相对路径 (TTF文件)
:param font_name: 在Paragraph/Table等处使用的字体名称,默认为 'SimHei'
"""
pdfmetrics.registerFont(TTFont(font_name, font_path))
logging.info(f"已注册字体: {font_name}, 路径: {font_path}")
# ========== 2. 自定义段落类:可向目录(TOC)登记标题信息 ==========
class TOCParagraph(Paragraph):
"""
继承自ReportLab的Paragraph,额外包含目录登记功能。
在drawOn阶段,将标题和当前页码加入给定的TableOfContents对象。
"""
def __init__(self, text, style, level, toc):
super().__init__(text, style)
self.level = level
self.title_text = text
self.toc = toc
def drawOn(self, canvas, x, y, _sW=0):
# 获取当前页面页码
current_page = canvas.getPageNumber()
# 将 (层级level, 标题文本, 页码) 信息注册到TOC
self.toc.addEntry(self.level, self.title_text, current_page)
# 调用父类的drawOn来进行正常的段落绘制
super().drawOn(canvas, x, y, _sW)
# ========== 3. 核心函数:生成PDF并带目录 ==========
def create_demo_pdf(output_filename, font_name='SimHei'):
"""
创建一个示例PDF,包含:封面、目录页、若干章节,以及中文表格内容。
最终写到 output_filename。
"""
# 1) 定义文档模板 (页面大小A4)
doc = SimpleDocTemplate(output_filename, pagesize=A4)
# 2) 获取初始样式表
styles = getSampleStyleSheet()
# 让默认样式的 fontName 都设置为中文字体
for s in styles.byName:
styles.byName[s].fontName = font_name
# 3) 创建一个TableOfContents对象(目录)
# 后续所有标题段落都会在这里登记
the_toc = TableOfContents()
# 4) 定义页眉/页脚的函数
def draw_page_number(canvas, doc_obj):
canvas.saveState()
page_txt = f"当前页: {doc_obj.page}"
# 设置字体为中文
canvas.setFont(font_name, 9)
# 在页面底部某个位置绘制页码
canvas.drawString(inch, 0.5 * inch, page_txt)
canvas.restoreState()
# 5) story就是ReportLab将按顺序渲染的元素列表
story = []
# (a) 封面(首页)
story.append(Paragraph("示例报告:带目录 & 中文支持", styles['Title']))
story.append(Spacer(1, 24)) # 空行
story.append(Paragraph("作者:某某\n可以在此添加封面描述、日期等信息。", styles['BodyText']))
story.append(PageBreak()) # 强制分页
# (b) 目录页
story.append(Paragraph("目 录", styles["Heading1"]))
# 在这里插入TOC对象,它会自动根据后续标题收集信息
story.append(the_toc)
story.append(Spacer(1, 12))
# 再分页
story.append(PageBreak())
# (c) 插入几个章节演示:每章用TOCParagraph登记到目录
chapter_titles = ["第一章:概述", "第二章:详细分析", "第三章:建议与结论"]
contents = [
"这是关于本报告的背景和概述,可以写很多段落内容...",
"这一部分用来展示更详细的分析,比如风险等级、原理说明等...",
"最后给出修复建议或总结结论,帮助读者采取下一步动作。"
]
for i, ch_title in enumerate(chapter_titles):
# 用自定义的TOCParagraph生成标题
story.append(TOCParagraph(ch_title, styles['Title'], level=1, toc=the_toc))
story.append(Spacer(1, 12))
# 加一小段文本
story.append(Paragraph(contents[i], styles['BodyText']))
story.append(Spacer(1, 12))
# 另外演示插入一张表格
table_data = [
["列1", "列2", "列3"],
[f"数据{i+1}-1", f"数据{i+1}-2", f"数据{i+1}-3"],
[f"数据{i+1}-4", f"数据{i+1}-5", f"数据{i+1}-6"]
]
tbl = Table(table_data, colWidths=[100, 100, 100])
tbl.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
('FONTNAME', (0, 0), (-1, -1), font_name),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
]))
story.append(tbl)
story.append(Spacer(1, 20))
# (d) 调用 multiBuild,让TOC能正确捕获页码
doc.multiBuild(
story,
onFirstPage=draw_page_number,
onLaterPages=draw_page_number
)
logging.info(f"PDF 生成完毕: {output_filename}")
# ========== 4. 主入口:执行脚本生成PDF ==========
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
# 首先,注册中文字体 (确保SimHei.ttf存在)
current_dir = os.path.dirname(os.path.abspath(__file__))
simhei_path = os.path.join(current_dir, "fonts", "SimHei.ttf")
register_chinese_font(simhei_path, 'SimHei')
# 定义输出文件名
pdf_file = os.path.join(current_dir, "test_demo.pdf")
# 生成pdf
create_demo_pdf(pdf_file, font_name='SimHei')
print(f"已生成 PDF 文件: {pdf_file}")
四、分步详细说明
第1步:注册中文字体
pdfmetrics.registerFont(TTFont(font_name, font_path))
font_path:TTF 文件路径(如fonts/SimHei.ttf);font_name:在脚本中使用的名字,这里取'SimHei';- 注册完成后,我们就可以在段落、表格等地方使用
fontName='SimHei'来渲染中文。
第2步:自定义 TOCParagraph 类
class TOCParagraph(Paragraph):
def __init__(self, text, style, level, toc):
...
def drawOn(self, canvas, x, y, _sW=0):
...
- 继承 ReportLab 的
Paragraph; - 在
drawOn时,通过canvas.getPageNumber()获取当前页码,并调用toc.addEntry(level, text, page_number); - 这样就能把每个标题段落记录到目录对象
TableOfContents。
第3步:create_demo_pdf 函数
doc = SimpleDocTemplate(output_filename, pagesize=A4): 创建一个 PDF 文档模板,指定输出文件和页面大小;styles = getSampleStyleSheet(): 获取一系列默认的样式,如Title,Heading1,BodyText等;- 把所有样式的 fontName 改成 'SimHei':这样就能让所有中文都渲染正常;
the_toc = TableOfContents(): 创建一个目录对象,后续的标题信息会写到这里;story是一个列表,依次放入段落、表格、分页等元素;Paragraph或自定义的TOCParagraph;Spacer表示空行(比如Spacer(1, 24)高度 24 points);PageBreak表示强制分页;
doc.multiBuild(story, ...): 进行多次渲染,可以让 TOC 里的页码与实际文档页码保持一致。onFirstPage和onLaterPages用于设置页眉页脚;在脚本中,我只是在底部打印一个“当前页: x”。
第4步:在 if __name__ == "__main__": 中执行
logging.basicConfig(level=logging.INFO): 开启日志;- 找到
SimHei.ttf文件并注册; - 调用
create_demo_pdf(...)生成 PDF; - 最终生成
test_demo.pdf文件。
五、运行效果与检查
-
执行脚本
在终端进入脚本所在目录,然后运行:python demo_pdf.py如果一切正常,打印类似:
INFO:root:已注册字体: SimHei, 路径: /xxx/xxx/fonts/SimHei.ttf INFO:root:PDF 生成完毕: /xxx/xxx/test_demo.pdf 已生成 PDF 文件: /xxx/xxx/test_demo.pdf -
查看生成的 PDF
打开test_demo.pdf可以看到:- 第一页:标题“示例报告:带目录 & 中文支持”,下面是作者信息
- 第二页:标题“目 录”,下方自动生成了章标题列表,并附有页码
- 第三页及以后:分别是“第一章:概述”、“第二章:详细分析”、“第三章:建议与结论”这三章;每章里面有一段文本和一个简易表格;
- 每一页底部都会显示“当前页: x”,即我们的页眉页脚示例。
-
中文显示是否正确
- 如果中文出现方块或乱码,需要确认:
- 你的字体文件
SimHei.ttf是否包含简体中文; - 脚本路径、大小写拼写等有没有写错;
- 是否真的执行了
register_chinese_font并把styles.byName[s].fontName = 'SimHei'。
- 你的字体文件
- 如果中文出现方块或乱码,需要确认:
-
目录与实际页码对不对
- 一般通过
doc.multiBuild(...)都能保证目录页码准确; - 如果你先后插入了很多 PageBreak,或在执行前后动态修改 story,可能需要多次渲染。通常此示例足以满足多数需求。
- 一般通过
六、常见疑问
1. 如何改成别的中文字体?
- 只要在
register_chinese_font里换成自己喜欢的.ttf文件,改一下font_name即可; - 也可以注册多个字体,然后对不同样式指定不同的
fontName。
2. 我想要多级目录(Heading2, Heading3)可以吗?
- 可以。在
TOCParagraph里传不同的level值,level=1表示一级标题,level=2二级标题,依此类推。 - 在目录对象
TableOfContents生成的目录中,会自动对多级标题做缩进或其他样式。你也可进行更细致的自定义。
3. 怎么插入图片或自定义封面?
- 你可以使用
Image('path/to/image')并放入 story; - 或者单独做一页作为封面,加LOGO、加背景图等都可以;
- 把所有想放在封面上的元素(Paragraph、Image、Spacer等)依次 append 到 story 里,然后
PageBreak()分割开。
4. 想让目录也带链接或可点击跳转到相应页?
- ReportLab 的 TOC 也支持内部跳转(Outline/Bookmark 机制),但需要额外配置链接的 tag。可参考 ReportLab User Guide 中的高级用法,或在
addEntry时设置跳转属性。
5. 我想换个版式,比如上下双栏、背景图、复杂模板等?
- ReportLab 提供“PageTemplate”“Frame”“Canvas” 等低级API,能实现非常丰富的版式,但会比 SimpleDocTemplate 要复杂一些。
- 如果只是轻度自定义,SimpleDocTemplate + story 也可以满足大部分“插图、表格、段落、分页”等需求。
七、总结
在本篇中,我们从零开始展示了如何用 Python + ReportLab 做到以下目标:
- 注册中文字体,让 PDF 内能够正确显示中文;
- 自动生成目录(TOC),并把标题和页码对应起来;
- 多页排版:封面、目录页、正文、多段落、多表格;
- 基本的页眉页脚(这里只加了页码,还可以加 LOGO、线条、日期等)。
只要理解了这几个核心点,你就可以进一步将其扩展到各种自动化报告场景,例如:漏洞扫描报告、统计分析报告、财务报告、科研数据可视化 等。
欢迎你参考此示例脚本,结合自己项目需求不断迭代完善。如果你在过程中遇到问题,欢迎在评论区交流讨论~
参考链接
如果觉得这篇文章对你有所帮助,欢迎点赞、收藏或关注,期待与你一起进步!
——完——
更多推荐
所有评论(0)