使用pyinstaller和pyqt进行Python程序的打包

建议大家每次开发新项目的时候一定要使用新的环境,进行环境隔离,避免打的包越来越大。

一、基础打包——pyinstaller

1. 简单使用

示例Python代码

1
2
3
4
5
print('欢迎使用智能助手!')
name = input("请输入用户名:")
message = f"VIP-{name}"
print(message)
input('')

打包命令:

1
2
3
4
# 首先安装
pip install pyinstaller
# 然后构建
pyinstaller -D xxx.py

-D 打包,但是会把所有的文件都列出来,不会合并成一个文件,多文件的形式。

2. 打包出错问题或者闪现

如果程序出现问题,那么就把程序拖到终端,也就是在终端运行可执行文件。

3. 单文件和路径的问题

打包成单文件:

1
pyinstaller -F xxx.py  

结构如下:

1
2
3
├── dist
│   └── start
├── start.py

那么如何改名呢?使用-n xxx名字,也就是 pyinstaller -F start.py -n 小帮手

1
2
├── dist
│   └── 小帮手

但是对于单文件来说如果有配置文件呢,如何处理?

首先使用相对路径,执行会报错。然后使用绝对路径还是报错。

1
2
3
4
5
6
7
8
9
import os

base_dir = os.path.dirname(os.path.abspath(__file__))
print('欢迎使用智能助手!')
f_path = os.path.join(base_dir, 'config.ini')
f1 = open(f_path, 'r', encoding='utf-8')
data = f1.read()
f1.close()
print(data)

会提示如下的报错:

1
2
3
4
Traceback (most recent call last):
File "start.py", line 6, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '/var/folders/gv/b5kwm_6s6sj3klvjc2d0m2w40000gn/T/_MEIKrqdTi/config.ini'
[29203] Failed to execute script 'start' due to unhandled exception!

可以理解成代码需要解压,放到了系统的临时目录,之后再运行程序。所以配置文件放在程序的边上是没有用的。

打包成单文件的时候读取文件时,路径会出现问题。那么怎么办呢,有两种方式。

方式一 sys.argv

sys.argv获取当前程序所在的目录,将配置文件复制到dist目录下。

1
2
3
print(sys.argv)
# 输出
['/Users/medivh/PycharmProjects/GoodTools/start.py']

所以一般使用sys.argv[0]

整体代码:

1
2
3
4
5
6
7
8
9
10
import os
import sys

base_dir = os.path.dirname(sys.argv[0])
print('欢迎使用智能助手!')
f_path = os.path.join(base_dir, 'config.ini')
f1 = open(f_path, 'r', encoding='utf-8')
data = f1.read()
f1.close()
print(data)

但是需要注意,如果是使用命令行路径的方式执行程序的话,就有可能找不到文件,取决于运行代码过程中输入的目录不同导致获取的路径变成相对路径。解决方式就是全部处理成绝对路径。

1
base_dir = os.path.dirname(os.path.realpath(sys.argv[0]))

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  GoodTools ./dist/小帮手
欢迎使用智能助手!
[server]
k1=123
k2=456
[client]
url=https://econow.cn
➜ GoodTools cd ..
➜ PycharmProjects ./GoodTools/dist/小帮手
欢迎使用智能助手!
[server]
k1=123
k2=456
[client]
url=https://econow.cn

这样就会所有的路径都变成了绝对路径,更为稳妥。

方式二 frozen

增加判断条件,逻辑是判断代码打开还是pyinstaller打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
import sys

print('欢迎使用智能助手!')
base_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
if getattr(sys,'frozen',False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
f_path = os.path.join(base_dir, 'config.ini')
f1 = open(f_path, 'r', encoding='utf-8')
data = f1.read()
f1.close()
print(data)

3. configparser的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import configparser

config = configparser.ConfigParser()
config.read(f_path, encoding='utf-8')

print(config.sections()) # 获取节点 ['server', 'client']
groups = config.items('server') # 获取值
print(groups) # [('k1', '123'), ('k2', '456')]


# 处理成字典
config_dict = {
'server': {}
}
for sec in config.sections():
config_dict[sec] = dict()
for k, v in config.items(sec):
config_dict[sec][k] = v
print(config_dict) # {'server': {'k1': '123', 'k2': '456'}, 'client': {'url': 'https://econow.cn'}}

5. 动态导入

首先新建一个模块:

1
2
3
4
5
6
7
8
9
# 创建 utils/encrypt.py
import hashlib


def md5(data_string):
obj = hashlib.md5()
obj.update(data_string.encode('utf-8'))
return obj.hexdigest()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import sys
from utils.encrypt import md5

print('欢迎使用智能助手!')
base_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
f_path = os.path.join(base_dir, 'config.ini')

user = input('用户名: ')
pwd = input('密码: ')
password = md5(pwd)
print(user, password)

# 输出 tom 202cb962ac59075b964b07152d234b70

另外一种导入模式——import_module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import sys
import importlib

print('欢迎使用智能助手!')
base_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
f_path = os.path.join(base_dir, 'config.ini')

user = input('用户名: ')
pwd = input('密码: ')
m = importlib.import_module("utils.encrypt")
password = m.md5(pwd)
print(user, password)

但是这种导入就会报错。

解决方式,修改hiddenimports增加上方法路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = Analysis(
['start.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=["utils.encrypt"],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)

但是打包的时候要换个执行方式,不要加-F

1
pyinstaller xxxx.spec

这种方式的优点是可以使代码更简洁。

二、Pyqt

示例一

拆分:

  • 首先观察整体是一个大的垂直的layout,其他的layout往里添加
  • 头部,水平的一个layout,两个按钮后增加一个弹簧挤压到最左边
  • 表单的layout,一个输入框和按钮
  • 表格的layout,要配置表格的headers
  • footer的layout,左边是个label,然后弹簧,之后再若干按钮

初步使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import sys

from PyQt5.QtWidgets import QApplication, QWidget, QDesktopWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLineEdit, \
QTableWidget, QTableWidgetItem, QLabel


class MainWindow(QWidget):
def __init__(self):
super().__init__()
# 窗体标题和尺寸
self.setWindowTitle('NB的XX系统')
self.resize(980, 480)
# 窗体位置
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)

# 创建布局 div
layout = QVBoxLayout() # 垂直
# layout = QHBoxLayout() # 水平
# 头部
layout.addLayout(self.init_header_view())
# form
layout.addLayout(self.init_form_view())
# table
layout.addLayout(self.init_table_view())
# 底部
layout.addLayout(self.init_footer_view())

layout.addStretch(1)
self.setLayout(layout)

def init_header_view(self):
header_layout = QHBoxLayout()
btn1 = QPushButton('开始')
header_layout.addWidget(btn1)
btn2 = QPushButton('停止')
header_layout.addWidget(btn2)
header_layout.addStretch(1)
return header_layout

def init_form_view(self):
form_layout = QHBoxLayout()
self.txt_add = txt_add = QLineEdit()
txt_add.setPlaceholderText("请输入商品ID和价格")
form_layout.addWidget(txt_add)
btn_add = QPushButton('添加')
btn_add.clicked.connect(self.event_click_add)
form_layout.addWidget(btn_add)
return form_layout

def init_table_view(self):
headers = [('ID', 80), ("Name", 80), ("Mail", 60), ("Title", 80), ("Status", 80), ("FM", 60), ("Price", 60),
("Address", 180)]

table_layout = QHBoxLayout()
self.table_widget = table = QTableWidget(5, len(headers))
# table.setMinimumWidth(400)
for idx, ele in enumerate(headers):
text, width = ele
item = QTableWidgetItem()
item.setText(text)
table.setHorizontalHeaderItem(idx, item)
table.setColumnWidth(idx, width)

table_layout.addWidget(table)
return table_layout

def init_footer_view(self):
footer_layout = QHBoxLayout()
lbl = QLabel("待执行")
footer_layout.addWidget(lbl)
footer_layout.addStretch(1)
footer_layout.addWidget(QPushButton('重新初始化'))
footer_layout.addWidget(QPushButton('重新监测'))
footer_layout.addWidget(QPushButton('清零'))
footer_layout.addWidget(QPushButton('邮箱配置'))
footer_layout.addWidget(QPushButton('IP代理'))
return footer_layout

def event_click_add(self):
print('点击添加了')
text = self.txt_add.text()
print(text)
cell = QTableWidgetItem(text)
self.table_widget.setItem(1, 1, cell)


if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

示例二

layout其实是不可见的,效果图:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import sys

from PyQt5.QtWidgets import QApplication, QWidget, QDesktopWidget, QHBoxLayout, QVBoxLayout, QPushButton, QLineEdit, \
QTableWidget, QTableWidgetItem, QLabel, QTextEdit


class MainWindow(QWidget):
def __init__(self):
super().__init__()
# 窗体标题和尺寸
self.setWindowTitle('Bili的注册系统')
self.resize(1150, 450)
# 窗体位置
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)

# 创建布局 div
# layout = QVBoxLayout() # 垂直
layout = QHBoxLayout() # 水平
layout.setContentsMargins(10, 10, 10, 10) # 边距,左上右下

left = QVBoxLayout()

headers = [('ID', 80), ("Name", 80), ("Mail", 60), ("Title", 80), ("Status", 80), ("FM", 60), ("Price", 60),
("Address", 180)]

table = QTableWidget(10, len(headers))
table.setMinimumHeight(430)
for idx, ele in enumerate(headers):
text, width = ele
item = QTableWidgetItem()
item.setText(text)
table.setHorizontalHeaderItem(idx, item)
table.setColumnWidth(idx, width)

left.addWidget(table)
footer_layout = QHBoxLayout()
lbl = QLabel("待执行")
footer_layout.addWidget(lbl)
footer_layout.addStretch(1)
footer_layout.addWidget(QPushButton('线程重置'))
footer_layout.addWidget(QPushButton('代理IP'))

left.addLayout(footer_layout)
left.addStretch(1)
layout.addLayout(left, 4) # 数字表示比例 4/5

r = QWidget() # 以插件的形式
r.setStyleSheet("border-left:1px solid rgb(245,245,245)")
right = QVBoxLayout()
h1 = QHBoxLayout()
h1.addWidget(QLabel("成功:100"))
h1.stretch(1)
h1.addWidget(QLabel("失败:3"))
right.addLayout(h1)

h2 = QHBoxLayout()
h2.addWidget(QLabel("线程:"))
h2.addWidget(QLineEdit("10"))
h2.addWidget(QPushButton("确定"))
right.addLayout(h2)

h3 = QHBoxLayout()
h3.addWidget(QLabel("卡数:"))
h3.addWidget(QLabel("100"))
right.addLayout(h3)
h3.addStretch(1)

h4 = QHBoxLayout()
h4.addWidget(QPushButton("加载"))
h4.addWidget(QPushButton("重置"))
right.addLayout(h4)

h5 = QHBoxLayout()
btn_start = QPushButton("开始")
btn_start.setFixedHeight(50)
h5.addWidget(btn_start)
btn_stop = QPushButton("停止")
btn_stop.setFixedHeight(50)
h5.addWidget(btn_stop)
right.addLayout(h5)

h6 = QHBoxLayout()
h6.addWidget(QLabel("运行记录"))
right.addLayout(h6)
h7 = QHBoxLayout()
h7.addWidget(QTextEdit())
right.addLayout(h7)

right.addStretch(1)
r.setLayout(right)
layout.addWidget(r, 1)

self.setLayout(layout)


if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

三、价格监测工具

1. 添加监控项

  • 点击添加绑定事件
  • 获取文本信息
    • 商品ID、概要、价格、频率、状态
    • 添加到页面表格
    • 状态(初始化)【待检测】【检测中】【完成并提醒】【初始化失败】【异常并停止】
    • 持久化