提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


使用 PyInstaller 打包 Python 项目时,如何实现外部模型和配置文件的灵活调用:深入理解 PyInstaller 的 --collect-all--add-data 参数

引言

在 Python 项目部署过程中,PyInstaller 是一个常用的打包工具,它可以将 Python 脚本及其依赖打包成独立的可执行文件。然而,当项目需要调用本地的模型文件或配置文件时,直接打包往往会遇到路径识别和文件访问的问题。本文将通过一个实际案例,详细介绍如何解决这些问题。

问题背景

在我们的项目中,我们使用了 HanLP 自然语言处理库来处理中文文本,需要在离线环境中加载本地的分词模型和命名实体识别模型。同时,项目还需要读取外部的 JSON 配置文件。直接使用 PyInstaller 打包后,出现了以下问题:

  1. 模型文件无法找到:打包后的 exe 无法正确识别相对路径中的模型文件

  2. 依赖模块缺失:HanLP 的相关模块没有被正确打包在这里插入图片描述

  3. 配置文件无法更新:打包后的配置文件成为静态版本,无法动态更新

解决方案

首先说一下我的代码的目录结构:

项目目录/
├── 测试.py
├── config.json
├── hanlp_model/
│   ├── tok/
│   │   └── coarse_electra_small_20220616_012050/
│   └── ner/
│       └── msra_ner_electra_small_20220215_205503/
└──

“hanlp_model”文件夹下是需要调用的模型文件,config.json是配置文件

1. 资源路径处理

首先,我们需要创建一个资源路径处理函数,用于在开发和打包环境下都能正确访问文件:

import os
import sys

def resource_path(relative_path):
    """获取资源的绝对路径"""
    try:
        # PyInstaller 创建临时文件夹,将路径存储在 _MEIPASS 中
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    
    return os.path.join(base_path, relative_path)

2. 模型文件加载

针对模型文件的加载,我们需要根据是否打包来选择不同的路径:

# 根据是否打包选择模型路径
if getattr(sys, 'frozen', False):
    # 打包后的路径
    model_root = resource_path('hanlp_model')
else:
    # 开发时的路径
    model_root = 'hanlp_model'

# 模型路径
tokenizer_path = os.path.join(model_root, 'tok', 'coarse_electra_small_20220616_012050')
ner_path = os.path.join(model_root, 'ner', 'msra_ner_electra_small_20220215_205503')

# 全局加载HanLP模型
try:
    hanlp_pipeline = hanlp.pipeline() \
        .append(hanlp.load(tokenizer_path), output_key='tok') \
        .append(hanlp.load(ner_path), input_key='tok', output_key='ner')
    print("HanLP模型加载成功!")
except Exception as e:
    print(f"HanLP模型加载失败: {e}")
    hanlp_pipeline = None

3. 配置文件处理

对于配置文件,我们采用优先读取外部文件的策略:

def load_bank_config():
    """加载银行名称映射配置 - 优先读取外部配置文件"""
    try:
        # 确定配置文件路径
        if getattr(sys, 'frozen', False):
            config_path = os.path.join(os.path.dirname(sys.executable), 'config.json')
        else:
            config_path = 'config.json'
        
        print(f"配置文件路径: {config_path}")
        
        if os.path.exists(config_path):
            with open(config_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        else:
            # 创建默认配置文件
            default_config = {"中国工商银行": "ICBC", "工商银行": "ICBC"}
            with open(config_path, 'w', encoding='utf-8') as f:
                json.dump(default_config, f, ensure_ascii=False, indent=2)
            return default_config
            
    except Exception as e:
        print(f"读取配置文件时出错: {str(e)}")
        return {}

4. 完整的打包命令

使用正确的打包命令确保所有依赖都被包含:

pyinstaller -F --console ^
  --add-data "hanlp_model\tok\coarse_electra_small_20220616_012050;hanlp_model/tok/coarse_electra_small_20220616_012050" ^
  --add-data "hanlp_model\ner\msra_ner_electra_small_20220215_205503;hanlp_model/ner/msra_ner_electra_small_20220215_205503" ^
  --collect-all hanlp ^
  --collect-all transformers ^
  --collect-all torch ^
  测试.py

关键技术点

1. 路径处理机制

  • sys._MEIPASS:PyInstaller 创建的临时目录,包含所有打包的资源
  • sys.executable:获取 exe 文件的路径,用于定位同目录下的配置文件
  • getattr(sys, 'frozen', False):判断是否处于打包环境

2. 依赖管理

使用 --collect-all 参数自动收集所有依赖:

  • --collect-all hanlp:收集 HanLP 的所有模块
  • --collect-all transformers:收集 transformers 库的依赖
  • --collect-all torch:收集 PyTorch 的依赖

常见问题及解决方案

1. 模块缺失错误

错误信息

ModuleNotFoundError: Some modules required by this model are missing

解决方案

# 添加必要的 hidden imports
--hidden-import hanlp.components.tokenizers
--hidden-import hanlp.components.ner
--hidden-import hanlp.layers.transformers

但是试了多次发现仍然提示有一些模块没有导入,最后实在没办法,直接使用了“–collect-all”参数。

2. 文件权限问题

解决方案

def check_permissions(path):
    """检查路径是否有写入权限"""
    if not os.path.exists(path):
        path = os.path.dirname(path)
    return os.access(path, os.W_OK)

3. 配置文件更新问题

解决方案:优先读取 exe 同目录下的外部配置文件,而不是打包的内部文件。

最佳实践

  1. 分离代码和配置:将配置信息放在外部文件中,便于修改
  2. 提供默认配置:当外部配置文件不存在时,自动创建默认配置
  3. 详细的日志输出:在关键步骤添加日志,便于调试
  4. 错误处理机制:提供友好的错误提示和恢复方案
  5. 用户文档:说明配置文件的格式和放置位置

结论

通过合理的路径处理、依赖管理和文件结构设计,我们可以成功解决 PyInstaller 打包后无法调用本地模型文件和配置文件的问题。关键技术点包括:

  1. 使用 resource_path() 函数处理资源路径
  2. 优先读取外部配置文件以实现动态更新
  3. 使用 --collect-all 确保所有依赖被正确打包
  4. 提供完善的错误处理和用户提示

这种方案不仅适用于 HanLP 模型,也适用于其他需要加载外部资源的 Python 项目,具有很好的通用性和实用性。

附录

完整的打包命令示例

# Windows 系统
pyinstaller -F --console ^
  --add-data "hanlp_model\tok\coarse_electra_small_20220616_012050;hanlp_model/tok/coarse_electra_small_20220616_012050" ^
  --add-data "hanlp_model\ner\msra_ner_electra_small_20220215_205503;hanlp_model/ner/msra_ner_electra_small_20220215_205503" ^
  --collect-all hanlp ^
  --collect-all transformers ^
  --collect-all torch ^
  测试.py

# Linux/macOS 系统
pyinstaller -F --console \
  --add-data "hanlp_model/tok/coarse_electra_small_20220616_012050:hanlp_model/tok/coarse_electra_small_20220616_012050" \
  --add-data "hanlp_model/ner/msra_ner_electra_small_20220215_205503:hanlp_model/ner/msra_ner_electra_small_20220215_205503" \
  --collect-all hanlp \
  --collect-all transformers \
  --collect-all torch \
  测试.py

深入理解 PyInstaller 的 --collect-all--add-data 参数

--add-data 参数详解
作用与用途

--add-data 参数用于将非 Python 文件(如配置文件、模型文件、图像、数据文件等)添加到打包后的可执行文件中。这些文件在运行时可以被程序访问,但对于 Python 的导入系统来说是不可见的。

语法格式
--add-data "<源路径>;<目标路径>"   # Windows
--add-data "<源路径>:<目标路径>"   # Linux/macOS
参数说明
  • 源路径:本地文件系统中的文件或目录路径
  • 目标路径:在打包后的 exe 中的相对路径
  • 分隔符:Windows 使用 ;,Unix 系统使用 :
使用示例
1. 添加单个文件
# 添加配置文件到根目录
--add-data "config.json;."

# 添加配置文件到指定目录
--add-data "config.json;configs"

# 添加图像文件
--add-data "images/logo.png;images"
2. 添加整个目录
# 添加模型目录
--add-data "hanlp_model;hanlp_model"

# 添加数据目录
--add-data "data;data"

# 添加模板目录
--add-data "templates;templates"
3. 多个文件添加
# 添加多个文件
--add-data "config.json;." --add-data "settings.ini;." --add-data "README.md;."

# 添加多个目录
--add-data "models;models" --add-data "data;data" --add-data "static;static"
在代码中访问添加的文件

使用 sys._MEIPASS 来访问打包后的资源文件:

import os
import sys

def resource_path(relative_path):
    """获取打包资源的绝对路径"""
    try:
        base_path = sys._MEIPASS  # PyInstaller 创建的临时目录
    except Exception:
        base_path = os.path.abspath(".")
    
    return os.path.join(base_path, relative_path)

# 访问打包的文件
config_path = resource_path('config.json')
model_path = resource_path('hanlp_model/tok/model.bin')
注意事项
  1. 路径分隔符:注意 Windows 和 Unix 系统的区别
  2. 相对路径:源路径相对于当前工作目录
  3. 文件更新:打包后的文件是静态的,无法直接修改
  4. 文件大小:大文件会增加 exe 的体积和启动时间
--collect-all 参数详解
作用与用途

--collect-all 参数用于自动收集指定包的所有相关文件,包括:

  • Python 模块和子包
  • 包内的数据文件(如 .json, .txt, .pth 等)
  • 包所需的共享库和扩展模块
  • 包的元数据和配置文件
语法格式
--collect-all <包名>
使用示例
1. 收集常用科学计算包
# 收集深度学习相关包
--collect-all torch
--collect-all transformers
--collect-all tensorflow

# 收集科学计算包
--collect-all numpy
--collect-all pandas
--collect-all scipy
--collect-all sklearn
2. 收集数据处理包
# 收集数据处理包
--collect-all pandas
--collect-all openpyxl
--collect-all xlrd
--collect-all sqlalchemy
3. 收集 Web 相关包
# 收集 Web 框架包
--collect-all flask
--collect-all django
--collect-all requests
--collect-all beautifulsoup4
工作原理

当使用 --collect-all 时,PyInstaller 会:

  1. 递归收集:收集指定包及其所有子包
  2. 包含数据文件:包含包内的所有数据文件
  3. 包含二进制依赖:包含包所需的共享库和扩展
  4. 包含元数据:包含包的版本信息等元数据
--hidden-import 的区别
特性--collect-all--hidden-import
作用范围整个包及其所有内容单个模块
包含内容模块 + 数据文件 + 依赖仅模块
使用场景复杂包、有数据文件的包简单模块、动态导入的模块
打包体积较大较小
实际案例:打包 HanLP 项目
# 使用 --collect-all 收集所有相关包
--collect-all hanlp           # 主包
--collect-all transformers    # 依赖的 transformers 库
--collect-all torch           # 依赖的 PyTorch
--collect-all tokenizers      # 分词器相关
--collect-all datasets        # 数据集相关


通过本文介绍的方法,您可以顺利解决 PyInstaller 打包中的外部资源调用问题,实现更加灵活和可维护的项目部署。
Logo

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

更多推荐