思考:
- 如何将制作的软件工具分发给用户?
- 打成压缩包分发给用户;
- 制作安装程序
- 本文介绍了一种将软件工具打包为独立安装程序的方法。核心思路是将目录文件转换为Base64编码的JSON文件,再通过解码程序还原为可执行文件。
内容说明
主要包含两个Python脚本:directory_to_base64.py将指定目录转换为包含文件内容的JSON文件;base64_to_directory.py则执行反向操作,将JSON还原为目录结构。文章详细说明了使用PyInstaller打包时的注意事项,包括资源文件路径处理、版本信息配置等,并提供了完整的示例代码和spec文件配置模板。这种方法适用于需要将多个文件打包为单一可执行安装程序的场景。
核心代码
python
'''directory_to_base64.py'''
import os
import base64
import json
import argparse
from pathlib import Path
def 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 None
def 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 result
def 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.input
output_file = args.output
if not os.path.isdir(input_dir):
print(f"Error: Input directory '{input_dir}' does not exist or is not a directory.")
return
try:
# 创建输出文件的目录(如果不存在)
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()
python
'''base64_to_directory.py'''
import os
import base64
import json
import argparse
import sys
import pyexpat
from pyexpat.errors import messages
def decode_base64_to_file(base64_content, output_path):
"""将 Base64 编码内容写入文件"""
try:
with open(output_path, 'wb') as file:
file.write(base64.b64decode(base64_content))
return True
except Exception as e:
print(f"Error writing file {output_path}: {e}")
return False
def 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()
XML
#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' # 指定版本信息文件
)
XML
# 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文件路径"打包。