assets/2026-07-01-医数智联系列开篇-医院数字化工作者的工具箱_2026-07-01_18-28-11.jpg

医数智联·第 20 期:Python + 质控报告 PDF 自动生成 —— 一键生成院长周报,告别 Word 排版

[!ABSTRACT] 核心摘要

  • 本期主题:Python + reportlab 自动生成 PDF 质控报告
  • 核心结论:报告 PDF 不再用 Word 排,1 行代码搞定,数据更新自动重出
  • 3 大核心库:reportlab(PDF 生成)+ matplotlib(图表)+ pandas(数据)
  • 3 大医院场景:基础报告、含图表报告、完整院长周报
  • 典型效果:从「半天排版 Word」变成「5 秒生成 PDF」
  • 阅读时长:约 10 分钟

一、场景引入:为什么医院需要「PDF 自动生成」?

[!example] 一个真实的周一早晨
质管办小李每周一上午要给院长交一份「上周质控周报」。

他的工作流程:

  1. 跑 SQL 拿数据 → 半小时
  2. 复制到 Excel 排版 → 半小时
  3. 截图插入 Word → 半小时
  4. 调整字号、标题、页眉页脚 → 半小时
  5. 导出 PDF → 5 分钟

2 小时过去了,周报终于交出。

但院长:「上周数据有更新,重做一遍。」

2 小时又过去了

这是医院周报的典型困境——「数据 + 排版」分离,改数据就要全部重做

现状 痛点
Word 排版 改数据要重新排版
截图插图 图片模糊,不能交互
手动复制 易错、效率低
一次性输出 无法复用,每月重做

[!quote] 狼叔的判断
“Word 是给’人’用的,Python 是给’流程’用的。” 这一期,带你用 Python 一键生成 PDF 报告,数据更新 + 一键重出


二、工具原理:reportlab + matplotlib 是什么?

2.1 reportlab 是什么?

[!info] reportlab 简介
reportlab 是 Python 最强大的 PDF 生成库,支持文字、表格、图表、图像、模板

江湖地位:

  • Python PDF 生成 #1 库(90% 场景)
  • 银行对账单、保险单、医院报告的「标配」

2.2 核心类

1
2
3
4
5
6
SimpleDocTemplate     → PDF 文档对象
Paragraph → 段落
Table / TableStyle → 表格
Image → 图像
Spacer → 空白
PageBreak → 换页

2.3 reportlab vs Word 对照

操作 Word reportlab
文档对象 .docx SimpleDocTemplate
段落 输入文字 Paragraph
表格 插入表格 Table + TableStyle
字体 选字体 setFont('SimSun', 12)
颜色 选颜色 setFont('SimSun', 12, color='red')
页眉页脚 复杂 PageTemplate
图片 插入图片 Image

三、医院实战:3 大场景

3.1 安装与准备

1
pip install reportlab matplotlib pandas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Table, TableStyle,
Image, Spacer, PageBreak
)
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import matplotlib.pyplot as plt
import pandas as pd

# 注册中文字体
pdfmetrics.registerFont(TTFont('SimHei', 'simhei.ttf'))
pdfmetrics.registerFont(TTFont('SimSun', 'simsun.ttc'))

# 中文字体样式
styles = getSampleStyleSheet()
中文样式 = ParagraphStyle(
'Chinese',
parent=styles['Normal'],
fontName='SimHei',
fontSize=12,
leading=18
)
标题样式 = ParagraphStyle(
'Title',
parent=styles['Title'],
fontName='SimHei',
fontSize=20,
alignment=1 # 居中
)

3.2 场景一:基础 PDF 报告

[!example] 实战 ①:基础报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def 生成基础报告():
"""生成最基础的 PDF"""
doc = SimpleDocTemplate('基础报告.pdf', pagesize=A4)
故事 = [] # reportlab 用「故事」列表构建文档

# 标题
故事.append(Paragraph('本月质控简报', 标题样式))
故事.append(Spacer(1, 1 * cm))

# 段落
故事.append(Paragraph(
'本月出院病案共 3250 份,其中主诊错误 75 例,错误率 2.31%,'
'较上月下降 0.4 个百分点,较去年同期下降 1.2 个百分点。',
中文样式
))
故事.append(Spacer(1, 0.5 * cm))

# 段落
故事.append(Paragraph(
'骨科、神经科、消化科位列错误数 Top 3,'
'占全部错误的 73.3%。',
中文样式
))
故事.append(Spacer(1, 0.5 * cm))

# 简单表格
数据 = [
['科室', '总病案数', '错误数', '错误率'],
['骨科', '280', '28', '10.0%'],
['神经科', '195', '15', '7.7%'],
['消化科', '220', '12', '5.5%'],
['心内科', '185', '8', '4.3%']
]

表 = Table(数据, colWidths=[3 * cm, 3 * cm, 3 * cm, 3 * cm])
表.setStyle(TableStyle([
('FONT', (0, 0), (-1, -1), 'SimHei', 11),
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
]))
故事.append(表)

# 生成 PDF
doc.build(故事)
print('✓ 基础报告.pdf 已生成')

生成基础报告()

3.3 场景二:含图表的 PDF 报告

[!example] 实战 ②:含图表报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def 生成图表(数据, 输出图片):
"""用 matplotlib 生成图表,返回图片路径"""
fig, ax = plt.subplots(figsize=(8, 4))

科室 = [行[0] forin 数据[1:]]
错误率 = [float(行[3].rstrip('%')) forin 数据[1:]]

ax.bar(科室, 错误率, color='steelblue', edgecolor='black')
ax.set_title('各科室主诊错误率', fontsize=14, fontweight='bold')
ax.set_xlabel('科室', fontsize=12)
ax.set_ylabel('错误率(%)', fontsize=12)

plt.tight_layout()
plt.savefig(输出图片, dpi=200, bbox_inches='tight')
plt.close()
return 输出图片

def 生成含图表报告():
"""生成含图表的 PDF"""
doc = SimpleDocTemplate('含图表报告.pdf', pagesize=A4)
故事 = []

故事.append(Paragraph('本月质控分析报告', 标题样式))
故事.append(Spacer(1, 1 * cm))

# 数据
数据 = [
['科室', '总病案数', '错误数', '错误率'],
['骨科', '280', '28', '10.0%'],
['神经科', '195', '15', '7.7%'],
['消化科', '220', '12', '5.5%'],
['心内科', '185', '8', '4.3%']
]

# 1. 表格
表 = Table(数据, colWidths=[3 * cm, 3 * cm, 3 * cm, 3 * cm])
表.setStyle(TableStyle([
('FONT', (0, 0), (-1, -1), 'SimHei', 11),
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
]))
故事.append(表)
故事.append(Spacer(1, 0.5 * cm))

# 2. 图表(先用 matplotlib 生成,再嵌入)
图片路径 = 生成图表(数据, '本月错误率.png')
故事.append(Image(图片路径, width=14 * cm, height=7 * cm))
故事.append(Spacer(1, 0.5 * cm))

# 3. 文字解读
故事.append(Paragraph(
'<b>分析:</b>骨科错误率最高(10.0%),主要原因是术后感染诊断编码不准确。'
'建议加强骨科编码员培训和主诊选择培训。',
中文样式
))

doc.build(故事)
print('✓ 含图表报告.pdf 已生成')

生成含图表报告()

3.4 场景三:完整院长周报(终极实战)

[!example] 实战 ③:完整院长周报
包含:封面 + 摘要 + 多张图表 + 多张表格 + 分页 + 页眉页脚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def 生成完整院长周报(首页数据, 错误数据, 趋势数据):
"""完整院长周报"""
doc = SimpleDocTemplate(
'院长周报.pdf',
pagesize=A4,
topMargin=2 * cm,
bottomMargin=2 * cm,
leftMargin=2 * cm,
rightMargin=2 * cm
)
故事 = []

# ============ 封面 ============
故事.append(Spacer(1, 5 * cm))
故事.append(Paragraph('医院质量管理', 标题样式))
故事.append(Spacer(1, 1 * cm))
故事.append(Paragraph('每周质控报告', 标题样式))
故事.append(Spacer(1, 2 * cm))
故事.append(Paragraph(
f'报告日期:2026 年 7 月 3 日',
ParagraphStyle('Subtitle', fontName='SimHei', fontSize=14, alignment=1)
))
故事.append(PageBreak())

# ============ 摘要 ============
故事.append(Paragraph('一、本周核心指标', 标题样式))
故事.append(Spacer(1, 0.5 * cm))

# 关键指标卡片
指标数据 = [
['指标', '本周', '上周', '同比'],
['出院病案数', '825', '790', '+4.4%'],
['主诊错误数', '18', '21', '-14.3%'],
['主诊错误率', '2.18%', '2.66%', '-0.48pp'],
['DRG 入组异常', '12', '15', '-20.0%']
]

表 = Table(指标数据, colWidths=[5 * cm, 3 * cm, 3 * cm, 3 * cm])
表.setStyle(TableStyle([
('FONT', (0, 0), (-1, -1), 'SimHei', 11),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1e7e34')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
('FONTNAME', (0, 0), (-1, 0), 'SimHei'),
('FONTSIZE', (0, 0), (-1, 0), 12)
]))
故事.append(表)
故事.append(PageBreak())

# ============ 错误类型分析 ============
故事.append(Paragraph('二、错误类型分析', 标题样式))
故事.append(Spacer(1, 0.5 * cm))

# 饼图
fig, ax = plt.subplots(figsize=(8, 6))
错误类型 = [行['类型'] forin 错误数据]
错误数 = [行['数'] forin 错误数据]
ax.pie(错误数, labels=错误类型, autopct='%1.1f%%',
colors=['#ff6b6b', '#4ecdc4', '#45b7d1', '#ffd93d'])
ax.set_title('错误类型占比', fontsize=14, fontweight='bold')
plt.savefig('错误类型.png', dpi=200, bbox_inches='tight')
plt.close()

故事.append(Image('错误类型.png', width=12 * cm, height=9 * cm))
故事.append(PageBreak())

# ============ 趋势分析 ============
故事.append(Paragraph('三、月度趋势', 标题样式))
故事.append(Spacer(1, 0.5 * cm))

# 折线图
fig, ax = plt.subplots(figsize=(10, 5))
月份 = [行['月'] forin 趋势数据]
本月错误率 = [行['本月'] forin 趋势数据]
去年错误率 = [行['去年'] forin 趋势数据]
ax.plot(月份, 本月错误率, marker='o', label='2026', linewidth=2)
ax.plot(月份, 去年错误率, marker='s', label='2025',
linewidth=2, linestyle='--')
ax.set_title('主诊错误率月度趋势', fontsize=14, fontweight='bold')
ax.set_xlabel('月份')
ax.set_ylabel('错误率(%)')
ax.legend()
ax.grid(True, linestyle='--', alpha=0.5)
plt.savefig('趋势.png', dpi=200, bbox_inches='tight')
plt.close()

故事.append(Image('趋势.png', width=14 * cm, height=7 * cm))
故事.append(PageBreak())

# ============ Top 医生 ============
故事.append(Paragraph('四、本周 Top 错误医生', 标题样式))
故事.append(Spacer(1, 0.5 * cm))

Top医生 = [
['医生', '科室', '错误数', '主要错误类型'],
['李医生', '骨科', '8', '主诊未选主要诊断'],
['张医生', '骨科', '5', 'ICD 与性别不符'],
['王医生', '神经科', '3', '主诊未选主要诊断'],
['陈医生', '消化科', '2', '手术 ICD 错误']
]

2 = Table(Top医生, colWidths=[3 * cm, 3 * cm, 3 * cm, 6 * cm])
2.setStyle(TableStyle([
('FONT', (0, 0), (-1, -1), 'SimHei', 11),
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1e7e34')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('GRID', (0, 0), (-1, -1), 0.5, colors.grey)
]))
故事.append(表2)
故事.append(PageBreak())

# ============ 整改建议 ============
故事.append(Paragraph('五、本周整改建议', 标题样式))
故事.append(Spacer(1, 0.5 * cm))

建议列表 = [
'1. 骨科:加强「术后感染」编码培训,本周内组织 1 次专项培训',
'2. 神经科:复盘「脑梗死」主诊选择流程,确保选主要诊断',
'3. 全院:启动「DRG 入组异常」专项整改,目标下周下降 30%',
'4. 信息科:完善首页校验脚本,自动拦截明显错误'
]

for 建议 in 建议列表:
故事.append(Paragraph(建议, 中文样式))
故事.append(Spacer(1, 0.3 * cm))

# 生成 PDF
doc.build(故事)
print('✓ 院长周报.pdf 已生成')

# 运行
首页数据 = pd.read_excel('本月首页.xlsx')
错误数据 = [
{'类型': '主诊未选', '数': 45},
{'类型': 'ICD不符', '数': 18},
{'类型': '手术ICD', '数': 8},
{'类型': '其他', '数': 4}
]
趋势数据 = [
{'月': '1月', '本月': 3.2, '去年': 4.5},
{'月': '2月', '本月': 2.8, '去年': 4.2},
{'月': '3月', '本月': 3.5, '去年': 4.0},
# ...
]

生成完整院长周报(首页数据, 错误数据, 趋势数据)

四、避坑指南:5 个让 reportlab「失败」的常见错误

[!warning] 雷区 ①:中文显示为方块
症状:PDF 里中文是「□□□」。
解决:注册中文字体 + 在 Paragraph 里指定 fontName:

1
2
pdfmetrics.registerFont(TTFont('SimHei', 'simhei.ttf'))
Paragraph('中文', ParagraphStyle('S', fontName='SimHei'))

[!warning] 雷区 ②:表格列宽不够,内容换行乱
症状:表格文字「主诊错误率」被截断成「主诊错」。
解决:用 Paragraph 包裹表格单元格:

1
2
from reportlab.platypus import Paragraph
表 = Table([[Paragraph('主诊错误率', 中文样式), ...]])

[!warning] 雷区 ③:图片太大,PDF 卡顿
症状:PDF 几十 MB,打开慢。
解决:DPI 200 就够,不要 600:

1
plt.savefig('图.png', dpi=200)

[!warning] 雷区 ④:页眉页脚不显示
症状:首页/末页没页眉页脚。
解决:onFirstPageonLaterPages:

1
2
3
4
5
6
def 页眉(canvas, doc):
canvas.saveState()
canvas.setFont('SimHei', 9)
canvas.drawString(2 * cm, 29 * cm, '医院质量管理 · 周报')
canvas.restoreState()
doc.build(故事, onFirstPage=页眉, onLaterPages=页眉)

[!warning] 雷区 ⑤:matplotlib 图没关,内存泄漏
症状:生成 100 张图后,内存占满。
解决:每张图加 plt.close():

1
2
plt.savefig('图.png')
plt.close() # 关键!

五、把 PDF 生成嵌入自动化

[!TIP] 完整工作流

  1. 每周一 02:00(任务计划程序):脚本自动跑
  2. 从 SQL 读数据(承第 09 期)
  3. 生成图表(承第 16 期)
  4. 生成 PDF(本期)
  5. 自动发邮件给院长 + 科主任

效果:周一早上院长醒来,周报已经在邮箱


六、下期预告

[!quote] 第 21 期
Python 综合:首页质控全流程自动化 —— 一个脚本搞定所有

这一期,把第 14-20 期的所有 Python 技能串起来,搭建一个完整的「首页质控自动化流水线」。


[!TIP] 互动
如果你觉得这篇「医数智联·第 20 期」对你有用,请点赞、在看、转发给科里的同事。

文中 PDF 生成脚本、模板、字体包,我已经整理成「PDF 自动生成工具包」。点击「阅读原文」可直接下载

我是白衣狼,一个用数字化工具把医院管理做「轻」的实战派。咱们下期见。