pyinstaller打包多python文件项目时常见问题及报错(绝对路径问题,动态导入机制问题,命令行参数报错问题)

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


前言


pyinstaller其实有两种打包方法,一种是将资源文件嵌入到EXE中,只留下单个的EXE文件,另一种则是不将资源文件嵌入,后者操作方便,实现难度小,但是实际意义却没有多大,所以我只会对前者进行问题讲解

先我们要清楚将资源文件嵌入到可执行文件中的目的是什么,无非就是使得项目代码在没有暴露的情况下,可以便捷的提供项目的功能给需求者使用,所以我们的目的有两个,一是使得项目具有密封性,二是使得项目具有可移植性,我参考过网上很多博主写的相关文章,可以说绝大部分,或者说我看过的所有文章,都没有意识到,甚至是直接忽略了可移植性,基本上都是把重心放在了教读者怎么把项目打包出来,使之在自己的电脑或者服务器上正常运行,而他们忽略的可移植性,往往才是在实战中会出现且最为致命的问题,当然除了可移植性之外,其实还存在着很多网上绝大部分文章都没有提到过的问题,为什么没有我就不做过多的评价,这里,我花了将近几个礼拜的实战时间,深入的了解了怎么将python项目真正的打包,以及整理了在打包过程中需要注意的问题

为了直接切入主题,了解打包的大致流程以及必要的知识,请参考笔者的上一篇文章
单多python文件打包成exe可执行文件

一、因绝对路径而产生的不可移植问题

1.问题解析

这就是上文提到的可移植性问题,当我们代码中存在必要的绝对路径时,这个问题就会凸显出来
很多人在打包后,明明在自己的电脑上能正常运行,但是移植到另一台电脑时,就会显示exe找不到各种文件

那么为什么会出现这个问题,首先,要明白exe运行的逻辑,当我们点击exe文件时,该文件会在我们电脑的C盘,或者是服务器的根目录下,生成 C:\Users\当前用户\AppData\Local\Temp/xxxxx 以及 /tmp/xxxxx (其中xxxxx是随机的)的这个临时目录,而exe文件要引用的所有的库,以及所有的资源文件,都会释放在这个临时目录中,已支持exe正常运作,那么当我们把含有绝对路径的代码文件打包进可执行文件中时,可执行文件其实还是在按照这个路径来寻找需要的资源文件,比如:
在这里插入图片描述
在这张图中,我们需要引用到一个
E:\暑期学习\dog-cat\ultralytics-main\runs\train\exp9\weights该路径下的last.pt文件,那么当它被打包进exe中后,exe读取的其实还是这段路径,在自己电脑或者服务器上运行一点问题都没有,但是当exe移植的时候,就会必然会出现exe找不到该路径下的文件的报错,因为这段路径只存在于我们自己的电脑或者服务器中,我们需要exe找的路径,应该是生成在临时目录中的被我们打包进exe文件中的资源文件对应路径

2.解决方法

而解决这个问题也很简单,首先我们先要确定 /Temp/xxxxx下会释放我们打包进exe的资源文件,而上一篇文章也提到过,我们需要将资源文件全部写入 .sepc文件中的datas参数中,
在这里插入图片描述
这里res就是一个资源文件,如果.sepc文件是与主运行文件在同一目录下,则前面引号中的res可以写相对路径,也就是直接写res就行,如果不是,则要换成res的绝对路径。

然后,以以下面代码为例

import sys
if getattr(sys, 'frozen', False):
    application_path = sys._MEIPASS
else:
    application_path = os.path.dirname(os.path.abspath(__file__))

将这个代码放到需要引用绝对路径的代码中,
这里首先会用 if 判断现在是正常开发环境还是已打包成可执行文件的状态,然后将需要获取的路径赋值给 application_path变量,如果是可执行文件状态,则 application_path的值就会变为临时目录的路径,否则, application_path的值就是当前脚本所在的文件夹路径,这样,在代码中引用到资源文件时,就可以按照如下的处理方式:

if getattr(sys, 'frozen', False):
    application_path = sys._MEIPASS
else:
    application_path = os.path.dirname(os.path.abspath(__file__))

# 使用 os.path.join 来构建路径,避免路径分隔符的问题
default_model_path1 = os.path.join(application_path, 'wight')
default_model_path2 = os.path.join(application_path, 'imges_and_video', 'huoyan.png')
default_model_path3 = os.path.join(application_path, 'imges_and_video', 'beijing1.png')

self.folder_path = default_model_path1  # 模型文件夹路径
self.setWindowIcon(QIcon(default_model_path2))  # 设置窗口图标
self.set_background_image(default_model_path3) # 设置背景图

可以看到这样,如果是已打包成可执行文件的状态,则会先获取临时目录的位置并赋值给application_path变量,在将之与资源文件的相对路径拼接起来赋值给另一个变量,最后引用资源文件的时候直接指定该变量值,逻辑上就能完美的解决在default_model_path1 2 3处需要使用绝对路径的问题

二、因importlib动态导入机制而不能完全被打包进exe中的必要库问题

当我们打包后运行exe,报错是找不到某个库中的文件或者某库,就可能是该原因导致,或者说源代码中的某个功能失效了,也可能是相关库中使用了相对导入机制而使得某相关文件没被打包进exe中

1.问题解析

a > :
动态导入的原理:
动态导入是指在 Python 运行时根据条件动态地加载模块。这通常通过importlib模块来实现。例如,importlib.import_module(“module_name”)可以在运行时动态地导入名为module_name的模块。这种导入方式与常规的静态导入(如import module_name)不同,它提供了更大的灵活性,使得程序可以根据用户输入、配置文件或者运行时的其他条件来决定需要导入哪些模块。

对 PyInstaller 打包的影响:
b > :
模块收集困难:
PyInstaller 在打包过程中,主要是通过分析代码中的静态导入语句来确定需要打包哪些模块。对于动态导入,由于模块的加载是在运行时决定的,PyInstaller 很难预先知道所有需要打包的模块。例如,如果一个程序根据用户输入动态地导入不同的插件模块,PyInstaller 在默认情况下可能无法检测到所有这些潜在的插件模块,从而导致打包后的程序在运行时出现 “找不到模块” 的错误。
c > :
路径解析问题:
动态导入时的路径解析可能与 PyInstaller 的打包机制产生冲突。在正常的 Python 环境中,动态导入可以使用相对路径或绝对路径来指定模块。但是在打包后的环境中,模块的位置可能会因为 PyInstaller 的重新组织而发生变化。如果动态导入的路径没有正确考虑这种变化,就可能导致模块无法正确加载。例如,在开发环境中,一个动态导入使用了相对路径来导入同目录下的一个模块,但是在打包后,这个相对路径可能不再有效,因为模块的物理位置已经被 PyInstaller 重新安排。

在这里插入图片描述
像这里,fairsec库中就使用了动态导入来导入criterions文件,可以看到这里报错说没有找到

2.解决方法

这个就更简单了,不过必须得有试错经历,除非你对代码的所有引用库和文件的机制了如指掌,知道哪里是使用了动态导入,不然就像我一样老老实实打包后查看报错情况吧。
在报错后,记住报错提示,一般是提醒你没有找到该库中的某文件,然后去.scep文件中的datas参数中另开一对引号,前面填该库的绝对路径,后面填该库的名字,重新打包即可解决
当然有点时候报错并不会直接告诉你是缺少了什么库或者库中的什么文件,而是告诉你一些比较隐晦的报错信息,这个时候就比较头大,因为这需要我们自行判断是不是因为这个问题而产生的报错,如果是,又是什么库中存在动态导入机制,这就完全得靠自己的个人能力来判断了,请原谅笔者能力不足,占时还没有想到什么办法来教大家进行准确的判断,如果后期灵光炸现我会及时补充。

三、命令行参数报错问题

这是我在打包问题中,碰到的最恶心的问题,卡进度的时间也是最长的,原因就在于根本就不太可能想到是因为该原因报的错,涉及到对于报错的逻辑推理的部分基本为零,我也是偶然巧合下才发现报错的原因。

2.报错原因

例如我现在有以下代码:

import argparse

# 创建解析器对象
parser = argparse.ArgumentParser(description='这是一个示例程序,用于演示argparse的用法')

# 添加位置参数
parser.add_argument('input_file', type=str, help='输入文件的路径')

# 添加可选参数
parser.add_argument('-v', '--verbose', action='store_true', help='开启详细输出模式')
parser.add_argument('-o', '--output', type=str, default='output.txt', help='输出文件的路径,默认是output.txt'

在运行程序时,代码会一项一项的解读关于parser中添加的参数实例,这些参数可能在代码中的某部分被定义了,也可能直接使用的是默认值,无论怎么样,在程序运行的时候,每一项参数都会被赋予一个值,按道理来说,如果代码中对所有参数都进行了定义,并且传入的值也都是正确的,被打包的程序是能正常运行的,而结果并非如此,在我打包的项目中,就出现了如下的报错:
在这里插入图片描述
可执行文件运行后在终端显示,有一个命令参数是不被期望的值,而这个参数是一条路径,这个路径也正是我可执行文件的路径 /home/zhu/3090_data/yolov5-4.0/dist/heibanzi ,但是我重新回到代码中检察时,发现并没有任何一个参数是可能生成这个变量的,当时还增加了调试代码,输出所有传入的参数的值,发现也没有这个参数的存在,在我束手无策时,无意中读到一个知识点,说argparse在读取所有参数前,先会读取一个默认参数值,这个参数值叫原始命令行参数值,我当时想,这个与全局代码都没有关系的参数值可能就是从这里传进去的,所以我试着打印了打包后原始命令行参数的值,过不奇然:
在这里插入图片描述
打包后的原始命令行参数多出了一条绝对路径,而argparse它只能读取一条,所以剩下的一条无家可归,也就产生了报错,当时发现了就有点无语,如果不是巧合根本就不会想到是这样的原因,我还为此去查看了pyinstaller的官方文档,

在这里插入图片描述
全然没有提到过这方面的问题,但这显然不是我项目本身代码的原因,而是pyinstaller本身打包逻辑或者exe运行时的逻辑漏洞,也希望官方早点发现并给予解决方案吧。

2.解决方法

这里讲一下我的解决方法:

original_argv = sys.argv.copy()

    # print("原始命令行参数:", original_argv)

    new_argv = []
    if len(original_argv) == 1:
        new_argv = original_argv
    elif len(original_argv) > 1:
        new_argv.append(original_argv[0])

    sys.argv = new_argv

    # 解析参数
    parser = argparse.ArgumentParser()

原理我就不过多解释了,就是一个限制列表长度的操作,将这些代码添加到解析参数
parser = argparse.ArgumentParser()之前,就能确保原始命令参数传入的只有一个值了

总结

打包其实还有一些细节上的处理,不过都容易解决,这里只是将笔者在实战打包中碰到的最难的问题总结了出来,如果除了上面提及的问题之外,还有卡了读者很久的没能解决的问题,也欢迎在评论区留言讨论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值