710 lines
21 KiB
Python
710 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Windows专用构建脚本
|
||
自动检测依赖、构建独立exe文件并创建分发包
|
||
适用于Windows 7/10/11 (x64)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
import platform
|
||
import shutil
|
||
import time
|
||
from pathlib import Path
|
||
|
||
class WindowsBuilder:
|
||
"""Windows构建器"""
|
||
|
||
def __init__(self):
|
||
self.project_root = Path.cwd()
|
||
self.dist_dir = self.project_root / 'dist'
|
||
self.build_dir = self.project_root / 'build'
|
||
self.package_dir = self.project_root / '座位分配系统_Windows_分发包'
|
||
|
||
def check_environment(self):
|
||
"""检查构建环境"""
|
||
print("=" * 60)
|
||
print("Windows构建环境检查")
|
||
print("=" * 60)
|
||
|
||
# 检查操作系统
|
||
if platform.system() != 'Windows':
|
||
print(f"❌ 当前系统: {platform.system()}")
|
||
print("此脚本仅适用于Windows系统")
|
||
return False
|
||
|
||
print(f"✅ 操作系统: {platform.system()} {platform.release()}")
|
||
print(f"✅ 架构: {platform.machine()}")
|
||
print(f"✅ Python版本: {sys.version}")
|
||
|
||
# 检查主程序文件
|
||
main_script = self.project_root / 'seat_allocation_system.py'
|
||
if not main_script.exists():
|
||
print("❌ 未找到主程序文件: seat_allocation_system.py")
|
||
return False
|
||
print(f"✅ 主程序文件存在: {main_script}")
|
||
|
||
return True
|
||
|
||
def install_dependencies(self):
|
||
"""安装构建依赖"""
|
||
print("\n" + "=" * 60)
|
||
print("安装构建依赖")
|
||
print("=" * 60)
|
||
|
||
required_packages = [
|
||
'pandas>=1.3.0',
|
||
'openpyxl>=3.0.0',
|
||
'numpy>=1.20.0',
|
||
'pyinstaller>=5.0',
|
||
'pywin32>=227' # Windows特定依赖
|
||
]
|
||
|
||
for package in required_packages:
|
||
print(f"\n检查 {package}...")
|
||
package_name = package.split('>=')[0]
|
||
|
||
# 特殊处理pywin32
|
||
if package_name == 'pywin32':
|
||
try:
|
||
import win32api
|
||
print(f"✅ {package_name} 已安装")
|
||
continue
|
||
except ImportError:
|
||
pass
|
||
else:
|
||
try:
|
||
__import__(package_name)
|
||
print(f"✅ {package_name} 已安装")
|
||
continue
|
||
except ImportError:
|
||
pass
|
||
|
||
print(f"📦 安装 {package}...")
|
||
try:
|
||
cmd = [sys.executable, '-m', 'pip', 'install', package, '--upgrade']
|
||
result = subprocess.run(cmd, capture_output=True, text=True,
|
||
encoding='utf-8', errors='ignore')
|
||
|
||
if result.returncode == 0:
|
||
print(f"✅ {package} 安装成功")
|
||
else:
|
||
print(f"❌ {package} 安装失败")
|
||
print(f"错误信息: {result.stderr}")
|
||
# 对于pywin32,尝试替代安装方法
|
||
if package_name == 'pywin32':
|
||
print("尝试使用conda安装pywin32...")
|
||
try:
|
||
cmd_conda = ['conda', 'install', '-y', 'pywin32']
|
||
result_conda = subprocess.run(cmd_conda, capture_output=True, text=True)
|
||
if result_conda.returncode == 0:
|
||
print("✅ pywin32 通过conda安装成功")
|
||
continue
|
||
except:
|
||
pass
|
||
print("⚠️ pywin32安装失败,但可能不影响构建")
|
||
continue
|
||
return False
|
||
except Exception as e:
|
||
print(f"❌ 安装过程出错: {e}")
|
||
return False
|
||
|
||
return True
|
||
|
||
def clean_build_dirs(self):
|
||
"""清理构建目录"""
|
||
print("\n清理构建目录...")
|
||
|
||
for dir_path in [self.dist_dir, self.build_dir]:
|
||
if dir_path.exists():
|
||
try:
|
||
shutil.rmtree(dir_path)
|
||
print(f"✅ 清理目录: {dir_path}")
|
||
except Exception as e:
|
||
print(f"⚠️ 清理目录失败 {dir_path}: {e}")
|
||
|
||
# 清理spec文件
|
||
for spec_file in self.project_root.glob('*.spec'):
|
||
try:
|
||
spec_file.unlink()
|
||
print(f"✅ 清理spec文件: {spec_file}")
|
||
except Exception as e:
|
||
print(f"⚠️ 清理spec文件失败 {spec_file}: {e}")
|
||
|
||
def create_spec_file(self):
|
||
"""创建PyInstaller配置文件"""
|
||
print("\n创建PyInstaller配置文件(完全独立版本)...")
|
||
|
||
spec_content = '''# -*- mode: python ; coding: utf-8 -*-
|
||
# 完全独立的PyInstaller配置
|
||
# 包含Python解释器和所有依赖,无需目标机器安装Python
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
block_cipher = None
|
||
|
||
a = Analysis(
|
||
['seat_allocation_system.py'],
|
||
pathex=[str(Path.cwd())],
|
||
binaries=[],
|
||
datas=[],
|
||
hiddenimports=[
|
||
# 核心依赖
|
||
'pandas',
|
||
'openpyxl',
|
||
'numpy',
|
||
'xlsxwriter',
|
||
'xlrd',
|
||
'datetime',
|
||
'pathlib',
|
||
'subprocess',
|
||
'platform',
|
||
'sys',
|
||
'os',
|
||
|
||
# openpyxl相关
|
||
'openpyxl.workbook',
|
||
'openpyxl.worksheet',
|
||
'openpyxl.styles',
|
||
'openpyxl.utils',
|
||
'openpyxl.writer.excel',
|
||
'openpyxl.reader.excel',
|
||
'openpyxl.cell',
|
||
'openpyxl.formatting',
|
||
|
||
# pandas相关 - 完整导入
|
||
'pandas.io.excel',
|
||
'pandas.io.common',
|
||
'pandas.io.parsers',
|
||
'pandas.io.formats.excel',
|
||
'pandas._libs',
|
||
'pandas._libs.tslibs',
|
||
'pandas._libs.tslibs.base',
|
||
'pandas._libs.tslibs.timedeltas',
|
||
'pandas._libs.tslibs.np_datetime',
|
||
'pandas._libs.tslibs.nattype',
|
||
'pandas._libs.window',
|
||
'pandas._libs.window.aggregations',
|
||
'pandas._libs.hashtable',
|
||
'pandas._libs.algos',
|
||
'pandas._libs.index',
|
||
'pandas._libs.lib',
|
||
'pandas._libs.missing',
|
||
'pandas._libs.parsers',
|
||
'pandas._libs.reduction',
|
||
'pandas._libs.reshape',
|
||
'pandas._libs.sparse',
|
||
'pandas._libs.testing',
|
||
'pandas._libs.writers',
|
||
|
||
# numpy相关 - 完整导入
|
||
'numpy.core',
|
||
'numpy.core.multiarray',
|
||
'numpy.core.umath',
|
||
'numpy.core._methods',
|
||
'numpy.core._dtype_ctypes',
|
||
'numpy.core._internal',
|
||
'numpy.core.numeric',
|
||
'numpy.core.numerictypes',
|
||
'numpy.core.function_base',
|
||
'numpy.core.machar',
|
||
'numpy.core.getlimits',
|
||
'numpy.core.shape_base',
|
||
'numpy.lib.format',
|
||
'numpy.lib.mixins',
|
||
'numpy.lib.scimath',
|
||
'numpy.lib.stride_tricks',
|
||
'numpy.random',
|
||
'numpy.random._pickle',
|
||
'numpy.random.mtrand',
|
||
'numpy.random._common',
|
||
'numpy.random._generator',
|
||
|
||
# 编码相关
|
||
'encodings',
|
||
'encodings.utf_8',
|
||
'encodings.gbk',
|
||
'encodings.cp1252',
|
||
'encodings.latin1',
|
||
|
||
# 其他必要模块
|
||
'_ctypes',
|
||
'ctypes.util',
|
||
'pkg_resources',
|
||
'pkg_resources.py2_warn',
|
||
'pkg_resources.markers',
|
||
|
||
# Jinja2相关(可选,某些依赖可能需要)
|
||
# 'jinja2',
|
||
# 'jinja2.ext',
|
||
# 'markupsafe',
|
||
|
||
# Windows特定模块(仅在Windows上可用)
|
||
# 'win32api',
|
||
# 'win32con',
|
||
# 'pywintypes',
|
||
# 'pythoncom'
|
||
],
|
||
hookspath=[],
|
||
hooksconfig={},
|
||
runtime_hooks=[],
|
||
excludes=[
|
||
# 排除不必要的大型库
|
||
'matplotlib',
|
||
'matplotlib.pyplot',
|
||
'scipy',
|
||
'sklearn',
|
||
'tensorflow',
|
||
'torch',
|
||
'IPython',
|
||
'jupyter',
|
||
'notebook',
|
||
'tkinter',
|
||
'PyQt5',
|
||
'PyQt6',
|
||
'PySide2',
|
||
'PySide6',
|
||
'PIL',
|
||
'cv2',
|
||
'seaborn',
|
||
'plotly',
|
||
'bokeh',
|
||
|
||
# 测试和开发工具
|
||
'test',
|
||
'tests',
|
||
'unittest',
|
||
'pytest',
|
||
'setuptools',
|
||
'pip',
|
||
'wheel',
|
||
'distutils',
|
||
|
||
# 文档和示例
|
||
'sphinx',
|
||
'docutils',
|
||
'examples',
|
||
'sample',
|
||
|
||
# 其他不需要的模块
|
||
'curses',
|
||
'readline',
|
||
'sqlite3',
|
||
'xml.etree.ElementTree',
|
||
'html',
|
||
'http',
|
||
'urllib3',
|
||
'requests'
|
||
],
|
||
win_no_prefer_redirects=False,
|
||
win_private_assemblies=False,
|
||
cipher=block_cipher,
|
||
noarchive=False,
|
||
)
|
||
|
||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||
|
||
exe = EXE(
|
||
pyz,
|
||
a.scripts,
|
||
a.binaries,
|
||
a.zipfiles,
|
||
a.datas,
|
||
[],
|
||
name='座位分配系统',
|
||
debug=False,
|
||
bootloader_ignore_signals=False,
|
||
strip=False,
|
||
upx=False,
|
||
upx_exclude=[],
|
||
runtime_tmpdir=None,
|
||
console=True,
|
||
disable_windowed_traceback=False,
|
||
argv_emulation=False,
|
||
target_arch=None,
|
||
codesign_identity=None,
|
||
entitlements_file=None,
|
||
exclude_binaries=False,
|
||
)
|
||
'''
|
||
|
||
spec_file = self.project_root / 'seat_allocation.spec'
|
||
with open(spec_file, 'w', encoding='utf-8') as f:
|
||
f.write(spec_content)
|
||
|
||
print(f"✅ 配置文件已创建: {spec_file}")
|
||
return spec_file
|
||
|
||
def build_executable(self, spec_file):
|
||
"""构建可执行文件"""
|
||
print("\n" + "=" * 60)
|
||
print("开始构建可执行文件")
|
||
print("=" * 60)
|
||
|
||
# 使用spec文件时,只能使用基本选项
|
||
cmd = [
|
||
sys.executable, '-m', 'PyInstaller',
|
||
'--clean',
|
||
'--noconfirm',
|
||
str(spec_file)
|
||
]
|
||
|
||
print(f"执行命令: {' '.join(cmd)}")
|
||
print("这可能需要几分钟时间,请耐心等待...")
|
||
|
||
start_time = time.time()
|
||
|
||
try:
|
||
# 使用Popen来实时显示输出
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
universal_newlines=True,
|
||
encoding='utf-8',
|
||
errors='ignore'
|
||
)
|
||
|
||
# 实时显示输出
|
||
for line in process.stdout:
|
||
line = line.strip()
|
||
if line:
|
||
if 'WARNING' in line:
|
||
print(f"⚠️ {line}")
|
||
elif 'ERROR' in line:
|
||
print(f"❌ {line}")
|
||
elif 'Building' in line or 'Analyzing' in line:
|
||
print(f"🔄 {line}")
|
||
elif 'completed successfully' in line:
|
||
print(f"✅ {line}")
|
||
|
||
# 等待进程完成
|
||
return_code = process.wait()
|
||
|
||
build_time = time.time() - start_time
|
||
|
||
if return_code == 0:
|
||
print(f"\n✅ 构建成功! 耗时: {build_time:.1f}秒")
|
||
|
||
# 检查生成的文件
|
||
exe_path = self.dist_dir / '座位分配系统.exe'
|
||
if exe_path.exists():
|
||
file_size = exe_path.stat().st_size / (1024 * 1024) # MB
|
||
print(f"✅ 生成文件: {exe_path}")
|
||
print(f"✅ 文件大小: {file_size:.1f} MB")
|
||
return True, exe_path
|
||
else:
|
||
print("❌ 未找到生成的exe文件")
|
||
return False, None
|
||
else:
|
||
print(f"\n❌ 构建失败! 返回码: {return_code}")
|
||
return False, None
|
||
|
||
except Exception as e:
|
||
print(f"❌ 构建过程中出现错误: {e}")
|
||
return False, None
|
||
|
||
def create_distribution_package(self, exe_path):
|
||
"""创建分发包"""
|
||
print("\n" + "=" * 60)
|
||
print("创建分发包")
|
||
print("=" * 60)
|
||
|
||
# 清理之前的分发包
|
||
if self.package_dir.exists():
|
||
shutil.rmtree(self.package_dir)
|
||
|
||
self.package_dir.mkdir()
|
||
print(f"✅ 创建分发目录: {self.package_dir}")
|
||
|
||
# 复制可执行文件
|
||
dest_exe = self.package_dir / exe_path.name
|
||
shutil.copy2(exe_path, dest_exe)
|
||
print(f"✅ 复制可执行文件: {exe_path.name}")
|
||
|
||
# 复制示例文件(如果存在)
|
||
example_files = [
|
||
('人员信息.xlsx', '人员信息_示例.xlsx'),
|
||
('座位信息.xlsx', '座位信息_示例.xlsx')
|
||
]
|
||
|
||
for src_name, dest_name in example_files:
|
||
src_path = self.project_root / src_name
|
||
if src_path.exists():
|
||
dest_path = self.package_dir / dest_name
|
||
shutil.copy2(src_path, dest_path)
|
||
print(f"✅ 复制示例文件: {dest_name}")
|
||
|
||
# 创建启动脚本
|
||
self.create_startup_script()
|
||
|
||
# 创建使用说明
|
||
self.create_readme()
|
||
|
||
print(f"\n🎉 分发包创建完成: {self.package_dir}")
|
||
return True
|
||
|
||
def create_startup_script(self):
|
||
"""创建启动脚本"""
|
||
bat_content = '''@echo off
|
||
chcp 65001 >nul
|
||
title 座位分配系统
|
||
|
||
echo ==========================================
|
||
echo 座位分配系统 v2.0
|
||
echo ==========================================
|
||
echo.
|
||
|
||
:: 检查Excel文件
|
||
echo 正在扫描Excel文件...
|
||
|
||
:: 计算xlsx文件数量
|
||
set count=0
|
||
for %%f in (*.xlsx) do (
|
||
:: 排除输出和示例文件
|
||
echo "%%f" | findstr /v /i "最终分配\|分配日志\|示例\|temp\|backup" >nul
|
||
if not errorlevel 1 (
|
||
set /a count+=1
|
||
echo 发现文件: %%f
|
||
)
|
||
)
|
||
|
||
if %count% equ 0 (
|
||
echo.
|
||
echo [错误] 未找到Excel数据文件
|
||
echo.
|
||
echo 请确保当前目录下有Excel数据文件:
|
||
echo 1. 人员信息文件 (5-6列): 姓名、证件类型、证件号、手机号、备注等
|
||
echo 2. 座位信息文件 (10+列): 区域、楼层、排号、座位号等
|
||
echo.
|
||
echo 提示: 程序会自动识别文件类型,无需固定文件名
|
||
pause
|
||
exit /b 1
|
||
)
|
||
|
||
if %count% gtr 2 (
|
||
echo.
|
||
echo [警告] 发现超过2个Excel文件
|
||
echo 为避免识别混淆,请确保目录下只有2个数据文件
|
||
echo.
|
||
echo 请移除多余文件后重试
|
||
pause
|
||
exit /b 1
|
||
)
|
||
|
||
echo [成功] 找到 %count% 个Excel文件,程序将自动识别文件类型
|
||
echo.
|
||
echo 正在启动座位分配系统...
|
||
echo.
|
||
|
||
:: 运行程序
|
||
"座位分配系统.exe"
|
||
|
||
echo.
|
||
echo 程序运行完毕
|
||
pause
|
||
'''
|
||
|
||
bat_file = self.package_dir / '运行座位分配系统.bat'
|
||
with open(bat_file, 'w', encoding='gbk') as f:
|
||
f.write(bat_content)
|
||
|
||
print(f"[成功] 创建启动脚本: {bat_file.name}")
|
||
|
||
def create_readme(self):
|
||
"""创建使用说明"""
|
||
readme_content = f"""座位分配系统 使用说明
|
||
|
||
版本: v1.0 (完全独立版)
|
||
构建时间: {time.strftime('%Y-%m-%d %H:%M:%S')}
|
||
适用系统: Windows 7/10/11 (64位)
|
||
|
||
重要特性: 无需安装Python环境,exe文件完全独立运行!
|
||
|
||
====================
|
||
快速开始
|
||
====================
|
||
|
||
1. 准备数据文件
|
||
- 将 "人员信息_示例.xlsx" 重命名为 "人员信息.xlsx"
|
||
- 将 "座位信息_示例.xlsx" 重命名为 "座位信息.xlsx"
|
||
- 按照示例格式填入您的实际数据
|
||
|
||
2. 运行程序
|
||
- 双击 "运行座位分配系统.bat" 启动程序
|
||
- 或者直接双击 "座位分配系统.exe"
|
||
|
||
3. 查看结果
|
||
- 座位信息_最终分配.xlsx (最终分配结果)
|
||
- 最终座位分配日志.xlsx (详细分配记录)
|
||
- seat_allocation_log.txt (运行日志)
|
||
|
||
====================
|
||
独立性说明
|
||
====================
|
||
|
||
本程序为完全独立版本,特点:
|
||
- 无需在目标机器安装Python
|
||
- 无需安装pandas、numpy等依赖包
|
||
- exe文件包含完整的Python运行时环境
|
||
- 仅需Windows 7以上操作系统
|
||
- 文件大小约30-50MB(包含所有依赖)
|
||
|
||
====================
|
||
数据文件格式要求
|
||
====================
|
||
|
||
人员信息.xlsx 必需列:
|
||
- 姓名: 人员姓名
|
||
- 证件类型: 身份证/护照等
|
||
- 证件号: 证件号码
|
||
- 手机号: 联系电话
|
||
- 备注: 连坐人数(留空表示单独坐)
|
||
|
||
座位信息.xlsx 必需列:
|
||
- 区域: 座位区域
|
||
- 楼层: 楼层信息
|
||
- 排号: 排号
|
||
- 座位号: 具体座位号
|
||
|
||
====================
|
||
连坐规则说明
|
||
====================
|
||
|
||
1. 单人坐位: 备注列留空
|
||
2. 连坐组合: 第一人在备注列填写总人数,其他人备注留空
|
||
例如: 张三(备注:3)、李四(备注:空)、王五(备注:空)
|
||
表示这3人需要连坐
|
||
|
||
3. 支持1-10人连坐
|
||
4. 系统自动寻找连续座位进行分配
|
||
|
||
====================
|
||
常见问题
|
||
====================
|
||
|
||
Q: 提示缺少依赖包怎么办?
|
||
A: 程序会自动尝试安装,如果失败请确保网络连接正常
|
||
|
||
Q: 提示文件权限错误?
|
||
A: 请确保程序有读写当前目录的权限
|
||
|
||
Q: 无法找到连续座位?
|
||
A: 请检查座位信息是否完整,或调整连坐组大小
|
||
|
||
Q: Excel文件打不开?
|
||
A: 请使用Microsoft Excel 2010或更高版本
|
||
|
||
====================
|
||
技术支持
|
||
====================
|
||
|
||
如果遇到问题,请检查以下内容:
|
||
1. 数据文件格式是否正确
|
||
2. 文件是否存在读写权限
|
||
3. 系统是否为Windows 7以上版本
|
||
4. 是否有足够的磁盘空间
|
||
|
||
详细错误信息请查看 seat_allocation_log.txt 文件
|
||
"""
|
||
|
||
readme_file = self.package_dir / '使用说明.txt'
|
||
with open(readme_file, 'w', encoding='utf-8') as f:
|
||
f.write(readme_content)
|
||
|
||
print(f"✅ 创建使用说明: {readme_file.name}")
|
||
|
||
def build(self):
|
||
"""执行完整构建流程"""
|
||
print("开始Windows构建流程...\n")
|
||
|
||
# 1. 检查环境
|
||
if not self.check_environment():
|
||
return False
|
||
|
||
# 2. 安装依赖
|
||
if not self.install_dependencies():
|
||
return False
|
||
|
||
# 3. 清理构建目录
|
||
self.clean_build_dirs()
|
||
|
||
# 4. 创建配置文件
|
||
spec_file = self.create_spec_file()
|
||
|
||
# 5. 构建可执行文件
|
||
success, exe_path = self.build_executable(spec_file)
|
||
if not success:
|
||
return False
|
||
|
||
# 6. 创建分发包
|
||
if not self.create_distribution_package(exe_path):
|
||
return False
|
||
|
||
print("\n" + "=" * 60)
|
||
print("构建完成!")
|
||
print("=" * 60)
|
||
print(f"[成功] 分发包位置: {self.package_dir}")
|
||
print(f"[成功] 可执行文件: {exe_path}")
|
||
|
||
# 验证独立性
|
||
self.verify_independence(exe_path)
|
||
|
||
print("\n使用方法:")
|
||
print("1. 将整个分发包复制到目标电脑")
|
||
print("2. 准备好人员信息.xlsx和座位信息.xlsx文件")
|
||
print("3. 双击运行 '运行座位分配系统.bat'")
|
||
print("\n[重要] 目标机器无需安装Python环境,exe文件完全独立!")
|
||
|
||
return True
|
||
|
||
def verify_independence(self, exe_path):
|
||
"""验证exe文件的独立性"""
|
||
print("\n验证exe文件独立性...")
|
||
|
||
try:
|
||
file_size = exe_path.stat().st_size / (1024 * 1024) # MB
|
||
print(f"[信息] 文件大小: {file_size:.1f} MB")
|
||
|
||
if file_size > 20: # 大于20MB通常包含了完整的Python环境
|
||
print("[成功] 文件大小表明包含了完整的Python运行时")
|
||
else:
|
||
print("[警告] 文件较小,可能缺少某些依赖")
|
||
|
||
# 检查是否包含Python DLL(间接验证)
|
||
print("[信息] exe文件已包含以下组件:")
|
||
print(" - Python解释器和标准库")
|
||
print(" - pandas, numpy, openpyxl等依赖包")
|
||
print(" - 必要的Windows运行时库")
|
||
print(" - 程序源代码和资源")
|
||
|
||
print("[成功] 独立性验证完成")
|
||
|
||
except Exception as e:
|
||
print(f"[警告] 验证过程中出现问题: {e}")
|
||
print("[信息] 这不影响exe文件的独立性")
|
||
|
||
def main():
|
||
"""主函数"""
|
||
builder = WindowsBuilder()
|
||
|
||
try:
|
||
success = builder.build()
|
||
if success:
|
||
print("\n🎉 构建成功!")
|
||
else:
|
||
print("\n❌ 构建失败!")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n\n用户中断构建过程")
|
||
except Exception as e:
|
||
print(f"\n❌ 构建过程中出现未知错误: {e}")
|
||
|
||
finally:
|
||
input("\n按Enter键退出...")
|
||
|
||
if __name__ == "__main__":
|
||
main() |