资讯动态

PyInstaller 打包后模板预览内容大面积缺失:一个 import 作用域引发的问题

发布时间:2026/9/30 19:38:23 来源:尧图企业网站定制
PyInstaller 打包后模板预览内容大面积缺失一个 import 作用域引发的问题前言最近项目上线前遇到了一个问题基于 PySide6 开发的档案管理系统源码运行一切正常打包后的程序在开发机上也没问题但拷贝到单位办公电脑银河麒麟 V10上运行后目录模板预览页面内容大面积缺失——案卷目录只有标题没有表格、案卷封面只有档号和题名没有底部字段、案卷脊背只有一个空框、卷内目录同样只有标题。这个问题折腾了整整一天经历了四轮修复才最终解决。本文完整记录排查过程和真正的根因希望能帮到踩同样坑的开发者。技术栈Python 3.10 PySide6 6.6.3 PyInstaller 6.22.0 SQLite。一、问题现象打包后的程序在目标机器上的表现模板页面预期显示实际显示案卷目录标题 7列表格只有标题案卷目录案卷封面档号 题名框 4个底部字段只有档号和题名框案卷脊背6段分区保管期限/档号/题名等只有一个空矩形框卷内目录标题 7列表格只有标题卷内目录关键特征标题能显示但表格列、行数据、分段配置全部缺失。同时登录时还弹出一个错误框“加载字典失败locking protocol”。二、排查过程2.1 第一轮SQLite WAL 模式locking protocol“locking protocol” 是 SQLite 的经典错误。查看代码发现之前的综合审查修复中在get_db_connection()里给每个连接都加了PRAGMA journal_mode WALdefget_db_connection():connsqlite3.connect(DB_PATH,timeout30)conn.row_factorysqlite3.Row conn.execute(PRAGMA foreign_keys ON)conn.execute(PRAGMA journal_mode WAL)# ← 每次连接都执行returnconn目标机器的文件系统不支持 WAL可能是 NFS 或特殊挂载每次打开连接都报错。修复把 WAL 设置移到init_db()中只执行一次加try/except回退到默认 DELETE 模式。defget_db_connection():connsqlite3.connect(DB_PATH,timeout30)conn.row_factorysqlite3.Row conn.execute(PRAGMA foreign_keys ON)returnconn# 不再设置 WALdefinit_db():# ...try:cursor.execute(PRAGMA journal_mode WAL)exceptException:pass# 不支持 WAL 时回退到默认模式这一轮解决了加载字典失败的问题但模板预览内容缺失依旧。2.2 第二轮字体回退猜测中文字体缺失标题能显示但内容缺失我第二反应是字体问题——目标麒麟系统可能没有安装宋体Qt 的QFont(宋体)找不到字体会静默回退到默认字体而默认字体可能不支持中文。修复在预览字体构造函数中加了QFontDatabase检测和回退链classmethoddef_make_preview_font(cls,family,pt,scale,boldFalse):fromPySide6.QtGuiimportQFontDatabase familyfamilyor宋体dbQFontDatabase()iffamilynotindb.families():forfallbackin(Noto Serif CJK SC,Noto Sans CJK SC,WenQuanYi Zen Hei,WenQuanYi Micro Hei,SimSun,SimHei,sans-serif):iffallbackindb.families():familyfallbackbreakfQFont(family)f.setPixelSize(max(1,int(round(float(pt)*cls._PT_TO_MM*scale))))f.setBold(bool(bold))returnf结果问题依旧。现在回头看这个判断本身就是错的——如果字体问题标题也不应该显示。标题能显示恰恰说明字体没问题。2.3 第三轮PyInstaller 模块遗漏collect_submodules第三轮我怀疑是 PyInstaller 打包时遗漏了业务模块。代码中大量使用函数内延迟导入definit_db():# ...ifcursor.fetchone()[0]0:fromsrc.services.archive_template_defaultsimportget_default_archive_catalog_template# ↑ PyInstaller 静态分析扫不到函数内的 import检查打包产物确实在 PYZ 归档中找不到archive_template_defaults模块。修复在 spec 文件中用collect_submodules全量收集业务模块fromPyInstaller.utils.hooksimportcollect_submodules _src_hiddencollect_submodules(src)hiddenimports_src_hiddenprint(f[spec] collect_submodules(src):{len(_src_hidden)}个模块)打包日志显示collect_submodules(src): 26 个模块PYZ TOC 也确认所有关键模块都在包中。结果问题依然存在。用户反馈还是和之前一样。2.4 第四轮真正的根因——import 作用域 bug到这里我停下来重新审视问题。打包后的程序在开发机上完全正常说明模块确实已经打包进去了。问题只出现在目标机器上说明是运行时环境差异导致的。关键线索目标机器上 DB 已经存在之前几轮运行创建的而开发机上 DB 是源码运行时创建的完整数据。重新审视init_db()的模板初始化逻辑cursor.execute(SELECT COUNT(*) FROM sys_template)ifcursor.fetchone()[0]0:# ★ import 在 if 分支内fromsrc.services.archive_template_defaultsimportget_default_archive_catalog_template default_template_contentget_default_archive_catalog_template()cursor.execute(INSERT INTO sys_template ...,(...))else:# DB 已存在走迁移路径cursor.execute(SELECT template_content FROM sys_template WHERE template_codearchive_catalog)rowcursor.fetchone()ifrow:try:old_contentjson.loads(row[0])ifrow[0]else{}ifpage_settingsinold_contentorrowsnotinold_content.get(volume_cover,{}):# ★ 这里调用 get_default_archive_catalog_template()# 但 import 在 if 分支内else 分支中函数名未绑定new_contentget_default_archive_catalog_template()# ← NameError!cursor.execute(UPDATE sys_template SET template_content? ...,...)except(json.JSONDecodeError,Exception):pass# ← NameError 被静默吞掉这就是根因。Python 的from X import Y是运行时语句不是编译期声明。当import在if分支内时Y只在if分支被执行时才绑定到本地作用域。如果走else分支Y从未被绑定调用时抛出NameError。而NameError被外层的except (json.JSONDecodeError, Exception): pass静默吞掉不留任何痕迹。完整的问题链目标机器首次运行旧版本时archive_template_defaults模块未打包init_db()中 import 失败模板未正确插入或插入了空内容后续运行修复后的版本时DB 已存在 → 走else分支迁移代码检测到模板内容残缺rows不存在尝试调用get_default_archive_catalog_template()修复但import在if分支内else分支中函数名未绑定 →NameErrorexcept Exception: pass静默吞掉错误残缺模板永远无法修复预览渲染时cfg.get(rows, [])返回空列表cfg.get(columns, [])返回空列表只有硬编码的标题文字能显示所有依赖配置数据的表格/列/分段全部缺失为什么开发机正常开发机的 DB 是源码运行时创建的模板内容完整根本不走else迁移路径。三、修复方案3.1 核心import 提到 if/else 之前definit_db():# ...# ★ import 移到 if/else 之前两个分支都能使用fromsrc.services.archive_template_defaultsimportget_default_archive_catalog_template cursor.execute(SELECT COUNT(*) FROM sys_template)ifcursor.fetchone()[0]0:default_template_contentget_default_archive_catalog_template()cursor.execute(INSERT INTO sys_template ...,(...))else:# 迁移代码中调用 get_default_archive_catalog_template() 不再报 NameError...3.2 加强迁移验证原来只检查page_settings和volume_cover.rows现在增加对所有关键配置段的检查_need_replaceFalseifpage_settingsinold_content:_need_replaceTrue# 检查所有关键配置段是否存在且非空vcold_content.get(volume_cover,{})ifrowsnotinvcornotvc.get(rows):_need_replaceTruevsold_content.get(volume_spine,{})ifsectionsnotinvsornotvs.get(sections):_need_replaceTruefcold_content.get(file_catalog,{})ifcolumnsnotinfcornotfc.get(columns):_need_replaceTruecoold_content.get(catalog_of_files,{})ifcolumnsnotincoornotco.get(columns):_need_replaceTrueif_need_replace:new_contentget_default_archive_catalog_template()cursor.execute(UPDATE sys_template SET template_content? ...,...)3.3 消除静默异常把except: pass改为带日志输出except(json.JSONDecodeError,Exception)ase:logging.warning(farchive_catalog 模板迁移失败:{e})同时在模板编辑器的加载逻辑中也加了回退iftemplate.get(template_content):try:contentjson.loads(template[template_content])self._load_template_to_tabs(content)exceptExceptionase:logging.warning(f加载模板内容失败使用默认模板:{e})fromsrc.services.archive_export_serviceimportArchiveExportService defaultArchiveExportService._get_default_template()self._load_template_to_tabs(default)四、验证修复后打包拷贝到目标机器运行。程序启动时迁移代码检测到旧模板残缺自动替换为完整默认模板日志输出INFO: archive_catalog 模板内容残缺已替换为默认模板打开模板编辑页面所有预览页面完整渲染模板页面修复前修复后案卷目录只有标题标题 7列完整表格案卷封面只有档号和题名全部9行内容完整显示案卷脊背只有空框6段分区全部显示卷内目录只有标题标题 7列完整表格五、经验总结5.1 PyInstaller 打包的三层防御这次踩坑说明 PyInstaller 的模块收集需要三层防御层级手段作用第一层PyInstaller 静态分析自动检测模块级 import第二层hiddenimports显式声明补充第三方库延迟导入第三层collect_submodules(src)全量收集项目内所有子模块只有三层都到位才能确保延迟导入的业务模块不遗漏。5.2 Python import 作用域陷阱from X import Y是运行时语句不是编译期声明。它在哪个分支被执行就只在哪个分支绑定名字。这个特性在纯 Python 环境下很少出问题因为通常会被再次执行到但在 PyInstaller 打包 DB 持久化的场景下if分支只在首次运行时执行后续都走else导致else中的调用永远NameError。最佳实践函数内延迟导入应放在函数体顶部不要放在 if/else 分支内。5.3except: pass是定时炸弹except Exception: pass是 Python 中最危险的代码模式之一。它会让任何错误——包括你完全没预料到的NameError、ImportError、AttributeError——都静默消失让排查变得极其困难。这次问题排查了四轮才解决根本原因就是except: pass吞掉了NameError让迁移代码看起来执行了但什么都没做。最佳实践即使确实需要忽略某些异常也应该至少记录日志exceptExceptionase:logging.warning(f操作失败:{e})5.4 开发机与目标环境的关键差异打包后的程序在开发机上正常并不等于在目标机器上正常。这次问题的关键差异是开发机DB 由源码运行时创建模板内容完整不走迁移路径目标机器DB 由旧版本创建模板内容残缺走迁移路径但迁移失败这种开发机掩盖问题的现象在持久化数据相关的 Bug 中非常常见。测试打包版本时应该用全新的环境删除 DB 和配置文件进行测试而不是复用开发机的数据。六、避坑清单collect_submodules(src)收集项目模块PyInstaller 静态分析扫不到函数内的from src.xxx import yyy必须用collect_submodules全量收集延迟导入放在函数体顶部不要放在 if/else/for/try 分支内避免作用域问题except: pass改为except Exception as e: logging.warning(...)即使要忽略异常也要留痕迹迁移代码要验证关键数据不能只检查格式版本还要检查rows/columns/sections等关键配置段是否非空打包测试用全新环境删除 DB 和配置文件后再测试模拟首次部署场景WAL 模式不要在每个连接中设置SQLite 的PRAGMA journal_mode WAL应在init_db()中只执行一次且加异常回退字体检测不要只看标题能不能显示标题用硬编码默认值配置数据缺失时标题仍能显示容易误判为字体问题七、相关文章01 架构设计与模板配置02 QPainter 预览渲染03 python-docx 导出实现04 项目周期三位一体实践05 PDF 页号叠加与图形状态继承06 页码编号系统全流程实践

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑