基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

我在随笔《WxPython跨平台开发框架之使用PyInstaller 进行打包处理》中介绍过如何使用PyInstaller 进行打包处理的一些过程和事项。我们基于Python的FastAPI后端应用,在实际开发的时候,直接运行main.py 进行调试即可,但是部署的时候,我们就需要把它们进行打包处理,这里首选PyInstaller 进行打包。本文详细介绍了 如何使用 PyInstaller 对基于 Python 的 FastAPI 后端项目进行打包与部署,使其能够在目标环境中以独立可执行文件的形式运行,无需安装 Python 解释器或额外依赖。文章面向希望将 FastAPI 服务打包为独立运行服务的开发者,特别适用于企业内部系统或需要简化部署的场景。

一、背景与目标

  • 说明为什么需要将 FastAPI 项目打包为可执行文件。

  • 对比传统部署方式(如 uvicorn main:app)与 PyInstaller 打包方式的区别。

  • 适用场景:企业内网部署、Windows 服务、无 Python 环境的服务器等。

二、环境准备

  • Python 版本要求(推荐 Python 3.12.4+)。

  • FastAPI 与依赖(fastapi, uvicorn, pydantic, sqlalchemy, 等)。

  • 安装 PyInstaller:

pip install pyinstaller

三、FastAPI 项目结构示例

展示一个典型的 FastAPI 项目结构:

project/ ├── app/ │   ├── main.py │   ├── api/ │   ├── core/ │   ├── models/ │   ├── services/ │   └── __init__.py ├── requirements.txt

并说明 main.py 中如何启动服务,例如:

import uvicorn from app.main import app  if __name__ == "__main__":     uvicorn.run(app, host="0.0.0.0", port=8000)

四、使用 PyInstaller 打包

PyInstaller是目前最流行的Python打包工具之一。它可以将Python脚本打包成独立的可执行文件,支持Windows、Linux和macOS平台。

PyInstaller 有丰富的文档,提供了详细的使用说明和常见问题解答,你可以通过以下链接访问:

这些文档和资源能帮助你深入了解 PyInstaller 的使用方式,并解决在打包过程中可能遇到的问题。

打包后的可执行文件可以在没有 Python 环境的机器上运行。PyInstaller 会自动分析程序的依赖关系,并将所有必要的库和资源打包到一个文件或者一个文件夹中。

打包过程中,PyInstaller 会生成一个 .spec 文件。这个文件包含了 PyInstaller 的配置信息,其中包含了构建过程的所有配置信息。你可以修改这个文件来定制打包过程。

如果我们执行下面代码

pyinstaller main.py

或者指定更多的参数的代码

pyinstaller --onefile --icon=your_icon.ico main.py

PyInstaller 都会生成一个 .spec 文件,然后可以编辑 main.spec 文件,以便进行更好的控制管理打包文件。

虽然原则上.spec文件支持跨平台的配置,不过我们在实际中往往根据不同的平台配置特定的.spec文件。

你可以手动修改 .spec 文件来添加资源文件、修改导入模块、定制输出路径等。

你可以通过编辑.spec 文件,在EXE、COLLECT和BUNDLE块下添加一个name= ,为PyInstaller提供一个更好的名字,以便为应用程序(和dist 文件夹)使用。

EXE下的名字是可执行文件的名字,BUNDLE下的名字是应用程序包的名字。

import sys import os from pathlib import Path  # 本文件用于Window平台下打包整个项目,生成一个独立的exe文件,依赖文件松散组合 # 执行命令:pyinstaller main_my.spec # 打包后生成文件:distfastapi_appfastapi_app.exe # 运行后,会在当前目录生成一个 dist 文件夹,里面有 fastapi_app.exe 文件,在命令行窗口运行该文件即可启动服务。   if sys.platform == "win32":     icon = "app/images/app.ico" elif sys.platform == "darwin":     icon = "app/images/app.icns"  block_cipher = None  # 导入 PyInstaller 模块 from PyInstaller.building.build_main import Analysis from PyInstaller.building.build_main import PYZ from PyInstaller.building.build_main import EXE from PyInstaller.building.build_main import COLLECT  # Analysis: PyInstaller Analysis object a = Analysis(     ["app/main.py"],     pathex=[],     binaries=[],     datas=[         ("app/uvicorn_config.json", "app"),         ("app/.env", "."),         ("app/images/*", "app/images"),         ("app/templates/*", "app/templates"),         ("app/uploadfiles/*", "app/uploadfiles"),         ("app/logs/*", "app/logs"),     ],     hiddenimports=[          "uvicorn", "fastapi", "pydantic", "aiomysql", 'asyncio',   # 确保依赖被正确包含     ],     hookspath=[],     hooksconfig={},     runtime_hooks=[],     excludes=[],     win_no_prefer_redirects=False,     win_private_assemblies=False,     cipher=block_cipher,     noarchive=False,     optimize=0, )  # PYZ: PyInstaller PYZ object pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

修改完成后,执行以下命令来重新打包:

pyinstaller main_my.spec

如果我们想在Windows平台生成的dist目录中生成一个启动exe,和其他相关的Lib依赖库目录,那么我们可以适当调整下.spec文件,让它可以生成松散结构的文件目录包。

exe = EXE(     pyz,     a.scripts,     [],     exclude_binaries=True,     name="fastapi_app",     debug=False,     bootloader_ignore_signals=False,     strip=False,     upx=True,     upx_exclude=[],     runtime_tmpdir=None,     console=True,          # True = 有控制台输出(调试方便),False = 静默运行     onefile=False,  # <-- False取消、True使用 onefile 模式     icon=icon,  # <-- 图标路径     disable_windowed_traceback=False,     argv_emulation=False,     target_arch=None,     codesign_identity=None,     entitlements_file=None, )  coll = COLLECT(     exe,     a.binaries,     a.zipfiles,     a.datas,     strip=False,     upx=True,     name='fastapi_app' )

相当于之前在exe包中的a.binaries 和 a.datas从EXE 构造函数中移到了Collect的构造函数里面了。这样会生成下面的目录结构。

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

其中_internal目录包含程序的相关依赖包和文件资源。

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

由于打包的.spec文件指定的目录结构为松散结构(使用了COLLECT构造),那么可以看到 _internal / app目录下有下面的目录结构。

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

也就是我们前面通过 Analysis 模块指定的datas集合路径的内容。

 

解决常见问题

    • 缺少依赖库:如果打包后运行时出现缺少模块的错误,可以尝试将缺少的模块加入到 hiddenimports 中,或者通过 --hidden-import 选项指定:

    • 大文件:如果使用 --onefile 时打包后的文件太大,考虑使用 --onedir 或通过压缩文件等方法进行优化。
    • 处理资源文件:如果你的应用程序包含非 Python 代码的资源(如图像、配置文件、数据文件等),你需要通过 --add-data 选项指定资源文件的路径,或者在 .spec 文件中修改 datas 选项。
    • 动态链接库,如果你的应用程序依赖于特定的动态链接库(如 DLL 文件或 .so 文件),你需要将这些库包含到打包中。可以在 .spec 文件的 binaries 选项中指定:

    • 多平台支持:PyInstaller 支持 Windows、Linux 和 macOS 等多个平台,但需要在相应的平台上打包。例如,如果你要为 Windows 用户创建可执行文件,最好在 Windows 上运行 PyInstaller 来生成 Windows 的 .exe 文件。如果在 macOS 上打包,生成的文件只能在 macOS 上运行。

在使用 PyInstaller 打包 FastAPI(或其他 Python 应用)时,两个最常见、最容易混淆的参数就是:

  • --add-data(或 .spec 文件中的 datas

  • --hidden-import(或 .spec 文件中的 hiddenimports

当 PyInstaller 打包时,它默认只会分析 Python 代码的依赖模块,而不会自动包含图片、HTML 模板、配置文件等静态资源。
这时,就需要用 --add-data(或在 .spec 文件的 datas 中定义)告诉它要额外打包哪些文件或目录。

这里不介绍命令行的方式,只介绍.spec 文件写法:

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

PyInstaller 在打包时,会分析你的 Python 源代码(AST)来判断使用了哪些模块。但有些模块是动态导入的(例如通过 importlib 或字符串导入),它就可能漏掉。

解决办法:用 --hidden-import 或者.spec文件中指定 hiddenimports 集合,告诉 PyInstaller 把这些模块也打包进去,如上所示。

总结起来就是:

  • datas = “我还有额外的文件要带上”。

  • hiddenimports = “我还有额外的模块要带上”。

 

五、FastAPI 项目打包的处理

前面介绍了一个简单的fastapi的项目结构和启动,一般我们在开发的时候,启动fastapi,直接调用python解析器运行main.py文件即可启动,常规来说,main.py的启动部分函数代码如下。

if __name__ == "__main__":      # 日志配置路径     config_path = resource_path("app/uvicorn_config.json")     # 运行 uvicorn     try:         config = uvicorn.Config(             app = socket_app,             reload=True,             host=settings.SERVER_IP,             log_config = config_config,  # 日志配置         )         server = uvicorn.Server(config)         server.run()     except Exception as e:         raise e

上面就是我实际项目简化版本的main.py函数的启动内容,正常开发环境,测试是正常的。但是通过pyinstall打包完成,并运行fastapi_app.exe的时候,提示找不到配置文件uvicorn_config.json。

FileNotFoundError: [Errno 2] No such file or directory: 'app/uvicorn_config.json'

这个原因是打包后执行exe文件的当前路径改变了,打包进去的 exe 并没有找到这个文件。首先:修改 .spec 文件,确保文件被打包进去,在 datas 里加这一行 👇
(假设文件路径是 app/uvicorn_config.json

datas = [     ("app/uvicorn_config.json", "app"),     ("app/templates/*", "app/templates"),     ("app/static/*", "app/static"),     ("app/images/*", "app/images"), ]

这一步确保 exe 中确实包含了你的 uvicorn_config.json 文件。

其次:在 main.py 中使用通用的路径函数resource_path:

def resource_path(relative_path: str) -> str:     """     获取资源文件真实路径,支持:     - 开发模式     - PyInstaller onefile 模式     - PyInstaller COLLECT (_internal) 模式     """      if hasattr(sys, '_MEIPASS'): # onefile 模式         # exe 解压临时目录         base_path = sys._MEIPASS     else:         # 在松散模式下,_internal 目录才是真正的数据存放处         base_path = os.path.dirname(sys.executable) # exe 所在目录         internal_path = os.path.join(base_path, "_internal")         if os.path.exists(internal_path):# 松散打包目录             base_path = internal_path         else:             # 直接开发运行时             base_path = os.path.abspath(".")     return os.path.join(base_path, relative_path)

然后修改你的 uvicorn.Config 代码

替换硬编码路径为:

import uvicorn  config_path = resource_path("app/uvicorn_config.json")  config = uvicorn.Config(     app=socket_app,     host=settings.SERVER_IP,     port=settings.SERVER_PORT,     log_config=config_path,  # ✅ 动态获取正确路径 )  server = uvicorn.Server(config) server.run()

上面启动后,fastapi 配置文件定位到了,但是可能还会产生新的问题

你可能会发现 app/uvicorn_config.json 里面配置的日志文件路径和实际不对。

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

FileNotFoundError: [Errno 2] No such file or directory: '.../app/logs/log.log'

Uvicorn 在加载 uvicorn_config.json 时的日志路径是 相对进程工作目录
而不是相对 uvicorn_config.json 文件本身的路径 ——
这正是为什么你配置 "filename": "app/logs/log.log" 仍然报错的根本原因。

我们需要,在运行前动态修正 log_config.json 内部的路径

我们在加载 JSON 后,动态修改其中 "filename" 字段的路径为打包后正确的绝对路径。

修正代码后如下所示。

if __name__ == "__main__":      # 动态解析日志配置路径     config_path = resource_path("app/uvicorn_config.json")          # 加载并修改日志配置,主要对日志文件路径进行修正     with open(config_path, "r", encoding="utf-8") as f:         log_config = json.load(f)      # 找到其中的 file handler,改写 filename 为绝对路径     for handler in log_config.get("handlers", {}).values():         if "filename" in handler:             log_file = handler["filename"]             abs_log_path = resource_path(log_file)             os.makedirs(os.path.dirname(abs_log_path), exist_ok=True)             handler["filename"] = abs_log_path  # 替换为绝对路径      # 运行 uvicorn(传入已修改的 log_config dict)     try:         config = uvicorn.Config(             app = socket_app,             reload=True,             host=settings.SERVER_IP,             log_config = log_config,  # 日志配置,修正方式见上         )          server = uvicorn.Server(config)         server.run()     except Exception as e:         raise e

至此,所有问题都顺利解决,能够正常运行起来了,我们来看看FastAPI顺利启动后的效果。复制松散文件夹到服务器上双击运行即可,需要也可以修改配置文件.env实现相关修改。

基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署

 

✅ 如果运行打包的exe 提示Missing command. 

其实是 uvicorn 的提示,不是 PyInstaller 本身的报错。可能是你的app设置上的问题,你在 main.py 里可能用了这种启动方式:

uvicorn.run("app.main:app", host="0.0.0.0", port=8000)

解决方法 改成直接传入 app 对象,而不是字符串路径:

    # ✅ 改成直接传 app 对象     uvicorn.run(app, host="0.0.0.0", port=8000)

这样 uvicorn 就不会去找字符串形式的 module:app,而是直接运行你传进去的 FastAPI 实例。 打包后的 exe 就能正常运行。

 

✅ 如果提示No module named 'aiomysql'

这个问题其实是 PyInstaller 没有把 aiomysql 打包进去,因为它是动态导入的,PyInstaller 静态分析不到。

方法 A:命令行添加 hidden-import

pyinstaller --onefile --name fastapi_app --hidden-import aiomysql app/main.py

方法 B:在 .spec 文件里加 hiddenimports

找到 .spec 文件里的 Analysis,改成:

a = Analysis(     ['app/main.py'],     pathex=[],     binaries=[],     datas=datas,     hiddenimports=[         "uvicorn",         "fastapi",         "pydantic",         "aiomysql"   # 👈 加上这里     ],     hookspath=[],     runtime_hooks=[],     excludes=[],     win_no_prefer_redirects=False,     win_private_assemblies=False,     cipher=block_cipher,     noarchive=False, )

FastAPI + 数据库常用依赖很多(如 sqlalchemy[asyncio]asyncpgaiomysql 等),有些也可能被漏掉。做法同样:把缺失的库加到 hidden-import

hiddenimports=[     "uvicorn",     "fastapi",     "pydantic",     "aiomysql",     "asyncpg",     "sqlalchemy.ext.asyncio", ]

✅ Data内容的写法

("app/images/*", "app/images")

会把 app/images 下的所有文件 放到 exe 解压后的目录里,路径是 app/images/...

如果代码里是这样写的:

open("app/images/logo.png", "rb")

就能找到,也就是始终保持相对目录的正确性。

 

发表评论

评论已关闭。

相关文章