核心难点是跨库数据整合而非PDF生成;需用pandas.read_sql统一入口,分库建引擎并显式释放连接,明确字段名、统一时间类型、校验dtype与数据一致性,模板仅渲染HTML结构,禁用字体回退并显式指定中文字体路径。
用 pandas.read_sql 统一拉取多库多表数据
跨库导出 pdf 的核心难点不在 pdf 生成,而在数据整合——不同数据库(比如 mysql + postgresql)的连接、权限、时区、字符集都可能不一致,硬写多个 cursor.execute 容易散乱出错。pandas.read_sql 是最稳妥的入口,它能屏蔽底层驱动差异,返回统一的 dataframe。
实操建议:
- 每个库单独建
SQLAlchemy引擎,用engine.dispose()显式释放连接,避免连接池耗尽 - SQL 查询里避免写
SELECT *,必须明确字段名,防止跨表同名字段覆盖(如两个库都有id字段) - 时间字段统一用
pd.to_datetime()转为datetime64[ns],否则 PDF 表格里可能显示为Timestamp('2024-01-01 00:00:00')这种原始字符串 - 如果某库不支持
read_sql(如老版本 SQLite),改用pd.DataFrame(cursor.fetchall(), columns=[...]),但务必手动传columns
PDF 模板中用 jinja2 渲染结构,不硬编码数据
结构与数据分离的关键是:PDF 模板只定义「长什么样」,不定义「填什么」。直接用 reportlab 手动拼坐标位置,等于把表格结构写死在 Python 代码里,后续改列宽、加合计行就得重写逻辑。
实操建议:
- 用
jinja2写 HTML 模板(不是纯文本),再通过weasyprint或pdfkit转 PDF——HTML 天然支持 CSS 排版、分页、页眉页脚 - 模板里只出现
{{ df.to_html(index=False, escape=False) }}这类占位,不要写{{ df['name'][0] }}这种具体索引,否则数据一变就报KeyError - 禁止在模板里做计算(如
{{ row.a + row.b }}),所有聚合、格式化(金额加千分位、日期转中文)必须在 Python 层做完再传入render() -
weasyprint对 CSS 支持较好,但不支持position: fixed;pdfkit依赖本地 Chrome,适合复杂交互,但 Docker 环境需额外装chromium
导出前用 df.equals() 和 df.dtypes 校验数据一致性
跨库合并后,看似字段名一样,实际可能是 object vs string、int64 vs float64,导致 PDF 表格里同一列出现「123」和「123.0」混排,或者排序错乱。
实操建议:
- 合并前对每个子
DataFrame执行df.dtypes,重点关注数值列是否意外转成object(常见于空值混入字符串) - 用
df.fillna('')替换空值,别用df.fillna(0)——否则字符串列会变成0,PDF 里显示异常 - 合并后立即跑
df.equals(other_df)对比预期结果(可存一份 CSV 当 baseline),而不是等 PDF 出来才发现漏了某库数据 - 如果某库字段少几列,用
df.reindex(columns=all_columns, fill_value='')对齐,别靠pd.concat(..., join='outer'),它默认用NaN填充,PDF 里会显示nan
生成 PDF 时禁用 weasyprint 的默认字体回退
weasyprint 在找不到中文字体时会静默回退到英文 fallback 字体,结果 PDF 里中文全变成方框或乱码,且不报错——这是最隐蔽的坑。
实操建议:
- 显式指定中文字体路径:
HTML(string=html).write_pdf(target, font_config=font_config),其中font_config必须提前加载simhei.ttf或NotoSansCJKsc-Regular.otf - Docker 部署时,字体文件要 COPY 到镜像内,并在 Python 里用
os.path.join(os.getcwd(), 'fonts/simhei.ttf')绝对路径引用,不能用相对路径 - 测试阶段加一行
print(font_config.get_font_families()),确认中文字体名出现在列表里,否则后面全是白忙活 - 别信系统自带的
WenQuanYi Zen Hei,某些 Alpine 镜像里名字其实是WenQuanYi Zen Hei Sharp,少一个词就加载失败
结构和数据真正分离的标志,不是模板文件独立出来,而是任意改一个字段名、删一列、换数据库,都不需要碰 PDF 生成代码本身——只改 SQL 和模板里那一行 {{ df.to_html(...) }} 就够了。其余全是校验和兜底的事。










