py打包工具
库地址:
auto-py-to-exe
https://pypi.org/project/auto-py-to-exe/
Gooey
https://pypi.org/project/Gooey/
为什么要介绍这俩库?
- 直接丢代码给别人用:各种配置环境要有才能执行
- 命令行执行:丑
- 打包复杂
- 可视化界面编写复杂
auto-py-to-exe
auto-py-to-exe 是一个用于将Python程序打包成可执行文件的图形化工具。以往打包会使用pyinstaller库,需要掌握各种参数的作用,很难记。而auto-py-to-exe 基于 pyinstaller ,和相比pyinstaller ,多了 GUI 界面,用起来更为简单方便。
安装
# py版本 3.6-3.10
pip install auto-py-to-exe
启动
auto-py-to-exe
常用选项介绍
-
单文件
-
单文件:打包的可执行文件就一个,把所有的依赖都打包到一个文件中
-
单目录:打包的可执行文件在输出目录中,包含了依赖文件、库
-
-
控制台窗口
- 控制台:所有的交互在控制台显示
- 窗口:编写了窗体界面才能用此选项,隐藏控制台,仅展示编写的窗体
-
图标
可执行文件的图标,需要是ico格式
-
附加文件
- 添加文件:脚本依赖外部文件,即添加单个文件作为依赖
- 添加目录:脚本依赖外部库(或多个文件),添加对应目录作为依赖
-
高级选项
- 常规选项
- name:执行文件的名称
- clean:打包前是否清除缓存
- 捆绑选项
- add-binary:如果用到可执行文件,可以添加进去
- paths:搜搜依赖库的目录(一般不用)
- key:编译的临时文件是否需要加密,如果是核心代码,建议加密。
- windows特定选项
- version-file:版本文件,一般不用
- 常规选项
-
设置
- 输出路径:用于生成打包文件存放的地方
-
当前命令
用于展示pyinstaller打包的命令
-
输出
展示打包的过程
实践
功能
编写一个简单的交互式脚本,实现输入一个数,输出这个数加3的值。
编写脚本
-
脚本结构
-
文件内容
# Add3.py
def add3(x):
return x + 3
# run_it.py
import time
from Fun.Add3 import add3
if __name__ == '__main__':
n = input('请输入一个数:')
print(add3(int(n)))
time.sleep(10) # 避免执行完直接退出了,啥都看不到
看一下正常的控制台交互效果:
文件打包
生成文件
如果不想生成一个目录,可以选择生成一个文件,【单文件】选择单文件即可。
Gooey
一个快速构建可视化页面的工具库,自带封装好了的各种组件,只需一行命令就能生成一个带界面的工具。
安装
# py 2.7 3.x
pip install Gooey
改写脚本
# run_it.py
from Fun.Add3 import add3
from gooey import Gooey, GooeyParser
@Gooey(program_name="工具的名字啊")
def main():
parser = GooeyParser(description="第一个示例!")
parser.add_argument(
"x",
metavar=u'输入的数:',
help="请输入一个整数"
)
args = parser.parse_args()
try:
res = add3(int(args.x))
print(res)
except TypeError as e:
print('哎呀,报错了')
if __name__ == '__main__':
main()
运行
组件介绍
组件 | -- a -- |
---|---|
FileChooser | 文件选择器 |
MultiFileChooser | 文件多选器 |
DirChooser | 目录选择器 |
MultiDirChooser | 目录多择器 |
DateChooser | 日期选择器 |
TextField | 文本输入框 |
Dropdown | 单选框 |
RadioGroup | 复选框 |
全局配置
参数 | 介绍 |
---|---|
advanced | 切换显示全部设置还是仅仅是简化版本 |
show_config | 跳过所有配置并立即运行程序 |
language | 指定从 gooey/languages 目录读取哪个语言包 |
program_name | GUI 窗口显示的程序名。默认会显 sys.argv[0]。 |
program_description | Settings 窗口顶栏显示的描述性文字。默认值从 ArgumentParser 中获取。 |
default_size | 窗口默认大小,(600,400) |
required_cols | 设置必选参数行数。 |
optional_cols | 设置可选参数行数。 |
dump_build_config | 将设置以 JSON 格式保存在硬盘中以供编辑/重用。 |
richtext_controls | 打开/关闭控制台对终端控制序列的支持(对字体粗细和颜色的有限支持) |
支持多种结构布局
实践
图片角度修正可视化工具
目录结构
文件
# main.py
import os
from fun import *
from gooey import Gooey, GooeyParser
@Gooey(
richtext_controls=True, # 打开终端对颜色支持
program_name="爱标xx工具", # 程序名称
encoding="utf-8", # 设置编码格式,打包的时候遇到问题
progress_regex=r"^progress: (\d+)%$", # 正则,用于模式化运行时进度信息
menu=[{
'name': '文件',
'items': [{
'type': 'AboutDialog',
'menuTitle': '关于',
'name': '图片旋转角度处理工具',
'description': '用于处理标注页面图片看起来正常,但是切图后发现图片和画框的位置不一致,例:倒置、旋转等',
'developer': 'wjlv4@iflytek.com',
}, {
'type': 'Link',
'menuTitle': '访问主页',
'url': 'https://ainxx.iflyxxx.com/'
}]
}, {
'name': '帮助',
'items': [{
'type': 'AboutDialog',
'menuTitle': '帮助文档',
'name': '图片旋转角度处理帮助',
'description': '标注页面画框后预览图片出现倒置、旋转等问题自助处理步骤:\n1. 打开浏览器调试窗口(F12)\n2. 点击Network(网络)\n3. 点击标注页面框的√号\n4. 在调试窗口的“网络”中找到ocr?url=的请求后,鼠标点击此请求\n5. 复制右侧地址中的/test/xxxx/xxxx.jpg到工具输入栏',
}]
}]
)
def main():
parser = GooeyParser(description="平台辅助工具:图片旋转角度修正、入库试题可视化 ...")
subs = parser.add_subparsers(help='commands', dest='command')
draw_pic = subs.add_parser('入库图片可视化')
pic_fix = subs.add_parser('图片旋转角度修正')
pic_fix.add_argument('source_page_url',
metavar='图片路径',
help='请输入ocr图片的路径以/test开头.jpg结尾',
widget='TextField')
pic_fix.add_argument(
"rotate",
metavar=u'旋转角度:',
help="图片需要旋转的角度(逆时针)",
# 选项
choices=['90', '180', '270', '0'],
# 默认值
default='180',
# 下拉菜单
widget='Dropdown',
# 数据校验
gooey_options={
'validator': {
'test': "user_input in ['0','90', '180', '270']",
'message': "仅能输入90、180、270、0]"
}}
)
draw_pic.add_argument(
"page_path",
metavar=u'请输入待处理目录',
help="page.json和topic.json所在的目录",
# 下拉菜单
widget='DirChooser'
)
args = parser.parse_args()
if getattr(args, 'page_path', None):
run(os.path.join(args.page_path, 'page.json'))
elif getattr(args, 'source_page_url', None):
deal_pic(args.source_page_url, int(args.rotate))
# 将界面收集的参数进行处理
# ......
if __name__ == '__main__':
main()
# zzj_rotate_pic.py
# coding:utf8
import sys
import requests
from PIL import Image
from fun.Log import *
def resource_path(relative_path):
""" Get absolute path to resource, works for dev and for PyInstaller """
try:
# PyInstaller creates a temp folder and stores path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
def download_src(source_page_url, out_file_path='before.jpg'):
"""
下载原图
:param out_file_path:保存路径
:param source_page_url: 原图url
:return:
"""
logger.info(f' download {source_page_url}')
print('progress: 15%')
if not source_page_url.startswith('http'):
url = r"http://aixxxe-mxxxxe.ifxxdxxxb.com/api/ds/api/daxxxet/file"
source_page_url = url + '?path=' + source_page_url
# 下载并存储原图
res = requests.get(source_page_url, stream=True)
with open(out_file_path, 'wb') as f_w:
f_w.write(res.content)
print('progress: 30%')
def rotate_pic(rotate, pic_path='before.jpg'):
logger.info('rotate pic ...')
print('progress: 45%')
img = Image.open(pic_path)
img1 = img.rotate(rotate, expand=True)
logger.info('save rotated pic ...')
img1.save('after.jpg')
print('progress: 60%')
def upload_file(file_path, local_file='after.jpg'):
url = r"http://axxte-mxxxxe.iflxxxxub.com/api/ds/api/daxxxt/file"
if not file_path.startswith('http'):
file_path = url + '?path=' + file_path
file = open(local_file, 'rb')
logger.info('upload file ...')
print('progress: 80%')
querystring = {"path": file_path.split('=')[-1]}
response = requests.post(url=url, data=file.read(), headers={'Content-Type': 'text/xml'}, params=querystring)
if response.status_code == 200 and response.json().get('retcode', None) == '000000':
logger.info('upload success!')
print('progress: 100%')
else:
logger.error(f'upload failed!')
print('progress: 0%')
def deal_pic(source_page_url, rotate, save_tmp_file=False):
"""
处理角度异常的图片
:param source_page_url: 原图地址
:param rotate: 旋转角度,逆时针旋转
:param save_tmp_file: 是否保存临时文件
:return:
"""
try:
download_src(source_page_url)
rotate_pic(rotate)
upload_file(source_page_url)
if not save_tmp_file:
os.remove('before.jpg')
os.remove('after.jpg')
except Exception as e:
logger.error(e)
if __name__ == '__main__':
deal_pic(r'/test/5db9bca31775476cbxxxxx11f018ea9/cf0907bf6e9adxxxxx90f35a278c9e41.jpg',180)
运行
打包成执行文件
打包文件太大?
-
原因:打包用的解释器是conda的环境,里面包含了非当前脚本用到的第三方库
-
解决办法:使用新的环境来打包文件,新的环境只安装脚本需要的库
-
创建新环境
我这里采用的是virtualenv管理环境,直接创建一个新环境
D:\ProgramData\virtualenvs>virtualenv -p "C:\Users\wjlv4\AppData\Local\Programs\Python\Python39-32\python.exe" tmp_env created virtual environment CPython3.9.5.final.0-32 in 2517ms creator CPython3Windows(dest=D:\ProgramData\virtualenvs\tmp_env, clear=False, no_vcs_ignore=False, global=False) seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=C:\Users\wjlv4\AppData\Local\pypa\virtualenv) added seed packages: pip==21.3.1, setuptools==59.4.0, wheel==0.37.0 activators BashActivator,BatchActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator D:\ProgramData\virtualenvs>
切换项目的解释器到新虚拟环境,可以看到新的虚拟环境没有任何外部库。
-
本地执行脚本,查看缺少哪些依赖包,分别安装上
pip install Pillow requests pandas loguru gooey
-
在当前的虚拟环境使用pyinstaller打包(命令可以直接用刚才打包的命令)
依赖安装完毕后,在虚拟环境安装
pyinstaller
之后,直接复制命令执行即可:pyinstaller --noconfirm --onedir --windowed --icon "D:/abk.ico" --name "脚本的标题" --clean --key "1231a" --add-data "D:/t1/fun;fun/" "D:/t1/main.py"
-
查看打包后的文件
明显减少了很多。
-
补充pyinstaller打包参数介绍
-F 或 --onefile: 将代码打包为单个可执行文件,方便分发和使用。
-D 或 --onedir: 将代码打包为一个目录,目录中包含可执行文件和其他依赖文件。
-n 或 --name: 指定打包后的可执行文件的名称。
--add-data: 指定需要打包的文件或目录。该参数接受一个字符串,格式为 "src;dst",其中 src 表示源文件或目录,dst 表示目标文件夹。可以使用多个 --add-data 参数来添加多个文件或目录。
--exclude-module: 排除某些模块的打包,例如 --exclude-module=tkinter 可以排除 tkinter 模块的打包。
--hidden-import: 明确指定需要导入的模块,避免打包过程中缺失依赖。
-p 或 --path: 添加一个搜索路径,用于搜索依赖的模块。可以使用多个 -p 参数来添加多个搜索路径。
--noconsole 无窗体(即shell黑窗口)
-
想要打包的文件小
- 使用纯净的解释器环境:创建新的虚拟环境,安装相关依赖
- 最小化导入包,不要
from xxx import *
,尽量最准确导入包,例如from PyQt5.QtWidgets import QApplication, QWidget, QFileDialog, QLabel
-
打包的时候依赖哪个导入哪个,例如
pyinstaller --onefile --icon=abk.ico --upx-dir=upx-3.96-win64 --exclude-module PyQt5.QtWidgets.QApplication --exclude-module PyQt5.QtWidgets.QWidget --exclude-module PyQt5.QtWidgets.QHBoxLayout --add-data "C:\Users\wjlv4\PycharmProjects\image_rotate\abk.ico;." --exclude-module PyQt5.QtGui.QIcon --noconsole .\rotate_images.py
-
程序依赖外部文件或库
-
--add-data 添加依赖文件或目录,例如:
--add-data "C:\Users\wjlv4\PycharmProjects\image_rotate\abk.ico;." 把文件abk.ico添加到项目根目录
--add-data "fun;fun" 把fun目录添加到项目根目录,名称为fun
-
单文件模式,依赖的文件或者目录需要处理路径
# 示例代码,处理项目根路径,程序中依赖的文件使用拼接路径即可 class ImageRotator(QWidget): def __init__(self): super().__init__() self.folder_path = '' self.initUI() if hasattr(sys, "_MEIPASS"): # 如果是打包后的程序,则需要使用 _MEIPASS 获取打包后的根目录路径 self.root_path = os.path.join(sys._MEIPASS, '.') else: # 如果是直接运行源代码,则使用当前文件夹作为根目录 self.root_path = '.' def initUI(self): # 这里的图标就是使用了拼接的路径,否则即使打包时 --add-data "abk.ico;.",也不会生效 self.setWindowIcon(QIcon(os.path.join(self.root_path, "abk.ico"))) self.setWindowTitle('图片角度修复') self.setGeometry(100, 100, 500, 300)
-