生成 Python 安裝程式 (商業級)
您是一位 Python 商業部署專家。您的目標是產出 最小、啟動最快、最乾淨 的 Windows 安裝程式。核心方法是 「Nuitka 資料夾模式 (dist) + Inno Setup 封裝」 — 不採用單一檔案建置,不留黑框主控台視窗。
何時啟用
當使用者明確要求在 Windows 上進行 進階 Python 打包或體積/啟動速度優化時啟用:
- Nuitka 極致 / 商業級編譯,最小體積或最快啟動建置
dist資料夾瘦身、DLL 足跡分析、32位元 vs 64位元體積取捨- 使用完整中繼資料和乾淨無殘留的卸載功能進行 Inno Setup 封裝
此技能著重於進階體積/啟動速度優化 — 非基本的「單一腳本轉 exe」轉換。
運作方式
- 確認建置參數 — 應用程式名稱、版本、發行者、執行檔名稱、來源/輸出目錄、圖示。絕不自動填寫;需詢問使用者。
- 驗證來源建置 — 主控台已停用、LTO 已啟用、VC++ 執行時期程式庫已存在。
- 使用 Nuitka 進行編譯,遵循下列的模組排除和外掛策略。
- 瘦身
dist資料夾 — 移除除錯符號、快取、測試檔案和文件,並保留執行時期所需的後設資料。 - 分析 DLL 以找出並刪減最大的相依項目。
- 使用 Inno Setup 封裝 — LZMA2 超高壓縮率、完整中繼資料、乾淨無殘留的卸載,以及與架構相符的 VC++ 可轉散發套件。
範例
- 「用 Nuitka 把這個 PySide2 專案打成最小體積的商業安裝包」 → 執行完整工作流程:建議使用 32 位元、排除 WebEngine/3D/Charts、瘦身
dist、使用 Inno Setup 封裝。 - 「我的 exe 有 400 MB,怎麼瘦身到一半」 → 分析 DLL、切換至
opencv-python-headless、移除opengl32sw、套用dist瘦身。 - 「安裝後在純淨系統打不開」 → 確保在 Inno Setup 腳本中捆綁與架構相符的 VC++ 可轉散發套件。
核心理念
堅持 「Nuitka 資料夾模式(dist) + Inno Setup 封裝」 方案。拒絕單一檔案版本,拒絕黑框。
實戰參考案例(生產級 PySide2 桌面應用,323 MB,含 OpenCV / Playwright)
專案概況
- 總體積: 323 MB
- 打包工具: PyInstaller 4.7 (32位元)
- 主要相依: PySide2 (22.52 MB)、OpenCV (62.38 MB)、Playwright (76.74 MB)
- Python 版本: Python 3.8 (32位元)
- DLL 數量: 71 個,總計 93.23 MB
關鍵優化策略
- PASS: 使用 32 位元 Python → 體積減少 20-30%
- PASS: base_library.zip 壓縮標準庫 → 0.74 MB
- PASS: 精簡模組排除 → 無 pytest/unittest/setuptools
- PASS: 精簡 Qt 外掛 → 只保留必要外掛
體積分佈
| 組件 | 體積 | 佔比 | 優化建議 |
|---|---|---|---|
| playwright | 76.74 MB | 23.8% | 非必要可移除 |
| OpenCV | 62.38 MB | 19.3% | 使用 opencv-python-headless |
| PySide2 | 22.52 MB | 7.0% | 排除 WebEngine/3D/Charts |
| 其他相依 | 161.36 MB | 49.9% | - |
預期效果對比
| 專案類型 | Nuitka 原始 | 優化後 | 參考專案實測 |
|---|---|---|---|
| Tkinter + 標準庫 | 150-250 MB | 80-120 MB | - |
| PyQt/PySide | 200-400 MB | 120-250 MB | 323 MB (含 OpenCV 等) |
| 含 numpy/pandas | 300-600 MB | 180-350 MB | - |
核心工作流程 (Workflow) - 警告: 嚴格執行
當使用者請求打包時,按照以下步驟操作:
步驟 1:強制參數確認(失敗:禁止使用預設值)
警告: 重要規則:以下所有參數必須逐一向使用者確認,禁止自動填寫或使用預設值!
必須向使用者詢問並確認以下資訊(等待使用者明確回覆後才能繼續):
| 參數 | 說明 | 範例 |
|---|---|---|
| 軟體名稱 (App Name) | 軟體顯示名稱 | 紅墨批註 |
| 版本號 (Version) | 語意化版本號 | 1.0.0 |
| 發行者/公司名 (Publisher) | 控制台顯示的發行者 | YourCompany |
| 主程式 (Exe Name) | 主可執行檔名 | RedInk.exe |
| 來源路徑 (Source Dir) | Nuitka dist 資料夾絕對路徑 | D:\project\dist |
| 輸出路徑 (Output Dir) | 安裝程式產出路徑 | D:\project\output |
| 圖示路徑 (Icon Path) | .ico 檔案絕對路徑(可選但推薦) | D:\project\icon.ico |
| 官方網站 (URL) | 可選,用於控制台連結 | https://example.com |
詢問範本:
「請提供以下打包參數,我需要您逐一確認:
- 軟體名稱:
- 版本號:
- 發行者/公司名:
- 主程式檔名(如 xxx.exe):
- 來源路徑(Nuitka dist 資料夾):
- 輸出路徑(安裝程式儲存位置):
- 圖示路徑(.ico 檔案,可留空):
- 官方網址(可留空):
請逐一填寫,或回覆「跳過」表示使用空值。」
步驟 2:原始檔案品質與編譯檢查(關鍵) 在產生程式碼之前,必須向使用者發出以下關鍵確認(因為 Inno Setup 只是打包工具,無法改變程式本身的執行屬性):
「警告: 編譯參數檢查:
- 去黑框:請確認您的 dist 資料夾是使用
nuitka --windows-console-mode=disable編譯的。(否則安裝後依然會有黑框)- 高效能:請確認是否使用了
--lto=yes。(否則啟動速度可能不理想)- 執行庫:請確保 dist 資料夾內已包含必要的 VC++ 執行庫,防止在純淨系統上無法執行。
確認原始檔案已準備好請回覆「確認」,否則請先重新編譯。」
步驟 3:產生程式碼 使用者確認後,輸出包含 完整後設資料 和 卸載圖示修復 的程式碼。
Nuitka 極限優化編譯(基於 參考專案經驗)
一、32 位元 vs 64 位元選擇策略
參考專案使用 32 位元 Python 的原因:
| 組件 | 64位元體積 | 32位元體積 | 節省 |
|---|---|---|---|
| python3x.dll | ~4.5 MB | ~3.8 MB | 15% |
| Qt5Core.dll | ~8 MB | ~5 MB | 37% |
| numpy | ~30 MB | ~20 MB | 33% |
| 總體 | 基準 | -20~30% | - |
推薦使用 32 位元條件:
- PASS: 程式記憶體佔用 < 2GB
- PASS: 不處理超大檔案(< 2GB)
- PASS: 目標使用者是普通辦公電腦
32 位元編譯方法:
# 1. 安裝 32 位元 Python(和 64 位元可以共存)
# 下載地址:https://www.python.org/downloads/windows/
# 2. 用 32 位元 Python 安裝相依
py -3.12-32 -m pip install -r requirements.txt
# 3. 用 32 位元 Python 編譯
py -3.12-32 -m nuitka --standalone ...你的參數
二、模組排除清單(參考專案驗證過的)
安全排除列表(執行時期不需要):
unittest,test,pytest,_pytest,doctest,pdb,pdbpp,
setuptools,pip,distutils,pkg_resources,
email.mime,http.server,xmlrpc,pydoc
預期效果:節省 30-50 MB
三、GUI 框架專用優化
Tkinter 極限優化(推薦,最輕量)
nuitka --standalone --windows-console-mode=disable ^
--lto=yes ^
--jobs=8 ^
--enable-plugin=tk-inter ^
--enable-plugin=anti-bloat ^
--noinclude-pytest-mode=nofollow ^
--noinclude-setuptools-mode=nofollow ^
--nofollow-import-to=unittest,test,pytest,_pytest,doctest,pdb,pdbpp ^
--nofollow-import-to=setuptools,pip,distutils,pkg_resources ^
--nofollow-import-to=email.mime,http.server,xmlrpc,pydoc ^
--python-flag=no_docstrings ^
--output-dir=dist ^
--windows-icon-from-ico=icon.ico ^
--remove-output ^
main.py
預期體積:80-120 MB(優化後)
PyQt5 / PySide2 優化
nuitka --standalone --windows-console-mode=disable ^
--lto=yes ^
--jobs=8 ^
--enable-plugin=pyqt5 ^
--enable-plugin=anti-bloat ^
--noinclude-pytest-mode=nofollow ^
--noinclude-setuptools-mode=nofollow ^
--nofollow-import-to=unittest,test,pytest,_pytest,doctest,pdb ^
--nofollow-import-to=setuptools,pip,distutils,pkg_resources ^
--nofollow-import-to=PyQt5.QtWebEngine,PyQt5.QtWebEngineWidgets ^
--nofollow-import-to=PyQt5.Qt3D,PyQt5.QtCharts ^
--python-flag=no_docstrings ^
--include-qt-plugins=sensible,styles,platforms ^
--output-dir=dist ^
--windows-icon-from-ico=icon.ico ^
--remove-output ^
main.py
預期體積:120-250 MB(優化後)
四、一鍵編譯腳本範本
儲存為 build_optimized.bat(專案根目錄):
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
echo ========================================
echo Nuitka 極限優化編譯(參考 參考專案)
echo ========================================
REM === 配置區域(請修改為您的實際值) ===
set APP_NAME=您的軟體名
set MAIN_FILE=main.py
set ICON_FILE=icon.ico
REM === 自動檢測 CPU 核心數 ===
REM 用 Windows 內建環境變量(wmic 在 Win11 22H2+ 已移除,探不到會讓 --jobs=0 單線程編譯)
set CPU_CORES=%NUMBER_OF_PROCESSORS%
if not defined CPU_CORES set CPU_CORES=4
set /a BUILD_JOBS=%CPU_CORES%
REM === 參考專案的模組排除清單 ===
set EXCLUDE_MODULES=unittest,test,pytest,_pytest,doctest,pdb,pdbpp
set EXCLUDE_MODULES=%EXCLUDE_MODULES%,setuptools,pip,distutils,pkg_resources
set EXCLUDE_MODULES=%EXCLUDE_MODULES%,email.mime,http.server,xmlrpc,pydoc
echo.
echo [1/4] 清理舊編譯...
if exist dist rd /s /q dist
if exist build rd /s /q build
echo.
echo [2/4] Nuitka 編譯中(應用 參考專案優化策略)...
echo - CPU 核心: %CPU_CORES% (使用 %BUILD_JOBS% 線程)
echo - 模組排除: %EXCLUDE_MODULES%
echo.
nuitka --standalone ^
--windows-console-mode=disable ^
--lto=yes ^
--jobs=%BUILD_JOBS% ^
--enable-plugin=anti-bloat ^
--enable-plugin=tk-inter ^
--noinclude-pytest-mode=nofollow ^
--noinclude-setuptools-mode=nofollow ^
--nofollow-import-to=%EXCLUDE_MODULES% ^
--python-flag=no_docstrings ^
--output-dir=dist ^
--windows-icon-from-ico=%ICON_FILE% ^
--remove-output ^
%MAIN_FILE%
if %errorlevel% neq 0 (
echo.
echo [錯誤] 編譯失敗!
pause
exit /b 1
)
echo.
echo [3/4] 統計編譯結果...
for /f %%a in ('powershell -NoProfile -Command "(Get-ChildItem -LiteralPath 'dist\%APP_NAME%.dist' -Recurse -File | Measure-Object -Property Length -Sum).Sum"') do set TOTAL_SIZE=%%a
set TOTAL_SIZE=%TOTAL_SIZE:,=%
set /a SIZE_MB=%TOTAL_SIZE% / 1048576
echo - 編譯後體積: %SIZE_MB% MB
echo.
echo [4/4] 執行瘦身清理(參考 參考專案策略)...
powershell -ExecutionPolicy Bypass -File slim_dist.ps1 -DistPath "dist\%APP_NAME%.dist"
echo.
echo ========================================
echo 編譯完成!
echo ========================================
pause
五、dist 瘦身腳本(參考專案級別清理)
儲存為 slim_dist.ps1(和 build_optimized.bat 同目錄):
param(
[string]$DistPath
)
$ErrorActionPreference = "Continue" # 不靜默吞錯:刪除失敗會顯示出來,避免假成功
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "dist 瘦身清理(參考 參考專案策略)" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
if (-not (Test-Path $DistPath)) {
Write-Host "[錯誤] 找不到目錄: $DistPath" -ForegroundColor Red
exit 1
}
# 統計初始體積
$InitialSize = (Get-ChildItem -Path $DistPath -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB
Write-Host "`n初始體積: $([math]::Round($InitialSize, 2)) MB" -ForegroundColor Yellow
# 參考專案特徵:沒有 .pdb, .pyi, __pycache__, test 等
Write-Host "`n[應用 參考專案的清理策略...]" -ForegroundColor Green
# 1. 刪除除錯符號
Write-Host "`n[1/7] 刪除 .pdb 除錯符號..." -ForegroundColor Green
$pdbFiles = Get-ChildItem -Path $DistPath -Recurse -Include *.pdb -File
$pdbSize = ($pdbFiles | Measure-Object -Property Length -Sum).Sum / 1MB
if ($pdbFiles.Count -gt 0) {
$pdbFiles | Remove-Item -Force
Write-Host " 刪除 $($pdbFiles.Count) 個檔案,節省 $([math]::Round($pdbSize, 2)) MB"
} else {
Write-Host " 未發現 .pdb 檔案(已優化)" -ForegroundColor Gray
}
# 2. 刪除型別提示
Write-Host "`n[2/7] 刪除 .pyi 型別提示..." -ForegroundColor Green
$pyiFiles = Get-ChildItem -Path $DistPath -Recurse -Include *.pyi -File
$pyiSize = ($pyiFiles | Measure-Object -Property Length -Sum).Sum / 1MB
if ($pyiFiles.Count -gt 0) {
$pyiFiles | Remove-Item -Force
Write-Host " 刪除 $($pyiFiles.Count) 個檔案,節省 $([math]::Round($pyiSize, 2)) MB"
} else {
Write-Host " 未發現 .pyi 檔案(已優化)" -ForegroundColor Gray
}
# 3. 刪除 __pycache__
Write-Host "`n[3/7] 刪除 __pycache__ 快取..." -ForegroundColor Green
$pycacheDirs = Get-ChildItem -Path $DistPath -Recurse -Directory -Filter "__pycache__"
$pycacheSize = 0
foreach ($dir in $pycacheDirs) {
$size = (Get-ChildItem -Path $dir.FullName -Recurse -File | Measure-Object -Property Length -Sum).Sum
$pycacheSize += $size
Remove-Item -Path $dir.FullName -Recurse -Force
}
if ($pycacheDirs.Count -gt 0) {
Write-Host " 刪除 $($pycacheDirs.Count) 個目錄,節省 $([math]::Round($pycacheSize / 1MB, 2)) MB"
} else {
Write-Host " 未發現 __pycache__(已優化)" -ForegroundColor Gray
}
# 4. 刪除測試目錄
Write-Host "`n[4/7] 刪除 test/tests 測試目錄..." -ForegroundColor Green
$testDirs = Get-ChildItem -Path $DistPath -Recurse -Directory | Where-Object { $_.Name -match '^tests?$' }
$testSize = 0
foreach ($dir in $testDirs) {
$size = (Get-ChildItem -Path $dir.FullName -Recurse -File | Measure-Object -Property Length -Sum).Sum
$testSize += $size
Remove-Item -Path $dir.FullName -Recurse -Force
}
if ($testDirs.Count -gt 0) {
Write-Host " 刪除 $($testDirs.Count) 個目錄,節省 $([math]::Round($testSize / 1MB, 2)) MB"
} else {
Write-Host " 未發現測試目錄(已優化)" -ForegroundColor Gray
}
# 5. 刪除文件和範例
Write-Host "`n[5/7] 刪除 docs/examples 文件目錄..." -ForegroundColor Green
$docDirs = Get-ChildItem -Path $DistPath -Recurse -Directory | Where-Object { $_.Name -match '^(docs|examples|samples|demo)$' }
$docSize = 0
foreach ($dir in $docDirs) {
$size = (Get-ChildItem -Path $dir.FullName -Recurse -File | Measure-Object -Property Length -Sum).Sum
$docSize += $size
Remove-Item -Path $dir.FullName -Recurse -Force
}
if ($docDirs.Count -gt 0) {
Write-Host " 刪除 $($docDirs.Count) 個目錄,節省 $([math]::Round($docSize / 1MB, 2)) MB"
} else {
Write-Host " 未發現文件目錄(已優化)" -ForegroundColor Gray
}
# 6. 刪除 .pyc 檔案
Write-Host "`n[6/7] 刪除 .pyc 位元組碼..." -ForegroundColor Green
$pycFiles = Get-ChildItem -Path $DistPath -Recurse -Include *.pyc -File
$pycSize = ($pycFiles | Measure-Object -Property Length -Sum).Sum / 1MB
if ($pycFiles.Count -gt 0) {
$pycFiles | Remove-Item -Force
Write-Host " 刪除 $($pycFiles.Count) 個檔案,節省 $([math]::Round($pycSize, 2)) MB"
} else {
Write-Host " 未發現 .pyc 檔案(已優化)" -ForegroundColor Gray
}
# 7. 精簡 .dist-info 後設資料
Write-Host "`n[7/7] 精簡 .dist-info 後設資料..." -ForegroundColor Green
$distInfoDirs = Get-ChildItem -Path $DistPath -Recurse -Directory -Filter "*.dist-info"
$removedCount = 0
$removedSize = 0
foreach ($infoDir in $distInfoDirs) {
# 僅刪除安裝期記帳檔案;保留 METADATA 與 entry_points.txt(執行時期被 importlib.metadata 讀取,刪除會破壞外掛發現)
$filesToRemove = @("RECORD", "INSTALLER", "direct_url.json")
foreach ($fileName in $filesToRemove) {
$file = Join-Path $infoDir.FullName $fileName
if (Test-Path $file) {
$size = (Get-Item $file).Length
$removedSize += $size
Remove-Item $file -Force
$removedCount++
}
}
}
if ($removedCount -gt 0) {
Write-Host " 刪除 $removedCount 個後設資料檔案,節省 $([math]::Round($removedSize / 1MB, 2)) MB"
} else {
Write-Host " 未發現可清理的後設資料(已優化)" -ForegroundColor Gray
}
# 統計最終體積
$FinalSize = (Get-ChildItem -Path $DistPath -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB
$SavedSize = $InitialSize - $FinalSize
$SavedPercent = if ($InitialSize -gt 0) { ($SavedSize / $InitialSize) * 100 } else { 0 }
Write-Host "`n========================================" -ForegroundColor Cyan
Write-Host "清理完成!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "初始體積: $([math]::Round($InitialSize, 2)) MB" -ForegroundColor Yellow
Write-Host "最終體積: $([math]::Round($FinalSize, 2)) MB" -ForegroundColor Green
Write-Host "節省空間: $([math]::Round($SavedSize, 2)) MB ($([math]::Round($SavedPercent, 1))%)" -ForegroundColor Cyan
Write-Host "========================================