every blog every motto: A day is a miniature of eternity.
0. 前言
个人需要,设计了一款小软件,为了方便使用,将它打包,在这其中出现了若干问题,在这里进行总结。
注:
- 为了便于说明,在此对相关代码进行了简化。
- 经常参考别人博文,但别人有时只给出部分文件,不给出文件名。读者经常陷入不知所云的境地,特此将所需的文件进行罗列,并加以说明。
1. 正文
1. 所需文件
文件1:testui.py(GUI代码)
这是通过designer设计出的一个按钮。具体代码如下。
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(267, 206)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(90, 50, 93, 61))
self.pushButton.setObjectName("pushButton")
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 267, 26))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "开始"))
文件2:test_mian.py(主程序代码)
这是主程序代码,可以看作时GUI和功能函数的“桥梁”,具体代码如下。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys,os
if hasattr(sys, 'frozen'):
os.environ['PATH'] = sys._MEIPASS + ";" + os.environ['PATH']
from PyQt5.QtWidgets import QApplication,QMainWindow
# from testui import *
from test_function import *
import testui
# class MyWindow(QMainWindow,Ui_MainWindow):
# def __init__(self,parent=None):
# super(MyWindow,self).__init__(parent)
# self.setupUi(self)
#
#
#
# if __name__ == '__main__':
# app = QApplication(sys.argv)
# myWin = MyWindow()
# myWin.show()
# myWin.pushButton.clicked.connect(print_hello)
# sys.exit(app.exec())
if __name__ == '__main__':
app = QApplication(sys.argv)
MainWindow = QMainWindow()
ui = testui.Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
ui.pushButton.clicked.connect(printf)
sys.exit(app.exec_())
文件3: test_function.py (功能函数)
所要实现的功能,也可以写在文件2(test_main.py)中,为了条理清晰,独立出来。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def printf():
print('牛逼')
if __name__ == '__main__':
printf()
2. 问题
2.1 打包程序不可移植问题
问题描述:打包出来的exe文件只能在自己计算机上运行,不能在别人计算机上运行。
解决方法:在主程序(test_main.py),即文件2,开始出添加如下代码
if hasattr(sys, 'frozen'):
os.environ['PATH'] = sys._MEIPASS + ";" + os.environ['PATH']
说明:这个问题,时之前遇到的,一并记录之。
2.2 打包多个文件问题
问题描述:如果打包多个文件,并且多个文件都为(*.py文件),
网上已有相关教程
简化:如果是多个文件(都为.py),则只打包一个主程序即可。
pyinstaller -F test_main.py
2.3 打包出错
问题描述:ModuleNotFoundError: No module named ‘pkg_resources.py2_warn’
[51988] Failed to execute script pyi_rth_pkgres
原因:setuptools升级太快,pyinstaller跟不上导致。
解决方法:把setuptools降级到44.0.0以下,再重新打包即可 [1]
具体代码如下[2]
1 查看版本
pip isntall setuptools
2. 卸载已有版本
pip uninstall setuptools
- 安装新版本
pip install setuptools==39.1.0
2.4 打包资源文件
说明: 程序中经常会用到额外的资源文件(如:图片、文本、pdf、chromedriver等),需要将这些文件一本打包,在此一并附上。
注:
- 为了便于理解,在此用了一个单独的程序,随后附上(与前面程序无关,本程序名a2.py)
- 本程序需要的外部资源时一张图片存放在tools中
完整目录:
原理: - pyinstaller打包的可执行文件,运行sys,forzen会被设置成True,因此通过sys.fromzen的值区分,是开发环境还是打包的后的环境
- pyinstaller可以将资源文件一起bunild到exe中,当exe运行时,会生成一个临时文件夹,程序通过sys._MEIPASS访问临时文件夹的资源[4-5]
步骤一: 在程序中添加核心代码
核心代码:
def resource_path(relative_path):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
步骤二: 修改a2.spec文件
具体:修改datas,引文所需要的文件(图片)在tools中,具体添加如下图所示。
说明: 切换(上面提到)a2.py的目录下,用pyinstaller打包后会生成a2.spec(需要详细了解的参考文献4,6,7,8)
a2.spec文件说明:
analysis:以py文件为输入,它会分析py文件的依赖模块,并生成相应的信息
pyz:是一个.pyz的压缩比搜,包含程序运行需要的所有依赖
exe:根据上面两项生成
collect: 生成其他部分的输出文件夹,collect也可以没有
步骤三:
重新打包:
pyinstaller -F a2.spec
本例完整代码:
程序输出一条语句,读取图片并展示(图片存放在同级目录下tools)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
from PIL import Image
def resource_path(relative_path):
if getattr(sys, 'frozen', False):
base_path = sys._MEIPASS
else:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def main():
print('我是第二个文件')
# path = os.path.join('tu.jpg')
path = resource_path((os.path.join('tools', 'tu.jpg')))
img = Image.open(path)
img.show()
if __name__ == '__main__':
main()
2.5 打包太大问题
问题描述:用pyinstaller打包经常动辄几百M,实在吓人。经过搜索现总结如下方法:
说明: 本部分所用程序与前面无关,可单独写个简单的程序进行验证。方法二、三都是在方法一基础上加以实现,为了更进一步压缩。
方法一: 创建虚拟环境[3]
说明:如果不创建虚拟环境,会将很多无关的库打包进去,创建虚拟环境后,需要什么安装什么。
1.按装pipenv库,用于创建虚拟环境
pip install pipenv
- 在控制台(win + r => 输入:cmd)中切换文件夹
新建一个文件夹,将需要的文件放入其中。如在桌面新建个(wen),在控制台切换进入
cd D:\Data_saved\Desktop\wen
- 进入虚拟环境
pipenv shell
- 安装程序中需要用到的库(以pandas为例)
pip install pandas
- 安装pyinstaller模块,用于打包
pip install pyinstaller
- 用pyinstaller 打包
pyinstaller -F main_.py
结果:exe: 220M。
方法二: 打包成目录形式(类型平时我们安装的软件)
去掉参数F 即可。
pyinstaller main_.py
结果:总文件(dist) 615M,exe:5.3M。
方法三: 添加参数 upx[9-10]
–upx-dir=(upx所在路径)
upx下载地址:upx下载
pyinstaller -F main_.py --upx-dir=D:\softeware\upx-3.95-win32
结果:exe: 134M
但是无法运行!!!!!
可以参考文献11,12,进行测试。
参考文献
[1] https://bbs.csdn.net/topics/395779945
[2] https://blog.csdn.net/petSym/article/details/82840636
[3] https://blog.csdn.net/qq_40529853/article/details/100576791
[4] https://pythonhosted.org/PyInstaller/spec-files.html#spec-file-operation
[5] https://www.cnblogs.com/darcymei/p/9397173.html
[6] https://cloud.tencent.com/developer/news/299957
[7] http://legendtkl.com/2015/11/06/pyinstaller/
[8] https://www.cnblogs.com/chusiyong/p/12052930.html
[9] https://blog.csdn.net/xinyingzai/article/details/80282856
[10] https://blog.csdn.net/qq_27017791/article/details/102748766
[11] https://blog.mioshu.com/archives/570.html
[12] https://stackoverflow.com/questions/47730240/how-do-i-use-upx-with-pyinstaller