Python技术迷

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 time

defmain():
    print("Hello, Nuitka!")
    time.sleep(2)
    print("打包成 exe 也就这回事~")

if __name__ == "__main__":
    main()

然后你新建一个 build.py,专门用来调用 Nuitka 编译它:

# build.py
import subprocess
import sys
from pathlib import Path

defbuild_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 的“打包姿势”主要有两种:

  1. --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 Path

    defsearch_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 Path

    PROJECT_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
    returnTrue

    defcount_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 小工具项目,大概的套路就是:

    1. 正常写代码,先保证在虚拟环境里跑得好好的
    2. 整一个 build.py,把 Nuitka 相关的参数都写清楚
    3. 优先跑 --standalone,确认依赖都带齐了
    4. 没问题再试 --onefile,给非技术同学用就发单 exe

    你要是团队里经常要“给别人一个可执行文件”的,那真的可以把这套东西收拾成模板,新项目直接抄,用得多了也不觉得麻烦。

    行了我也不多说了,我这会儿还得回去改一个白天没改完的 bug。你要真准备上手撸 Nuitka,先搞个小脚本试试,别一上来就拿生产那套几十个依赖的大项目怼,上来就红屏那种,心态特别容易崩。