【从零开始】用 ReportLab 生成带目录的中文 PDF 报告:超详细实战指南

一、为什么要用 ReportLab?

ReportLab 是一个纯 Python 编写的库,可以让我们以脚本的方式灵活生成 PDF。相比手动编辑 Word/PDF,编写 Python 脚本能更好地批量化自动化地创建报告,尤其适合以下场景:

  • 自动生成安全审计或漏洞扫描报告;
  • 批量输出统计、财务、数据分析报表;
  • 自定义排版、插入图片、表格、图表等内容。

但是,如果要支持中文,我们必须额外注册中文字体;如果想要自动目录,就需要了解 TableOfContents 以及如何将标题和页码关联起来。这篇文章就将手把手带你完成这个过程。


二、准备工作

  1. 安装 Python 环境

    • 需要 Python 3.x,版本最好不要太老,3.7+ 即可。
  2. 安装 ReportLab

    • 在终端或 CMD 中执行:
      pip install reportlab
      
      等它自动下载安装完成。
  3. 准备一份中文字体(TTF 格式)

    • Windows 用户常见的“黑体”“宋体”等可能在 C:\Windows\Fonts 下;
    • Linux/Mac 可能需要自己去下载,比如可以在网上找 SimHei.ttf (黑体) 或者思源黑体等。
    • 假设我们有一个 SimHei.ttf 文件,并且把它放在同一个项目文件夹下的 fonts 目录里。

提示:中文字体文件通常比较大,请注意文件路径大小写一致,不要放错目录位置。


三、示例脚本:先看整体,再分步解释

下面给出的脚本名为 demo_pdf.py,它会:

  1. 注册中文字体(SimHei);
  2. 自定义段落来收集标题信息并生成目录;
  3. 创建 PDF 文件并写入几段示例性文本/表格;
  4. 在 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 里的页码与实际文档页码保持一致。
    • onFirstPageonLaterPages 用于设置页眉页脚;在脚本中,我只是在底部打印一个“当前页: x”。

第4步:在 if __name__ == "__main__": 中执行

  • logging.basicConfig(level=logging.INFO): 开启日志;
  • 找到 SimHei.ttf 文件并注册;
  • 调用 create_demo_pdf(...) 生成 PDF;
  • 最终生成 test_demo.pdf 文件。

五、运行效果与检查

  1. 执行脚本
    在终端进入脚本所在目录,然后运行:

    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
    
  2. 查看生成的 PDF
    打开 test_demo.pdf 可以看到:

    • 第一页:标题“示例报告:带目录 & 中文支持”,下面是作者信息
    • 第二页:标题“目 录”,下方自动生成了章标题列表,并附有页码
    • 第三页及以后:分别是“第一章:概述”、“第二章:详细分析”、“第三章:建议与结论”这三章;每章里面有一段文本和一个简易表格;
    • 每一页底部都会显示“当前页: x”,即我们的页眉页脚示例。
  3. 中文显示是否正确

    • 如果中文出现方块或乱码,需要确认:
      1. 你的字体文件 SimHei.ttf 是否包含简体中文;
      2. 脚本路径、大小写拼写等有没有写错;
      3. 是否真的执行了 register_chinese_font 并把 styles.byName[s].fontName = 'SimHei'
  4. 目录与实际页码对不对

    • 一般通过 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 做到以下目标:

  1. 注册中文字体,让 PDF 内能够正确显示中文;
  2. 自动生成目录(TOC),并把标题和页码对应起来;
  3. 多页排版:封面、目录页、正文、多段落、多表格;
  4. 基本的页眉页脚(这里只加了页码,还可以加 LOGO、线条、日期等)。

只要理解了这几个核心点,你就可以进一步将其扩展到各种自动化报告场景,例如:漏洞扫描报告、统计分析报告、财务报告、科研数据可视化 等。
欢迎你参考此示例脚本,结合自己项目需求不断迭代完善。如果你在过程中遇到问题,欢迎在评论区交流讨论~


参考链接

  1. ReportLab 官方文档
  2. Python: registerFont + TTFont 使用说明
  3. ReportLab TableOfContents 介绍

如果觉得这篇文章对你有所帮助,欢迎点赞、收藏或关注,期待与你一起进步!
——完——

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐