Python exe文件打包神器-Nuitka!
那天晚上快十一点多,我还在公司楼下啃着一盒已经凉透的便当,我们组那个小李在群里喊:“东哥,运维又让把那个脚本打个 exe,说他们机器上不想装 Python…PyInstaller 打出来又大又慢,有没有别的招?”
我当时脑子一抽,说了句:要不你试试 Nuitka?结果他一句:“那是个啥玩意?”我发现,好像还真挺多人没好好用过这个东西,那就…我困归困,还是跟你们唠一唠。
先说清楚 Nuitka 到底是个啥
你可以把大部分“打包工具”想象成打包箱子:把 Python 解释器 + 你的源码 + 依赖,一股脑儿塞进一个目录,或者塞进一个大 exe,启动的时候再解压出来跑。
Nuitka不太一样,它是一个“编译器”:把 Python 代码翻译成 C 代码,再丢给 C 编译器(MSVC / gcc / clang 那些)去编译,最后得到一个真正的本地程序。
官方说得挺直白:支持 Python 2.6、2.7、3.4–3.13,好几个平台都能跑,Windows、Linux、macOS 都行,反正基本上哪儿有 Python 就能用哪儿。
当然,底下还是要带着 CPython 那套运行时代码,所以你别指望它像 Go 那种“一个小几 MB 的纯二进制”,体积该大还是大,只是原理不一样。这个对性能也有点帮助,尤其是启动速度、计算密集型那块,会比纯脚本跑快一截。
我一般跟新同学解释就一句话:
“PyInstaller 更像是把 Python 打包成一个搬家纸箱,Nuitka 更像是真的给你编成一个 C 程序,只是里面还藏着 Python 的灵魂。”
环境这一块,别一上来就踩坑
我先说最容易被骂的点:Nuitka 没有 C 编译器是干不了活的。
Windows 上:乖乖装 Visual Studio Build Tools(别全选,选 MSVC 和 Windows SDK 那几个就行) Linux:apt / yum 装 gcc、g++、make macOS: xcode-select --install搞定 command line tools
它本身就是个 Python 包,用 pip 装就完了(最好在虚拟环境里搞):
pip install -U nuitka
平时你可以这样用(命令行):
python -m nuitka your_script.py
但你刚说要“语言用 Python”,那我就顺手写个小的 build 脚本,后面你只用 python build.py 就能编译了,也舒服一点。
先整一个最简单的例子,感受下
假设你有个小工具 hello.py,内容特别朴素:
# hello.py
import timedefmain():
print("Hello, Nuitka!")
time.sleep(2)
print("打包成 exe 也就这回事~")
if __name__ == "__main__":
main()
然后你新建一个 build.py,专门用来调用 Nuitka 编译它:
# build.py
import subprocess
import sys
from pathlib import Pathdefbuild_hello():
project_root = Path(__file__).parent
script = project_root / "hello.py"
ifnot script.exists():
raise FileNotFoundError(f"没找到 {script},先确认下文件在不在~")
cmd = [
sys.executable, "-m", "nuitka",
"--follow-imports", # 跟踪依赖,别漏
"--onefile", # 生一个单文件 exe(下面会解释利弊)
"--enable-console", # 控制台程序
str(script),
]
print("准备执行编译命令:")
print(" ".join(cmd))
# check=True 出错直接抛异常,失败一眼能看到
subprocess.run(cmd, check=True)
if __name__ == "__main__":
build_hello()
你看,这里其实就是 Python 调用 Nuitka,当成普通模块用而已。这样你的 CI、打包脚本就不用记一条长命令了,所有参数都写在 Python 里,改起来也方便。
跑完之后,Windows 下一般会在当前目录生成个 hello.exe,Linux / macOS 就是个可执行文件。
standalone / onefile 这俩模式,到底选谁
Nuitka 的“打包姿势”主要有两种:
--mode=standalone(有时候写成--standalone)
会生成一个目录:里面有 exe + 一堆依赖文件 你打包给别人,就是整个目录一起打包 好处是启动快、调试方便,哪个 DLL 少了,一眼能看见
--mode=onefile(或者 --onefile)
看起来只生成一个 exe 运行的时候,会先把依赖解压到临时目录再启动 所以第一次启动会慢一点,尤其是体积大的项目
官方文档也建议:先让 standalone 正常跑起来,再尝试 onefile。不然你一上来就 onefile,报错的时候一堆临时目录、解压步骤,全是噪音。
如果你想先玩稳一点,可以把上面 build.py 里的 --onefile 改成:
"--standalone",
"--output-dir=build", # 指定输出目录
然后执行完你会看到一个 build/hello.dist/ 目录,里面全是运行需要的东西。发版的时候,你直接把这个目录打 zip 给用户就可以了。
顺带说句对比:和 PyInstaller 有啥不一样
我自己踩完几个项目的坑下来,大概是这么个感觉(纯个人体验哈):
PyInstaller 上手更轻松一点,小脚本、简单 GUI,基本一把过 Nuitka 对环境要求高点:C 编译器、Python 版本、一些插件参数得配好 但 Nuitka 编译出来的东西,启动速度和 CPU 跑满时的性能,明显更舒服些 你后面要做混淆、IP 保护,Nuitka 也有一堆付费/高级功能可以玩
还有一个现实问题:因为它要走 C 编译这一趟,对大型项目来说,编译时间会比 PyInstaller 长一些,这个得有心理准备,尤其是你在 CI 上做频繁构建的时候。
来个稍微像样点的小项目例子
你总不能只打包个打印“Hello”的玩具,对吧。假设你有个稍微像样的 CLI 工具,比如做个简单的“日志搜索小助手”,文件叫 logtool/main.py:
# logtool/main.py
import argparse
from pathlib import Pathdefsearch_log(file: Path, keyword: str):
ifnot file.exists():
print(f"文件 {file} 不存在~")
return
with file.open("r", encoding="utf-8", errors="ignore") as f:
for num, line in enumerate(f, 1):
if keyword in line:
print(f"[{num:>5}] {line.rstrip()}")
defparse_args():
parser = argparse.ArgumentParser(description="简单日志搜索小工具")
parser.add_argument("file", type=Path, help="日志文件路径")
parser.add_argument("keyword", help="要搜索的关键字")
return parser.parse_args()
defmain():
args = parse_args()
search_log(args.file, args.keyword)
if __name__ == "__main__":
main()
这个项目结构大概是:
your_project/
logtool/
__init__.py
main.py
build.py
那 build.py 可以这样写:
# build.py
import subprocess
import sys
from pathlib import PathPROJECT_ROOT = Path(__file__).parent
PACKAGE_NAME = "logtool"
defbuild():
entry_script = PROJECT_ROOT / PACKAGE_NAME / "main.py"
cmd = [
sys.executable, "-m", "nuitka",
"--standalone",
"--onefile",
"--follow-imports", # 跟踪包内 import
"--include-package=logtool", # 确保整个包被带上
"--output-filename=logtool.exe"if sys.platform == "win32"else"logtool",
str(entry_script),
]
print("开始编译日志工具:")
print(" ".join(cmd))
subprocess.run(cmd, check=True)
if __name__ == "__main__":
build()
生成的二进制你就可以丢给运维、测试、甚至非技术同学用,让他们直接:
logtool.exe app.log ERROR
整完他们就再也不会问你“我怎么装 Python 才能用你这个脚本”了,干脆利落。
几个典型的坑,基本每个人都踩过
这个工具好倒是好,就是第一次玩的人,十有八九会在这些地方翻车:
1)没装 / 没配好 C 编译器
Windows 最典型,报错类似:“cl.exe not found” 之类。
解决方式真的很土:
把 VS Build Tools 装上 把对应命令行工具加到 PATH 有时候你得直接在 “Developer Command Prompt for VS” 里面跑 build(环境变量都帮你配好了)
Nuitka 官方也明确说了,它依赖系统上现成的 C/C++ 编译器,然后把生成的 C 代码交给它们去编。
2)第三方库没打进去
有些包比较“妖”,比如某些 GUI 库、科学计算库,用 PyInstaller 也会丢模块。
在 Nuitka 里,你可以强行包含:
cmd = [
sys.executable, "-m", "nuitka",
"--standalone",
"--follow-imports",
"--include-module=wx._xml", # 比如官方论坛举的这个例子
str(entry_script),
]
有些常见库还自带插件,比如 --enable-plugin=tk-inter,碰到 GUI 项目记得翻一下官方文档,插件开一开,省很多时间。
3)体积太大吓人
这时候你别急着怪 Nuitka,站在它的角度,人家是老老实实把你依赖的东西全给你打包进来了。
能做的优化大概就几种思路:
控制依赖:别一个包只用一个函数,就把整个“大物件”引进来 多看看 --noinclude-*那些选项,有些标准库模块你根本没用到非必要的调试信息可以关,小项目影响不大,大项目多少能减一点
4)编译时间太久
这个真的是“能力越大,编译越久”。你想象一下,它要先分析 Python AST,再生成 C 代码,然后再交给 C 编译器搞一遍整活,中间还有各种优化。
我的做法是:
开发期只对关键模块做局部编译测试
真正打包发版才跑一次完整编译
CI 上可以区分 snapshot / release 两种流程,别每个分支 push 都全量编译一遍,机器会骂你
顺便说一个性能小测法
很多人会问:那到底快多少?
我平时比较土办法,就是写个算发密集一点的小脚本,比如计算一堆质数:
# bench_prime.py
defis_prime(n: int) -> bool:
if n < 2:
returnFalse
if n % 2 == 0:
return n == 2
k = 3
while k * k <= n:
if n % k == 0:
returnFalse
k += 2
returnTruedefcount_primes(limit: int) -> int:
return sum(1for i in range(limit) if is_prime(i))
if __name__ == "__main__":
import time
start = time.perf_counter()
total = count_primes(500_000)
cost = time.perf_counter() - start
print("质数个数:", total)
print("耗时: %.3fs" % cost)
然后:
直接 python bench_prime.py跑一遍,记个时间用 Nuitka 编成 exe 再跑几遍,同一台机器上大概就能感受到差距
不要太较真,毕竟 I/O、网络这些本来就瓶颈在别的地方,但很多后台小工具、批处理脚本,Nuitka 编完确实是能明显快一点的。
最后唠两句实践上的小建议
我现在新起 Python 小工具项目,大概的套路就是:
正常写代码,先保证在虚拟环境里跑得好好的 整一个 build.py,把 Nuitka 相关的参数都写清楚优先跑 --standalone,确认依赖都带齐了没问题再试 --onefile,给非技术同学用就发单 exe
你要是团队里经常要“给别人一个可执行文件”的,那真的可以把这套东西收拾成模板,新项目直接抄,用得多了也不觉得麻烦。
行了我也不多说了,我这会儿还得回去改一个白天没改完的 bug。你要真准备上手撸 Nuitka,先搞个小脚本试试,别一上来就拿生产那套几十个依赖的大项目怼,上来就红屏那种,心态特别容易崩。