基于Python的FastAPI后端开发框架如何使用PyInstaller 进行打包与部署
<p>我在随笔《WxPython跨平台开发框架之使用PyInstaller 进行打包处理》中介绍过如何使用PyInstaller 进行打包处理的一些过程和事项。我们基于Python的FastAPI后端应用,在实际开发的时候,直接运行main.py 进行调试即可,但是部署的时候,我们就需要把它们进行打包处理,这里首选PyInstaller 进行打包。本文详细介绍了 <strong data-start="117" data-end="170">如何使用 PyInstaller 对基于 Python 的 FastAPI 后端项目进行打包与部署</strong>,使其能够在目标环境中以独立可执行文件的形式运行,无需安装 Python 解释器或额外依赖。文章面向希望将 FastAPI 服务打包为独立运行服务的开发者,特别适用于企业内部系统或需要简化部署的场景。</p><h4 data-start="295" data-end="309">一、背景与目标</h4>
<ul data-start="310" data-end="444">
<li data-start="310" data-end="342">
<p data-start="312" data-end="342">说明为什么需要将 FastAPI 项目打包为可执行文件。</p>
</li>
<li data-start="343" data-end="399">
<p data-start="345" data-end="399">对比传统部署方式(如 <code data-start="356" data-end="374">uvicorn main:app</code>)与 PyInstaller 打包方式的区别。</p>
</li>
<li data-start="400" data-end="444">
<p data-start="402" data-end="444">适用场景:企业内网部署、Windows 服务、无 Python 环境的服务器等。</p>
</li>
</ul>
<h4 data-start="446" data-end="459">二、环境准备</h4>
<ul data-start="460" data-end="624">
<li data-start="460" data-end="492">
<p data-start="462" data-end="492">Python 版本要求(推荐 Python 3.12.4+)。</p>
</li>
<li data-start="493" data-end="560">
<p data-start="495" data-end="560">FastAPI 与依赖(<code data-start="507" data-end="516">fastapi</code>, <code data-start="518" data-end="527">uvicorn</code>, <code data-start="529" data-end="539">pydantic</code>, <code data-start="541" data-end="553">sqlalchemy</code>, 等)。</p>
</li>
<li data-start="561" data-end="624">
<p data-start="563" data-end="580">安装 PyInstaller:</p>
</li>
</ul>
<div class="cnblogs_code">
<pre>pip <span style="color: rgba(0, 0, 255, 1)">install</span> pyinstaller</pre>
</div>
<h4 data-start="626" data-end="649">三、FastAPI 项目结构示例</h4>
<p data-start="650" data-end="673">展示一个典型的 FastAPI 项目结构:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="sticky top-9">
<div class="cnblogs_code">
<pre>project/<span style="color: rgba(0, 0, 0, 1)">
├── app</span>/<span style="color: rgba(0, 0, 0, 1)">
│ ├── main.py
│ ├── api</span>/<span style="color: rgba(0, 0, 0, 1)">
│ ├── core</span>/<span style="color: rgba(0, 0, 0, 1)">
│ ├── models</span>/<span style="color: rgba(0, 0, 0, 1)">
│ ├── services</span>/<span style="color: rgba(0, 0, 0, 1)">
│ └── __init__.py
├── requirements.txt<br></span></pre>
</div>
</div>
</div>
<p data-start="829" data-end="856">并说明 <code data-start="833" data-end="842">main.py</code> 中如何启动服务,例如:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">import uvicorn
from app.main import app
</span><span style="color: rgba(0, 0, 255, 1)">if</span> __name__ == <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">__main__</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:
uvicorn.run(app, host</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0.0.0.0</span><span style="color: rgba(128, 0, 0, 1)">"</span>, port=<span style="color: rgba(128, 0, 128, 1)">8000</span>)</pre>
</div>
<h4>四、使用 PyInstaller 打包</h4>
<p>PyInstaller是目前最流行的Python打包工具之一。它可以将Python脚本打包成独立的可执行文件,支持Windows、Linux和macOS平台。</p>
<p>PyInstaller 有丰富的文档,提供了详细的使用说明和常见问题解答,你可以通过以下链接访问:</p>
<ul>
<li>PyInstaller 官方文档:https://pyinstaller.readthedocs.io</li>
<li>GitHub 代码库:https://github.com/pyinstaller/pyinstaller</li>
</ul>
<p>这些文档和资源能帮助你深入了解 PyInstaller 的使用方式,并解决在打包过程中可能遇到的问题。</p>
<p data-start="829" data-end="856">打包后的可执行文件可以在没有 Python 环境的机器上运行。PyInstaller 会自动分析程序的依赖关系,并将所有必要的库和资源打包到一个文件或者一个文件夹中。</p>
<p class="marklang-paragraph">打包过程中,PyInstaller 会生成一个 <code>.spec</code> 文件。这个文件包含了 PyInstaller 的配置信息,其中包含了构建过程的所有配置信息。你可以修改这个文件来定制打包过程。</p>
<p class="marklang-paragraph">如果我们执行下面代码</p>
<div class="cnblogs_code">
<pre>pyinstaller main.py</pre>
</div>
<p>或者指定更多的参数的代码</p>
<div class="cnblogs_code">
<pre>pyinstaller --onefile --icon=your_icon.ico main.py</pre>
</div>
<p>PyInstaller 都会生成一个 <code>.spec</code> 文件,然后可以编辑 <code>main.spec</code> 文件,以便进行更好的控制管理打包文件。</p>
<p>虽然原则上.spec文件支持跨平台的配置,不过我们在实际中往往根据不同的平台配置特定的.spec文件。</p>
<p>你可以手动修改 <code>.spec</code> 文件来添加资源文件、修改导入模块、定制输出路径等。</p>
<p>你可以通过编辑<code>.spec</code> 文件,在EXE、COLLECT和BUNDLE块下添加一个<code>name=</code> ,为<em>PyInstaller</em>提供一个更好的名字,以便为应用程序(和<code>dist</code> 文件夹)使用。</p>
<p>EXE下的名字是<em>可执行文件</em>的名字,BUNDLE下的名字是应用程序包的名字。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> sys
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> os
</span><span style="color: rgba(0, 0, 255, 1)">from</span> pathlib <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> Path
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 本文件用于Window平台下打包整个项目,生成一个独立的exe文件,依赖文件松散组合</span><span style="color: rgba(0, 128, 0, 1)">
#</span><span style="color: rgba(0, 128, 0, 1)"> 执行命令:pyinstaller main_my.spec</span><span style="color: rgba(0, 128, 0, 1)">
#</span><span style="color: rgba(0, 128, 0, 1)"> 打包后生成文件:dist\fastapi_app\fastapi_app.exe</span><span style="color: rgba(0, 128, 0, 1)">
#</span><span style="color: rgba(0, 128, 0, 1)"> 运行后,会在当前目录生成一个 dist 文件夹,里面有 fastapi_app.exe 文件,在命令行窗口运行该文件即可启动服务。</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> sys.platform == <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">win32</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:
icon </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/images/app.ico</span><span style="color: rgba(128, 0, 0, 1)">"</span>
<span style="color: rgba(0, 0, 255, 1)">elif</span> sys.platform == <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">darwin</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:
icon </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/images/app.icns</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">
block_cipher </span>=<span style="color: rgba(0, 0, 0, 1)"> None
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 导入 PyInstaller 模块</span>
<span style="color: rgba(0, 0, 255, 1)">from</span> PyInstaller.building.build_main <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> Analysis
</span><span style="color: rgba(0, 0, 255, 1)">from</span> PyInstaller.building.build_main <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> PYZ
</span><span style="color: rgba(0, 0, 255, 1)">from</span> PyInstaller.building.build_main <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> EXE
</span><span style="color: rgba(0, 0, 255, 1)">from</span> PyInstaller.building.build_main <span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> COLLECT
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> Analysis: PyInstaller Analysis object</span>
a =<span style="color: rgba(0, 0, 0, 1)"> Analysis(
[</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/main.py</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">],
pathex</span>=<span style="color: rgba(0, 0, 0, 1)">[],
binaries</span>=<span style="color: rgba(0, 0, 0, 1)">[],
<span style="color: rgba(255, 0, 0, 1)"><strong>datas</strong></span></span>=<span style="color: rgba(0, 0, 0, 1)">[
<span style="color: rgba(255, 0, 0, 1)"> (</span></span><span style="color: rgba(255, 0, 0, 1)">"app/uvicorn_config.json", "app"),
("app/.env", "."),
("app/images/*", "app/images"),
("app/templates/*", "app/templates"),
("app/uploadfiles/*", "app/uploadfiles"),
("app/logs/*", "app/logs"</span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)">),</span>
],
<span style="color: rgba(255, 0, 0, 1)"><strong>hiddenimports</strong></span></span>=<span style="color: rgba(0, 0, 0, 1)">[
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">uvicorn</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">fastapi</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">pydantic</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">aiomysql</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">asyncio</span><span style="color: rgba(128, 0, 0, 1)">'</span>, <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 确保依赖被正确包含</span>
<span style="color: rgba(0, 0, 0, 1)"> ],
hookspath</span>=<span style="color: rgba(0, 0, 0, 1)">[],
hooksconfig</span>=<span style="color: rgba(0, 0, 0, 1)">{},
runtime_hooks</span>=<span style="color: rgba(0, 0, 0, 1)">[],
excludes</span>=<span style="color: rgba(0, 0, 0, 1)">[],
win_no_prefer_redirects</span>=<span style="color: rgba(0, 0, 0, 1)">False,
win_private_assemblies</span>=<span style="color: rgba(0, 0, 0, 1)">False,
cipher</span>=<span style="color: rgba(0, 0, 0, 1)">block_cipher,
noarchive</span>=<span style="color: rgba(0, 0, 0, 1)">False,
optimize</span>=<span style="color: rgba(0, 0, 0, 1)">0,
)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> PYZ: PyInstaller PYZ object</span>
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)</pre>
</div>
<p>修改完成后,执行以下命令来重新打包:</p>
<div class="cnblogs_code">
<pre>pyinstaller main_my.spec</pre>
</div>
<p>如果我们想在Windows平台生成的dist目录中生成一个启动exe,和其他相关的Lib依赖库目录,那么我们可以适当调整下.spec文件,让它可以生成松散结构的文件目录包。</p>
<div class="cnblogs_code">
<pre>exe =<span style="color: rgba(0, 0, 0, 1)"> EXE(
pyz,
a.scripts,
[],
<span style="color: rgba(255, 0, 0, 1)"><strong>exclude_binaries</strong></span></span><span style="color: rgba(255, 0, 0, 1)"><strong>=</strong></span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)"><strong>True,</strong></span>
name</span>=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">fastapi_app</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
debug</span>=<span style="color: rgba(0, 0, 0, 1)">False,
bootloader_ignore_signals</span>=<span style="color: rgba(0, 0, 0, 1)">False,
strip</span>=<span style="color: rgba(0, 0, 0, 1)">False,
upx</span>=<span style="color: rgba(0, 0, 0, 1)">True,
upx_exclude</span>=<span style="color: rgba(0, 0, 0, 1)">[],
runtime_tmpdir</span>=<span style="color: rgba(0, 0, 0, 1)">None,
console</span>=True, <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> True = 有控制台输出(调试方便),False = 静默运行</span>
onefile=False,<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> <-- False取消、True使用 onefile 模式</span>
icon=icon,<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> <-- 图标路径</span>
disable_windowed_traceback=<span style="color: rgba(0, 0, 0, 1)">False,
argv_emulation</span>=<span style="color: rgba(0, 0, 0, 1)">False,
target_arch</span>=<span style="color: rgba(0, 0, 0, 1)">None,
codesign_identity</span>=<span style="color: rgba(0, 0, 0, 1)">None,
entitlements_file</span>=<span style="color: rgba(0, 0, 0, 1)">None,
)
coll </span>=<span style="color: rgba(0, 0, 0, 1)"> COLLECT(
exe,
<span style="color: rgba(255, 0, 0, 1)"><strong> a.binaries,
a.zipfiles,
a.datas,</strong></span>
strip</span>=<span style="color: rgba(0, 0, 0, 1)">False,
upx</span>=<span style="color: rgba(0, 0, 0, 1)">True,
name</span>=<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">fastapi_app</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">
)</span></pre>
</div>
<p>相当于之前在exe包中的a.binaries 和 a.datas从EXE 构造函数中移到了Collect的构造函数里面了。这样会生成下面的目录结构。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011105724278-1565882326.png" alt="image" width="421" height="209" loading="lazy"></p>
<p>其中_internal目录包含程序的相关依赖包和文件资源。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011105858942-106096974.png" alt="image" width="424" height="588" loading="lazy"></p>
<p>由于打包的.spec文件指定的目录结构为松散结构(使用了COLLECT构造),那么可以看到 _internal / app目录下有下面的目录结构。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011113400548-169982050.png" alt="image" width="392" height="221" loading="lazy"></p>
<p>也就是我们前面通过 Analysis 模块指定的datas集合路径的内容。</p>
<p> </p>
<p><strong>解决常见问题</strong></p>
<ul>
<ul>
<li>
<p><strong>缺少依赖库</strong>:如果打包后运行时出现缺少模块的错误,可以尝试将缺少的模块加入到 <code>hiddenimports</code> 中,或者通过 <code>--hidden-import</code> 选项指定:</p>
</li>
<li><strong>大文件</strong>:如果使用 <code>--onefile</code> 时打包后的文件太大,考虑使用 <code>--onedir</code> 或通过压缩文件等方法进行优化。</li>
<li><strong>处理资源文件:</strong>如果你的应用程序包含非 Python 代码的资源(如图像、配置文件、数据文件等),你需要通过 <code>--add-data</code> 选项指定资源文件的路径,或者在 <code>.spec</code> 文件中修改 <code>datas</code> 选项。</li>
<li>
<p><strong>动态链接库</strong>,如果你的应用程序依赖于特定的动态链接库(如 DLL 文件或 <code>.so</code> 文件),你需要将这些库包含到打包中。可以在 <code>.spec</code> 文件的 <code>binaries</code> 选项中指定:</p>
</li>
<li>
<p><strong>多平台支持:</strong>PyInstaller 支持 Windows、Linux 和 macOS 等多个平台,但需要在相应的平台上打包。例如,如果你要为 Windows 用户创建可执行文件,最好在 Windows 上运行 PyInstaller 来生成 Windows 的 <code>.exe</code> 文件。如果在 macOS 上打包,生成的文件只能在 macOS 上运行。</p>
</li>
</ul>
</ul>
<p data-start="0" data-end="78">在使用 <strong data-start="16" data-end="31">PyInstaller</strong> 打包 FastAPI(或其他 Python 应用)时,两个最常见、最容易混淆的参数就是:</p>
<ul data-start="80" data-end="174">
<li data-start="80" data-end="120">
<p data-start="82" data-end="120"><code data-start="82" data-end="94">--add-data</code>(或 <code data-start="97" data-end="104">.spec</code> 文件中的 <code data-start="110" data-end="117">datas</code>)</p>
</li>
<li data-start="121" data-end="174">
<p data-start="123" data-end="174"><code data-start="123" data-end="140">--hidden-import</code>(或 <code data-start="143" data-end="150">.spec</code> 文件中的 <code data-start="156" data-end="171">hiddenimports</code>)</p>
</li>
</ul>
<p>当 PyInstaller 打包时,它默认只会分析 Python 代码的依赖模块,而不会自动包含图片、HTML 模板、配置文件等静态资源。<br data-start="404" data-end="407">这时,就需要用 <code data-start="415" data-end="427">--add-data</code>(或在 <code data-start="431" data-end="438">.spec</code> 文件的 <code data-start="443" data-end="450">datas</code> 中定义)告诉它要额外打包哪些文件或目录。</p>
<p>这里不介绍命令行的方式,只介绍<code data-start="702" data-end="709">.spec</code> 文件写法:</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011110706258-1292089188.png" alt="image" width="606" height="275" loading="lazy"></p>
<p>PyInstaller 在打包时,会分析你的 Python 源代码(AST)来判断使用了哪些模块。但有些模块是<strong data-start="1800" data-end="1809">动态导入的</strong>(例如通过 <code data-start="1815" data-end="1826">importlib</code> 或字符串导入),它就可能漏掉。</p>
<p>解决办法:用 <code data-start="1936" data-end="1953">--hidden-import</code> 或者.spec文件中指定 hiddenimports 集合,告诉 PyInstaller 把这些模块也打包进去,如上所示。</p>
<p>总结起来就是:</p>
<ul>
<li data-start="3058" data-end="3086">
<p data-start="3060" data-end="3086"><code data-start="3060" data-end="3067">datas</code> = “我还有额外的文件要带上”。</p>
</li>
<li data-start="3087" data-end="3123">
<p data-start="3089" data-end="3123"><code data-start="3089" data-end="3104">hiddenimports</code> = “我还有额外的模块要带上”。</p>
</li>
</ul>
<p> </p>
<h4 data-start="626" data-end="649">五、FastAPI 项目打包的处理</h4>
<p>前面介绍了一个简单的fastapi的项目结构和启动,一般我们在开发的时候,启动fastapi,直接调用python解析器运行main.py文件即可启动,常规来说,main.py的启动部分函数代码如下。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">if</span> <span style="color: rgba(128, 0, 128, 1)">__name__</span> == <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">__main__</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 日志配置路径</span>
config_path = resource_path(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/uvicorn_config.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 运行 uvicorn</span>
<span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">:</span><span style="color: rgba(0, 0, 0, 1)">
config </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Config(</span><span style="color: rgba(0, 0, 0, 1)">
app </span>=<span style="color: rgba(0, 0, 0, 1)"> socket_app,
reload</span>=<span style="color: rgba(0, 0, 0, 1)">True,
host</span>=<span style="color: rgba(0, 0, 0, 1)">settings.SERVER_IP,
log_config </span>= config_config,<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 日志配置</span>
<span style="color: rgba(0, 0, 0, 1)"> )
server </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Server(config)
server.run()
</span><span style="color: rgba(0, 0, 255, 1)">except</span><span style="color: rgba(0, 0, 0, 1)"> Exception as e:
</span><span style="color: rgba(0, 0, 255, 1)">raise</span> e</pre>
</div>
<p>上面就是我实际项目简化版本的main.py函数的启动内容,正常开发环境,测试是正常的。但是通过pyinstall打包完成,并运行fastapi_app.exe的时候,提示找不到配置文件uvicorn_config.json。</p>
<div class="cnblogs_code">
<pre>FileNotFoundError: No such file <span style="color: rgba(0, 0, 255, 1)">or</span> directory: <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app/uvicorn_config.json</span><span style="color: rgba(128, 0, 0, 1)">'</span></pre>
</div>
<p>这个原因是打包后执行exe文件的当前路径改变了,打包进去的 exe 并没有找到这个文件。首先:修改 <code data-start="221" data-end="228">.spec</code> 文件,确保文件被打包进去,在 <code data-start="245" data-end="252">datas</code> 里加这一行 👇<br data-start="261" data-end="264">
(假设文件路径是 <code data-start="273" data-end="298">app/uvicorn_config.json</code>)</p>
<div class="cnblogs_code">
<pre>datas =<span style="color: rgba(0, 0, 0, 1)"> [
<span style="color: rgba(255, 0, 0, 1)"><strong> (</strong></span></span><span style="color: rgba(255, 0, 0, 1)"><strong>"app/uvicorn_config.json", "app"</strong></span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)"><strong>),</strong></span>
(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/templates/*</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/templates</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/static/*</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/static</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/images/*</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/images</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">),
]</span></pre>
</div>
<p data-start="482" data-end="526">这一步确保 exe 中确实包含了你的 <code data-start="501" data-end="522">uvicorn_config.json</code> 文件。</p>
<p data-start="482" data-end="526">其次:在 <code data-start="544" data-end="553">main.py</code> 中使用通用的路径函数resource_path:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">def</span> resource_path(relative_path: str) -><span style="color: rgba(0, 0, 0, 1)"> str:
</span><span style="color: rgba(128, 0, 0, 1)">"""</span><span style="color: rgba(128, 0, 0, 1)">
获取资源文件真实路径,支持:
- 开发模式
- PyInstaller onefile 模式
- PyInstaller COLLECT (_internal) 模式
</span><span style="color: rgba(128, 0, 0, 1)">"""</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> hasattr(sys, <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">_MEIPASS</span><span style="color: rgba(128, 0, 0, 1)">'</span>): <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> onefile 模式</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> exe 解压临时目录</span>
base_path =<span style="color: rgba(0, 0, 0, 1)"> sys._MEIPASS
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 在松散模式下,_internal 目录才是真正的数据存放处</span>
base_path = os.path.dirname(sys.executable) <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> exe 所在目录</span>
internal_path = os.path.join(base_path, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">_internal</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">if</span> os.path.exists(internal_path):<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 松散打包目录</span>
base_path =<span style="color: rgba(0, 0, 0, 1)"> internal_path
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 直接开发运行时</span>
base_path = os.path.abspath(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">.</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">return</span> os.path.join(base_path, relative_path)</pre>
</div>
<p data-start="927" data-end="959">然后修改你的 <code data-start="940" data-end="956">uvicorn.Config</code> 代码</p>
<p data-start="961" data-end="970">替换硬编码路径为:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> uvicorn
config_path </span>= resource_path(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/uvicorn_config.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
config </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Config(
app</span>=<span style="color: rgba(0, 0, 0, 1)">socket_app,
host</span>=<span style="color: rgba(0, 0, 0, 1)">settings.SERVER_IP,
port</span>=<span style="color: rgba(0, 0, 0, 1)">settings.SERVER_PORT,
log_config</span>=config_path,<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> ✅ 动态获取正确路径</span>
<span style="color: rgba(0, 0, 0, 1)">)
server </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Server(config)
server.run()</span></pre>
</div>
<p>上面启动后,fastapi 配置文件定位到了,但是可能还会产生新的问题</p>
<p>你可能会发现 app/uvicorn_config.json 里面配置的日志文件路径和实际不对。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011111940199-65656164.png" alt="image" width="454" height="339" loading="lazy"></p>
<div class="cnblogs_code">
<pre>FileNotFoundError: No such file <span style="color: rgba(0, 0, 255, 1)">or</span> directory: <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">.../app/logs/log.log</span><span style="color: rgba(128, 0, 0, 1)">'</span></pre>
</div>
<p>Uvicorn 在加载 <code data-start="85" data-end="106">uvicorn_config.json</code> 时的日志路径是 <strong data-start="115" data-end="127">相对进程工作目录</strong>,<br data-start="128" data-end="131">
而不是相对 <code data-start="137" data-end="158">uvicorn_config.json</code> 文件本身的路径 ——<br data-start="169" data-end="172">
这正是为什么你配置 <code data-start="182" data-end="214">"filename": "app/logs/log.log"</code> 仍然报错的根本原因。</p>
<p data-start="594" data-end="630"><strong data-start="594" data-end="628">我们需要,在运行前动态修正 log_config.json 内部的路径</strong></p>
<p data-start="632" data-end="682">我们在加载 JSON 后,动态修改其中 <code data-start="652" data-end="664">"filename"</code> 字段的路径为打包后正确的绝对路径。</p>
<p data-start="632" data-end="682">修正代码后如下所示。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">if</span> <span style="color: rgba(128, 0, 128, 1)">__name__</span> == <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">__main__</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">:
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 动态解析日志配置路径</span>
config_path = resource_path(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/uvicorn_config.json</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 加载并修改日志配置,主要对日志文件路径进行修正</span>
with open(config_path, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">r</span><span style="color: rgba(128, 0, 0, 1)">"</span>, encoding=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">utf-8</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">) as f:
log_config </span>=<span style="color: rgba(0, 0, 0, 1)"> json.load(f)
</span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 找到其中的 file handler,改写 filename 为绝对路径</span>
<span style="color: rgba(0, 0, 255, 1)">for</span> handler <span style="color: rgba(0, 0, 255, 1)">in</span> log_config.get(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">handlers</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">, {}).values():
</span><span style="color: rgba(0, 0, 255, 1)">if</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">filename</span><span style="color: rgba(128, 0, 0, 1)">"</span> <span style="color: rgba(0, 0, 255, 1)">in</span><span style="color: rgba(0, 0, 0, 1)"> handler:
log_file </span>= handler[<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">filename</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">]
<span style="color: rgba(255, 0, 0, 1)"><strong>abs_log_path </strong></span></span><span style="color: rgba(255, 0, 0, 1)"><strong>=</strong></span><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)"><strong> resource_path(log_file)</strong></span>
os.makedirs(os.path.dirname(abs_log_path), exist_ok</span>=<span style="color: rgba(0, 0, 0, 1)">True)
handler[</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">filename</span><span style="color: rgba(128, 0, 0, 1)">"</span>] = <span style="color: rgba(255, 0, 0, 1)"><strong>abs_log_path </strong> </span><span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 替换为绝对路径</span>
<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 运行 uvicorn(传入已修改的 log_config dict)</span>
<span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)">:</span><span style="color: rgba(0, 0, 0, 1)">
config </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Config(</span><span style="color: rgba(0, 0, 0, 1)">
app </span>=<span style="color: rgba(0, 0, 0, 1)"> socket_app,
reload</span>=<span style="color: rgba(0, 0, 0, 1)">True,
host</span>=<span style="color: rgba(0, 0, 0, 1)">settings.SERVER_IP,
log_config </span>= log_config,<span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 日志配置,修正方式见上</span>
<span style="color: rgba(0, 0, 0, 1)"> )
server </span>=<span style="color: rgba(0, 0, 0, 1)"> uvicorn.Server(config)
server.run()
</span><span style="color: rgba(0, 0, 255, 1)">except</span><span style="color: rgba(0, 0, 0, 1)"> Exception as e:
</span><span style="color: rgba(0, 0, 255, 1)">raise</span> e</pre>
</div>
<p>至此,所有问题都顺利解决,能够正常运行起来了,我们来看看FastAPI顺利启动后的效果。复制松散文件夹到服务器上双击运行即可,需要也可以修改配置文件.env实现相关修改。</p>
<p><img src="https://img2024.cnblogs.com/blog/8867/202510/8867-20251011115335366-103340111.png" alt="image" width="898" height="527" loading="lazy"></p>
<p> </p>
<p>✅ 如果运行打包的exe 提示Missing command. </p>
<p>其实是 <code data-start="32" data-end="41">uvicorn</code> 的提示,不是 PyInstaller 本身的报错。可能是你的app设置上的问题,你在 <code data-start="84" data-end="93">main.py</code> 里可能用了这种启动方式:</p>
<div class="cnblogs_code">
<pre>uvicorn.run(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app.main:app</span><span style="color: rgba(128, 0, 0, 1)">"</span>, host=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0.0.0.0</span><span style="color: rgba(128, 0, 0, 1)">"</span>, port=8000)</pre>
</div>
<p>解决方法 改成直接传入 app 对象,而不是字符串路径:</p>
<div class="cnblogs_code">
<pre> <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> ✅ 改成直接传 app 对象</span>
uvicorn.run(app, host=<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">0.0.0.0</span><span style="color: rgba(128, 0, 0, 1)">"</span>, port=8000)</pre>
</div>
<p>这样 <code data-start="716" data-end="725">uvicorn</code> 就不会去找字符串形式的 <code data-start="738" data-end="750">module:app</code>,而是直接运行你传进去的 <code data-start="763" data-end="772">FastAPI</code> 实例。 打包后的 exe 就能正常运行。</p>
<p> </p>
<p>✅ 如果提示No module named 'aiomysql'</p>
<p>这个问题其实是 <strong data-start="73" data-end="108">PyInstaller 没有把 <code data-start="91" data-end="101">aiomysql</code> 打包进去</strong>,因为它是动态导入的,PyInstaller 静态分析不到。</p>
<p>方法 A:命令行添加 hidden-import</p>
<div class="cnblogs_code">
<pre>pyinstaller --onefile --name fastapi_app --hidden-<span style="color: rgba(0, 0, 255, 1)">import</span> aiomysql app/main.py</pre>
</div>
<p data-start="408" data-end="448">方法 B:在 <code data-start="420" data-end="427">.spec</code> 文件里加 <code data-start="433" data-end="448">hiddenimports</code></p>
<p data-start="449" data-end="479">找到 <code data-start="452" data-end="459">.spec</code> 文件里的 <code data-start="465" data-end="475">Analysis</code>,改成:</p>
<div class="cnblogs_code">
<pre>a =<span style="color: rgba(0, 0, 0, 1)"> Analysis(
[</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">app/main.py</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">],
pathex</span>=<span style="color: rgba(0, 0, 0, 1)">[],
binaries</span>=<span style="color: rgba(0, 0, 0, 1)">[],
datas</span>=<span style="color: rgba(0, 0, 0, 1)">datas,
hiddenimports</span>=<span style="color: rgba(0, 0, 0, 1)">[
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">uvicorn</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">fastapi</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">pydantic</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">aiomysql</span><span style="color: rgba(128, 0, 0, 1)">"</span> <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> 👈 加上这里</span>
<span style="color: rgba(0, 0, 0, 1)"> ],
hookspath</span>=<span style="color: rgba(0, 0, 0, 1)">[],
runtime_hooks</span>=<span style="color: rgba(0, 0, 0, 1)">[],
excludes</span>=<span style="color: rgba(0, 0, 0, 1)">[],
win_no_prefer_redirects</span>=<span style="color: rgba(0, 0, 0, 1)">False,
win_private_assemblies</span>=<span style="color: rgba(0, 0, 0, 1)">False,
cipher</span>=<span style="color: rgba(0, 0, 0, 1)">block_cipher,
noarchive</span>=<span style="color: rgba(0, 0, 0, 1)">False,
)</span></pre>
</div>
<p>FastAPI + 数据库常用依赖很多(如 <code data-start="959" data-end="980">sqlalchemy</code>、<code data-start="981" data-end="990">asyncpg</code>、<code data-start="991" data-end="1001">aiomysql</code> 等),有些也可能被漏掉。做法同样:<strong data-start="1022" data-end="1047">把缺失的库加到 hidden-import</strong>。</p>
<div class="cnblogs_code">
<pre>hiddenimports=<span style="color: rgba(0, 0, 0, 1)">[
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">uvicorn</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">fastapi</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">pydantic</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">aiomysql</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">asyncpg</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">sqlalchemy.ext.asyncio</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">,
]</span></pre>
</div>
<p data-start="129" data-end="168"><code data-start="136" data-end="168">✅ Data内容的写法</code></p>
<p data-start="129" data-end="168"><code data-start="136" data-end="168">("app/images/*", "app/images")</code></p>
<p>会把 <code data-start="174" data-end="186">app/images</code> 下的所有文件 <strong data-start="194" data-end="212">放到 exe 解压后的目录里</strong>,路径是 <code data-start="217" data-end="233">app/images/...</code></p>
<p data-start="238" data-end="251">如果代码里是这样写的:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="cnblogs_code">
<pre>open(<span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">app/images/logo.png</span><span style="color: rgba(128, 0, 0, 1)">"</span>, <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">rb</span><span style="color: rgba(128, 0, 0, 1)">"</span>)</pre>
</div>
<p>就能找到,也就是始终保持相对目录的正确性。</p>
<p> </p>
</div>
</div>
<div id="MySignature" role="contentinfo">
<div style="border-right-color: #cccccc; border-right-width: 1px; border-right-style: solid; padding-right: 5px; border-top-color: #cccccc; border-top-width: 1px; border-top-style: solid; padding-left: 4px; font-size: 13px; padding-bottom: 4px; border-left-color: #cccccc; border-left-width: 1px; border-left-style: solid; width: 98%; padding-top: 4px; border-bottom-color: #cccccc; border-bottom-width: 1px; border-bottom-style: solid; background-color: #eeeeee;">
<img src="http://www.cnblogs.com/Images/OutliningIndicators/None.gif" align="top" alt>
<span style="color: #000000"><span class="Apple-tab-span" style="white-space: pre"></span>
专注于代码生成工具、.Net/Python 框架架构及软件开发,以及各种Vue.js的前端技术应用。著有Winform开发框架/混合式开发框架、微信开发框架、Bootstrap开发框架、ABP开发框架、SqlSugar开发框架、Python开发框架等框架产品。
<br> 转载请注明出处:撰写人:伍华聪 http://www.iqidi.com <br> </span></div><br><br>
来源:https://www.cnblogs.com/wuhuacong/p/19134599
頁:
[1]