python制作的软件工具安装包
思考:
- 如何将制作的软件工具分发给用户?
- 打成压缩包分发给用户;
- 制作安装程序
- 本文介绍了一种将软件工具打包为独立安装程序的方法。核心思路是将目录文件转换为Base64编码的JSON文件,再通过解码程序还原为可执行文件。
内容说明
主要包含两个Python脚本:directory_to_base64.py将指定目录转换为包含文件内容的JSON文件;base64_to_directory.py则执行反向操作,将JSON还原为目录结构。文章详细说明了使用PyInstaller打包时的注意事项,包括资源文件路径处理、版本信息配置等,并提供了完整的示例代码和spec文件配置模板。这种方法适用于需要将多个文件打包为单一可执行安装程序的场景。
核心代码
'''directory_to_base64.py'''import os
import base64
import json
import argparse
from pathlib import Pathdef file_to_base64(file_path):"""将文件内容转换为 Base64 编码"""try:with open(file_path, 'rb') as file:content = file.read()return base64.b64encode(content).decode('utf-8')except Exception as e:print(f"Error reading file {file_path}: {e}")return Nonedef directory_to_dict(dir_path):"""递归将目录结构转换为字典"""result = {}for item in os.listdir(dir_path):item_path = os.path.join(dir_path, item)if os.path.isfile(item_path):base64_content = file_to_base64(item_path)if base64_content is not None:result[item] = {'type': 'file','content': base64_content}elif os.path.isdir(item_path):result[item] = {'type': 'directory','content': directory_to_dict(item_path)}return resultdef main():parser = argparse.ArgumentParser(description='Convert a directory and its contents to Base64 encoded JSON')parser.add_argument('--input', '-i', required=True, help='Input directory path')parser.add_argument('--output', '-o', required=True, help='Output JSON file path')args = parser.parse_args()input_dir = args.inputoutput_file = args.outputif not os.path.isdir(input_dir):print(f"Error: Input directory '{input_dir}' does not exist or is not a directory.")returntry:# 创建输出文件的目录(如果不存在)output_dir = os.path.dirname(output_file)if output_dir:os.makedirs(output_dir, exist_ok=True)# 转换目录结构为字典dir_dict = directory_to_dict(input_dir)# 添加元数据metadata = {'root_directory': os.path.basename(os.path.abspath(input_dir)),'total_files': sum(1 for _ in Path(input_dir).rglob('*') if os.path.isfile(_)),'total_directories': sum(1 for _ in Path(input_dir).rglob('*') if os.path.isdir(_))}result = {'metadata': metadata,'content': dir_dict}# 写入 JSON 文件with open(output_file, 'w') as f:json.dump(result, f, indent=2)print(f"Successfully converted directory '{input_dir}' to Base64 encoded JSON in '{output_file}'.")except Exception as e:print(f"An error occurred: {e}")if __name__ == "__main__":main()
'''base64_to_directory.py'''import os
import base64
import json
import argparse
import sys
import pyexpatfrom pyexpat.errors import messagesdef decode_base64_to_file(base64_content, output_path):"""将 Base64 编码内容写入文件"""try:with open(output_path, 'wb') as file:file.write(base64.b64decode(base64_content))return Trueexcept Exception as e:print(f"Error writing file {output_path}: {e}")return Falsedef process_directory(content_dict, output_dir):"""递归处理目录结构并还原文件"""for item_name, item_data in content_dict.items():item_path = os.path.join(output_dir, item_name)if item_data['type'] == 'file':# 创建文件base64_content = item_data['content']if decode_base64_to_file(base64_content, item_path):print(f"Created file: {item_path}")elif item_data['type'] == 'directory':# 创建目录os.makedirs(item_path, exist_ok=True)print(f"Created directory: {item_path}")# 递归处理子目录process_directory(item_data['content'], item_path)def resource_path(relative_path):"""获取资源的绝对路径,适应打包后的环境"""if hasattr(sys, '_MEIPASS'):# 打包后的环境return os.path.join(sys._MEIPASS, relative_path)# 开发环境return os.path.join(os.path.abspath("."), relative_path)def main():""" parser = argparse.ArgumentParser(description='Decode a Base64 encoded JSON file back to directory structure')parser.add_argument('--input', '-i', required=True, help='Input JSON file path')parser.add_argument('--output', '-o', required=True, help='Output directory path')args = parser.parse_args()"""# 使用示例input_file = resource_path("input.json")output_dir = "./Tool""""if not os.path.isfile(input_file):print(f"Error: Input file '{input_file}' does not exist or is not a file.")return
"""try:# 读取 JSON 文件with open(input_file, 'r') as f:data = json.load(f)# 创建输出目录(如果不存在)os.makedirs(output_dir, exist_ok=True)# 获取元数据metadata = data.get('metadata', {})root_directory = metadata.get('root_directory', '')total_files = metadata.get('total_files', 0)total_dirs = metadata.get('total_directories', 0)print(f"Decoding directory: {root_directory}")print(f"Total files expected: {total_files}")print(f"Total directories expected: {total_dirs}")print(f"Output location: {output_dir}")print("Starting decoding process...")# 处理内容content_dict = data.get('content', {})process_directory(content_dict, output_dir)print("Decoding completed successfully!")except Exception as e:print(f"An error occurred: {e}")if __name__ == "__main__":main()
#base64_to_directory.spec# -*- mode: python ; coding: utf-8 -*-a = Analysis(['D:\\workSpace\\python_work\\learn\\STtest\\build\\base64_to_directory.py'],pathex=[],binaries=[],datas=[('input.json', '.')],hiddenimports=[],hookspath=[],hooksconfig={},runtime_hooks=[],excludes=[],noarchive=False,optimize=0,
)
pyz = PYZ(a.pure)exe = EXE(pyz,a.scripts,a.binaries,a.datas,[],name='csvfileBatchGenerationToolInstall',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,upx_exclude=[],runtime_tmpdir=None,console=True,disable_windowed_traceback=False,argv_emulation=False,target_arch=None,codesign_identity=None,entitlements_file=None,version='version.txt' # 指定版本信息文件
)
# version.txt
# 版本信息文件
VSVersionInfo(ffi=FixedFileInfo(filevers=(1, 0, 0, 0),prodvers=(1, 0, 0, 0),mask=0x3f,flags=0x0,OS=0x40004,fileType=0x1,subtype=0x0,date=(0, 0)),kids=[StringFileInfo([StringTable('080404B0',[StringStruct('CompanyName', ''),StringStruct('FileDescription','工具安装程序'),StringStruct('FileVersion', '1.0.0'),StringStruct('InternalName', 'csv_data_install'),StringStruct('LegalCopyright', '© 2025 Company. All rights reserved.'),StringStruct('OriginalFilename', 'install.exe'),StringStruct('ProductName', 'ToolInstall'),StringStruct('ProductVersion', '1.0.0')])]),VarFileInfo([VarStruct('Translation', [2052, 1200]) # 2052=中文])]
)
注意事项
input.json文件作为数据资源文件,需要通过os.path.join(sys._MEIPASS, relative_path)设定打包后的路径。datas=[('input.json', '.')]指定资源文件(‘原路径’,‘目标路径’)。执行命令pyinstaller "spec文件路径"打包。