文章摘要
Python打包exe,解决打包后体积过大的痛点,基于坛友分享的知识和源码,表示感谢!
实现方法:
1、临时创建一个新的运行环境;
2、解析py脚本的运行依赖库,自动安装,采用清华源;
3、如果存在不能完全解析的库,可以手动添加;
4、打包结束后,删除临时运行环境。
通过网盘分享的文件:Python打包exe小工具
链接: 提取码:
–来自百度网盘超级会员v5的分享
(提示:电脑需要安装有Python环境)

经实测,打包后的体积,成倍数的缩小,再一次感谢坛友的分享!
欢迎各位修改提高,也请不吝分享。
- # !/usr/bin/env python
- # coding=utf-8
- import sys
- import os
- import re
- import shutil
- import subprocess
- import venv
- import ast
- from PyQt6.QtWidgets import (QApplication, QMainWindow, QPushButton,
- QFileDialog, QLabel, QVBoxLayout, QWidget,
- QProgressBar, QCheckBox, QGroupBox, QGridLayout,
- QLineEdit, QTextEdit, QHBoxLayout, QMessageBox, QListWidget)
- from PyQt6.QtCore import Qt, QThread, pyqtSignal
- from PyQt6.QtGui import QFont
- PACKAGE_MAPPING = {
- # 科学计算 / 数据处理
- ‘PIL’: ‘Pillow’,
- ‘cv2’: ‘opencv-python’,
- ‘sklearn’: ‘scikit-learn’,
- ‘yaml’: ‘PyYAML’,
- ‘bs4’: ‘beautifulsoup4’,
- ‘attr’: ‘attrs’,
- ‘gi’: ‘PyGObject’,
- ‘wx’: ‘wxPython’,
- ‘fitz’: ‘PyMuPDF’,
- ‘docx’: ‘python-docx’,
- ‘pptx’: ‘python-pptx’,
- ‘tables’: ‘pytables’,
- ‘xlsxwriter’: ‘XlsxWriter’,
- # Web / 网络
- ‘jwt’: ‘PyJWT’,
- ‘jose’: ‘python-jose’,
- ‘dotenv’: ‘python-dotenv’,
- ‘socks’: ‘PySocks’,
- ‘websocket’: ‘websocket-client’,
- ‘flask’: ‘Flask’,
- ‘django’: ‘Django’,
- # GUI / 图形
- ‘tkinter’: None, # 标准库,标记为None跳过安装
- ‘kivy’: ‘Kivy’,
- ‘pyautogui’: ‘PyAutoGUI’,
- ‘customtkinter’: ‘customtkinter’,
- ‘ttkbootstrap’: ‘ttkbootstrap’,
- # 数据库
- ‘psycopg2’: ‘psycopg2-binary’,
- ‘pymysql’: ‘PyMySQL’,
- ‘mysql’: ‘mysql-connector-python’,
- ‘sqlalchemy’: ‘SQLAlchemy’,
- ‘sqlite3’: None, # 标准库
- # 加密 / 安全
- ‘Crypto’: ‘pycryptodome’,
- ‘Cryptodome’: ‘pycryptodome’,
- ‘nacl’: ‘PyNaCl’,
- ‘OpenSSL’: ‘pyOpenSSL’,
- # 工具 / 杂项
- ‘dateutil’: ‘python-dateutil’,
- ‘serial’: ‘pyserial’,
- ‘usb’: ‘pyusb’,
- ‘magic’: ‘python-magic’,
- ‘slugify’: ‘python-slugify’,
- ‘barcode’: ‘python-barcode’,
- ‘win32api’: ‘pywin32’,
- ‘win32com’: ‘pywin32’,
- ‘winreg’: None, # 标准库
- ‘apscheduler’: ‘APScheduler’,
- ‘charset_normalizer’: ‘charset-normalizer’,
- ‘Levenshtein’: ‘python-Levenshtein’,
- ‘qrcode’: ‘qrcode’,
- ‘zbar’: ‘pyzbar’,
- }
- class PackThread(QThread):
- progress = pyqtSignal(str)
- finished = pyqtSignal(bool, str)
- stop_flag = False
- def __init__(self, file_path, options, manual_deps):
- super().__init__()
- self.file_path = file_path
- self.options = options
- self.manual_deps = manual_deps
- self.stop_flag = False
- self.venv_dir = None
- def log(self, message):
- self.progress.emit(message)
- def run_command(self, cmd, cwd=None, env=None):
- “””执行命令并实时返回输出,统一使用UTF-8编码防止GBK崩溃”””
- try:
- process = subprocess.Popen(
- cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- text=True,
- encoding=’utf-8′, # ✅ 修复: 显式指定UTF-8
- errors=’replace’, # ✅ 修复: 无法解码的字符用替代而非抛异常
- cwd=cwd,
- env=env,
- bufsize=1,
- universal_newlines=True
- )
- for line in iter(process.stdout.readline, ”):
- if self.stop_flag:
- process.terminate()
- return False, “用户停止操作”
- if line:
- self.log(line.strip())
- process.wait()
- return process.returncode == 0, “”
- except Exception as e:
- return False, str(e)
- def extract_imports(self, file_path):
- “””使用AST提取第三方库名称,并应用包名映射”””
- imports = set()
- try:
- with open(file_path, ‘r’, encoding=’utf-8′) as f:
- tree = ast.parse(f.read(), filename=file_path)
- for node in ast.walk(tree):
- if isinstance(node, ast.Import):
- for alias in node.names:
- imports.add(alias.name.split(‘.’)[0])
- elif isinstance(node, ast.ImportFrom):
- if node.module:
- imports.add(node.module.split(‘.’)[0])
- except Exception as e:
- self.log(f”警告: 无法解析导入语句: {str(e)}”)
- # 过滤标准库(优先使用Python内置集合,回退到手动列表)
- stdlib_modules = set(sys.stdlib_module_names) if hasattr(sys, ‘stdlib_module_names’) else {
- ‘os’, ‘sys’, ‘re’, ‘json’, ‘time’, ‘datetime’, ‘math’, ‘random’,
- ‘collections’, ‘itertools’, ‘functools’, ‘pathlib’, ‘typing’,
- ‘subprocess’, ‘shutil’, ‘glob’, ‘copy’, ‘string’, ‘io’, ‘abc’,
- ‘contextlib’, ‘unittest’, ‘logging’, ‘argparse’, ‘configparser’,
- ‘csv’, ‘xml’, ‘html’, ‘http’, ‘urllib’, ‘socket’, ‘ssl’, ‘threading’,
- ‘multiprocessing’, ‘concurrent’, ‘asyncio’, ‘signal’, ‘mmap’, ‘struct’,
- ‘codecs’, ‘unicodedata’, ‘textwrap’, ‘difflib’, ‘pprint’, ‘enum’,
- ‘operator’, ‘decimal’, ‘fractions’, ‘statistics’, ‘array’,
- ‘bisect’, ‘heapq’, ‘queue’, ‘types’, ‘weakref’, ‘gc’, ‘inspect’,
- ‘dis’, ‘pickle’, ‘shelve’, ‘dbm’, ‘sqlite3’, ‘zlib’, ‘gzip’, ‘bz2’,
- ‘lzma’, ‘zipfile’, ‘tarfile’, ‘tempfile’, ‘fnmatch’, ‘linecache’,
- ‘tokenize’, ‘token’, ‘keyword’, ‘ast’, ‘symtable’, ‘compileall’,
- ‘py_compile’, ‘traceback’, ‘warnings’, ‘dataclasses’,
- ‘importlib’, ‘pkgutil’, ‘modulefinder’, ‘runpy’, ‘site’, ‘sysconfig’,
- ‘builtins’, ‘errno’, ‘ctypes’, ‘platform’, ‘locale’, ‘gettext’,
- ‘curses’, ‘readline’, ‘rlcompleter’, ‘tty’, ‘termios’, ‘pty’, ‘fcntl’,
- ‘pipes’, ‘posixpath’, ‘ntpath’, ‘genericpath’, ‘posix’, ‘nt’, ‘pwd’,
- ‘grp’, ‘resource’, ‘select’, ‘selectors’, ‘stat’, ‘fileinput’, ‘filecmp’,
- ‘netrc’, ‘xdrlib’, ‘plistlib’, ‘hashlib’, ‘hmac’, ‘secrets’, ‘uuid’,
- ‘ipaddress’, ’email’, ‘mailbox’, ‘mimetypes’, ‘base64’, ‘binascii’,
- ‘quopri’, ‘uu’, ‘colorsys’, ‘imghdr’, ‘sndhdr’, ‘ossaudiodev’, ‘aifc’,
- ‘sunau’, ‘wave’, ‘chunk’, ‘cgi’, ‘cgitb’, ‘wsgiref’, ‘xmlrpc’, ‘ftplib’,
- ‘poplib’, ‘imaplib’, ‘nntplib’, ‘smtplib’, ‘smtpd’, ‘telnetlib’, ‘socketserver’,
- ‘robotparser’, ‘webbrowser’, ‘cmd’, ‘shlex’, ‘tkinter’,
- ‘turtle’, ‘turtledemo’, ‘idlelib’, ‘test’, ‘doctest’, ‘unittest’,
- ‘lib2to3’, ‘pdb’, ‘profile’, ‘cProfile’, ‘timeit’, ‘trace’, ‘tracemalloc’,
- ‘faulthandler’, ‘atexit’
- }
- mapped_deps = []
- for mod in imports:
- if mod in stdlib_modules or mod.startswith(‘.’):
- continue
- mapped = PACKAGE_MAPPING.get(mod)
- if mapped is None and mod in PACKAGE_MAPPING:
- # 显式标记为None的是标准库,跳过
- continue
- mapped_deps.append(mapped if mapped else mod)
- return list(set(mapped_deps))
- def run(self):
- if self.stop_flag:
- return
- script_dir = os.path.dirname(os.path.abspath(self.file_path))
- script_name = os.path.basename(self.file_path)
- base_name = self.options[‘name’] if self.options[‘name’] else os.path.splitext(script_name)[0]
- # 清理非法字符
- if not re.match(r’^[a-zA-Z0-9_\-.\u4e00-\u9fa5][a-zA-Z0-9_\-.\u4e00-\u9fa5 ]*$’, base_name):
- self.finished.emit(False, “程序名称包含非法字符或格式不正确!”)
- return
- dist_dir = os.path.join(script_dir, ‘dist_clean’)
- venv_path = os.path.join(script_dir, ‘.temp_build_venv’)
- self.venv_dir = venv_path
- try:
- # 1. 创建虚拟环境
- self.log(f”正在创建临时虚拟环境: {venv_path}”)
- if os.path.exists(venv_path):
- shutil.rmtree(venv_path)
- venv.create(venv_path, with_pip=True)
- self.log(“虚拟环境创建成功”)
- # 确定虚拟环境中的 python 和 pip 路径
- if os.name == ‘nt’:
- py_exe = os.path.join(venv_path, ‘Scripts’, ‘python.exe’)
- pip_exe = os.path.join(venv_path, ‘Scripts’, ‘pip.exe’)
- pyinstaller_exe = os.path.join(venv_path, ‘Scripts’, ‘pyinstaller.exe’)
- else:
- py_exe = os.path.join(venv_path, ‘bin’, ‘python’)
- pip_exe = os.path.join(venv_path, ‘bin’, ‘pip’)
- pyinstaller_exe = os.path.join(venv_path, ‘bin’, ‘pyinstaller’)
- # 2. 安装 PyInstaller
- self.log(“正在安装 PyInstaller…”)
- success, msg = self.run_command(
- [pip_exe, ‘install’, ‘pyinstaller’, ‘-i’, ‘https://pypi.tuna.tsinghua.edu.cn/simple’, ‘–trusted-host’,
- ‘pypi.tuna.tsinghua.edu.cn’])
- if not success:
- self.finished.emit(False, f”安装 PyInstaller 失败: {msg}”)
- return
- # 3. 分析并安装项目依赖
- self.log(“正在分析源码依赖…”)
- deps = self.extract_imports(self.file_path)
- # 合并手动添加的依赖
- if self.manual_deps:
- for dep in self.manual_deps:
- dep = dep.strip()
- if dep and dep not in deps:
- deps.append(dep)
- self.log(f”已添加手动指定的依赖: {‘, ‘.join(self.manual_deps)}”)
- if deps:
- self.log(f”最终需要安装的依赖: {‘, ‘.join(deps)}”)
- self.log(“正在安装项目依赖(使用清华源加速)…”)
- install_cmd = [pip_exe, ‘install’] + deps + [‘-i’, ‘https://pypi.tuna.tsinghua.edu.cn/simple’,
- ‘–trusted-host’, ‘pypi.tuna.tsinghua.edu.cn’]
- success, msg = self.run_command(install_cmd)
- if not success:
- self.log(f”部分依赖安装可能失败,继续尝试打包: {msg}”)
- else:
- self.log(“未检测到明显的第三方依赖”)
- # 4. 执行 PyInstaller
- self.log(“开始打包…”)
- command = [pyinstaller_exe]
- if self.options[‘onefile’]:
- command.append(‘–onefile’)
- else:
- command.append(‘–onedir’)
- if self.options[‘noconsole’]:
- command.append(‘–noconsole’)
- command.append(‘–windowed’)
- command.extend([‘–clean’])
- command.extend([‘–distpath’, dist_dir])
- command.extend([‘–workpath’, os.path.join(script_dir, ‘build_temp’)])
- # 处理图标
- if self.options[‘icon’] and os.path.exists(self.options[‘icon’]):
- command.extend([‘–icon’, self.options[‘icon’]])
- # 处理附加数据 (images 文件夹)
- images_dir = os.path.join(script_dir, ‘images’)
- if os.path.exists(images_dir):
- if os.name == ‘nt’:
- command.extend([‘–add-data’, f'{images_dir};images’])
- else:
- command.extend([‘–add-data’, f'{images_dir}:images’])
- command.extend([‘–name’, base_name])
- command.append(self.file_path)
- self.log(f”执行命令: {‘ ‘.join(command)}”)
- success, msg = self.run_command(command, cwd=script_dir)
- if not success:
- self.finished.emit(False, f”打包失败: {msg}”)
- return
- # 5. 验证输出
- output_ext = ‘.exe’ if os.name == ‘nt’ else ”
- if self.options[‘onefile’]:
- final_path = os.path.join(dist_dir, f”{base_name}{output_ext}”)
- else:
- final_path = os.path.join(dist_dir, base_name)
- if os.path.exists(final_path):
- self.log(f”打包成功!输出路径: {final_path}”)
- self.finished.emit(True, f”转换成功!请查看 {final_path}”)
- else:
- self.finished.emit(False, “打包完成但未找到输出文件,请检查日志”)
- except Exception as e:
- self.log(f”发生未知错误: {str(e)}”)
- self.finished.emit(False, f”发生未知错误: {str(e)}”)
- finally:
- # 6. 清理工作
- self.log(“正在清理临时文件…”)
- if os.path.exists(venv_path):
- try:
- shutil.rmtree(venv_path)
- self.log(“已删除临时虚拟环境”)
- except Exception as e:
- self.log(f”删除虚拟环境失败: {str(e)}”)
- build_temp = os.path.join(script_dir, ‘build_temp’)
- if os.path.exists(build_temp):
- try:
- shutil.rmtree(build_temp)
- except Exception:
- pass
- self.log(“清理完成”)
- class Py2ExeConverter(QMainWindow):
- def __init__(self):
- super().__init__()
- self.setWindowTitle(“Python打包exe小工具”)
- self.setGeometry(100, 100, 800, 650)
- self.setStyleSheet(“””
- QMainWindow { background-color: #f0f2f5; }
- QGroupBox { font-weight: bold; color: #333; border: 1px solid #ddd; border-radius: 5px; margin-top: 10px; padding-top: 10px; }
- QPushButton { background-color: #0078d4; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; }
- QPushButton:hover { background-color: #005a9e; }
- QPushButton:disabled { background-color: #ccc; }
- QLineEdit, QTextEdit, QListWidget { border: 1px solid #ccc; border-radius: 4px; padding: 5px; background: white; }
- QProgressBar { height: 10px; border-radius: 5px; }
- QProgressBar::chunk { background-color: #0078d4; }
- “””)
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
- main_layout = QVBoxLayout(central_widget)
- # 文件选择区域
- file_group = QGroupBox(“1. 选择 Python 脚本”)
- file_layout = QHBoxLayout()
- self.file_input = QLineEdit()
- self.file_input.setPlaceholderText(“请选择 .py 文件”)
- self.file_input.setReadOnly(True)
- self.btn_browse = QPushButton(“浏览”)
- self.btn_browse.clicked.connect(self.browse_file)
- file_layout.addWidget(self.file_input)
- file_layout.addWidget(self.btn_browse)
- file_group.setLayout(file_layout)
- main_layout.addWidget(file_group)
- # 依赖管理区域
- dep_group = QGroupBox(“2. 依赖管理 (解决缺失库问题)”)
- dep_layout = QVBoxLayout()
- dep_hint = QLabel(“如果打包后报错缺少模块,请在此处手动添加库名(如:pandas, numpy)”)
- dep_hint.setStyleSheet(“color: #666; font-size: 12px;”)
- dep_layout.addWidget(dep_hint)
- dep_input_layout = QHBoxLayout()
- self.dep_input = QLineEdit()
- self.dep_input.setPlaceholderText(“输入库名,例如: pandas”)
- self.btn_add_dep = QPushButton(“添加”)
- self.btn_add_dep.clicked.connect(self.add_manual_dep)
- self.btn_clear_dep = QPushButton(“清空”)
- self.btn_clear_dep.clicked.connect(self.clear_manual_deps)
- dep_input_layout.addWidget(self.dep_input)
- dep_input_layout.addWidget(self.btn_add_dep)
- dep_input_layout.addWidget(self.btn_clear_dep)
- dep_layout.addLayout(dep_input_layout)
- self.dep_list = QListWidget()
- self.dep_list.setMaximumHeight(100)
- dep_layout.addWidget(self.dep_list)
- dep_group.setLayout(dep_layout)
- main_layout.addWidget(dep_group)
- # 打包选项区域
- options_group = QGroupBox(“3. 打包选项”)
- options_layout = QGridLayout()
- self.check_onefile = QCheckBox(“单文件模式 (–onefile)”)
- self.check_onefile.setChecked(True)
- options_layout.addWidget(self.check_onefile, 0, 0)
- self.check_noconsole = QCheckBox(“无控制台窗口 (–windowed)”)
- self.check_noconsole.setChecked(False)
- options_layout.addWidget(self.check_noconsole, 0, 1)
- self.input_name = QLineEdit()
- self.input_name.setPlaceholderText(“可选:生成的 exe 名称”)
- options_layout.addWidget(QLabel(“程序名称:”), 1, 0)
- options_layout.addWidget(self.input_name, 1, 1)
- self.input_icon = QLineEdit()
- self.input_icon.setPlaceholderText(“可选:图标文件 (.ico)”)
- self.btn_icon = QPushButton(“选择图标”)
- self.btn_icon.clicked.connect(self.browse_icon)
- icon_layout = QHBoxLayout()
- icon_layout.addWidget(self.input_icon)
- icon_layout.addWidget(self.btn_icon)
- options_layout.addWidget(QLabel(“图标:”), 2, 0)
- options_layout.addLayout(icon_layout, 2, 1)
- options_group.setLayout(options_layout)
- main_layout.addWidget(options_group)
- # 控制按钮
- btn_layout = QHBoxLayout()
- self.btn_start = QPushButton(“开始打包”)
- self.btn_start.clicked.connect(self.start_conversion)
- self.btn_stop = QPushButton(“停止”)
- self.btn_stop.setEnabled(False)
- self.btn_stop.clicked.connect(self.stop_conversion)
- btn_layout.addWidget(self.btn_start)
- btn_layout.addWidget(self.btn_stop)
- main_layout.addLayout(btn_layout)
- # 日志区域
- log_group = QGroupBox(“4. 进度日志”)
- log_layout = QVBoxLayout()
- self.log_text = QTextEdit()
- self.log_text.setReadOnly(True)
- self.log_text.setFont(QFont(“Consolas”, 9))
- self.progress_bar = QProgressBar()
- self.progress_bar.setTextVisible(False)
- log_layout.addWidget(self.log_text)
- log_layout.addWidget(self.progress_bar)
- log_group.setLayout(log_layout)
- main_layout.addWidget(log_group)
- self.thread = None
- self.manual_deps = []
- def browse_file(self):
- file_path, _ = QFileDialog.getOpenFileName(self, “选择 Python 脚本”, “”, “Python Files (*.py)”)
- if file_path:
- self.file_input.setText(file_path)
- def browse_icon(self):
- file_path, _ = QFileDialog.getOpenFileName(self, “选择图标”, “”, “Icon Files (*.ico);;All Files (*)”)
- if file_path:
- self.input_icon.setText(file_path)
- def add_manual_dep(self):
- dep = self.dep_input.text().strip()
- if dep:
- if re.match(r’^[a-zA-Z0-9_\-\.]+$’, dep):
- if dep not in self.manual_deps:
- self.manual_deps.append(dep)
- self.dep_list.addItem(dep)
- self.dep_input.clear()
- else:
- QMessageBox.warning(self, “提示”, “该依赖已存在”)
- else:
- QMessageBox.warning(self, “错误”, “库名格式不正确,只能包含字母、数字、下划线、横线和点”)
- else:
- QMessageBox.warning(self, “提示”, “请输入库名”)
- def clear_manual_deps(self):
- self.manual_deps.clear()
- self.dep_list.clear()
- def log_message(self, msg):
- self.log_text.append(msg)
- scrollbar = self.log_text.verticalScrollBar()
- scrollbar.setValue(scrollbar.maximum())
- def start_conversion(self):
- file_path = self.file_input.text()
- if not file_path or not os.path.exists(file_path):
- QMessageBox.warning(self, “错误”, “请先选择一个有效的 Python 脚本文件!”)
- return
- options = {
- ‘onefile’: self.check_onefile.isChecked(),
- ‘noconsole’: self.check_noconsole.isChecked(),
- ‘name’: self.input_name.text().strip(),
- ‘icon’: self.input_icon.text().strip()
- }
- self.btn_start.setEnabled(False)
- self.btn_stop.setEnabled(True)
- self.log_text.clear()
- self.progress_bar.setRange(0, 0)
- self.thread = PackThread(file_path, options, self.manual_deps)
- self.thread.progress.connect(self.log_message)
- self.thread.finished.connect(self.on_finished)
- self.thread.start()
- def stop_conversion(self):
- if self.thread:
- self.thread.stop_flag = True
- self.log_message(“正在请求停止…”)
- self.btn_stop.setEnabled(False)
- def on_finished(self, success, message):
- self.btn_start.setEnabled(True)
- self.btn_stop.setEnabled(False)
- self.progress_bar.setRange(0, 100)
- self.progress_bar.setValue(100 if success else 0)
- if success:
- QMessageBox.information(self, “成功”, message)
- else:
- QMessageBox.critical(self, “失败”, message)
- self.log_message(“— 任务结束 —“)
- if __name__ == ‘__main__’:
- app = QApplication(sys.argv)
- window = Py2ExeConverter()
- window.show()
- sys.exit(app.exec())