资讯动态

Python项目打包成exe与安装包:Django/Flask实战指南

发布时间:2026/9/30 1:31:11 来源:尧图企业网站定制
先说结论这个需求我研究过很久最终交付的形态是一个双击就能启动的exe外加一个能双击安装、带卸载程序的Windows安装包。整个过程不用客户装Python、不用配环境、不用管依赖连数据库都替你建好。如果你正在被这些问题困扰这篇内容值得看完项目做完了要交货但客户电脑上没有Python环境也没装任何依赖包想把自己写的Django/Flask小工具发给同事用但对方根本不知道什么是命令行局域网内部想跑一个工具站、数据管理后台、文件服务又不想买台服务器整天维护项目做完发现对方只能跑起来一个窗口却不知道怎么在系统里把它变成能安装、能卸载的正常软件标题描述得很直接“将python项目(django/flask)打包成exe和安装包”我不打算做那种标题党式的泛泛而谈而是把我试了整整两个星期、踩了无数坑之后沉淀下来的那套做法完整讲清楚。从工具选型到框架差异从spec文件细节到安装包脚本全部按能直接复现的标准来写。需要先说清楚一点打包Web项目本质上不是把Python变成exe这么简单。你面对的是一个运行中的HTTP服务它有路由、有模板、有静态文件、有ORM、还有可能依赖SQLite数据库。这意味着打包方案需要考虑的不只是依赖收集还有启动方式、数据目录、生产服务器选型、浏览器自启等一连串问题。这也是网上大量教程只讲pyinstaller -F xxx.py却总是打包失败的根本原因——他们拿打包脚本的路子来套Web项目十有八九要翻车。下面这篇我尽量把细枝末节都写清楚。1. 为什么主推PyInstaller一件顺手但需要调整的工具PyInstaller应该是目前把Python打包成exe最主流的方案没有之一。它的核心思路一句话就能讲明白把Python解释器、你的代码、第三方依赖库全部收集到一起生成一个独立的可执行文件。对用户来说这就是一个普通的.exe双击就能跑和Python没有任何关系。我在动手之前也对比过其他几条路各有各的适用场景但综合下来PyInstaller在Web项目打包这个场景里胜出是有原因的。1.1 几种打包方案的横向对比方案原理优点缺点Web项目适用度PyInstaller打包解释器依赖代码生态成熟、资料多、开箱即用误报率相对高、体积大高Nuitka将Python转成C再编译性能好、误报率低、体积小配置复杂、对Django这种动态导入重的框架坑多中Docker生成镜像而非exe环境隔离、适合服务端部署客户机要装Docker不适合小白用户低Nativefier把网页地址壳化成exe适用于已有线上服务本质是套壳浏览器不是打包Python低PyInstaller对Django/Flask的适配是最成熟的PyInstaller官方Hook和社区贡献的Hook基本把这两个框架的常用依赖都覆盖了。网上搜Django PyInstaller能找到大量真实的打包案例遇到问题也容易排查。Nuitka虽然生成的exe更干净但对于Django这种大量使用元类、动态导入、装饰器、ORM反射的框架编译期静态分析经常漏掉模块补配置的工作量大到让人怀疑人生。另外我要泼一盆凉水如果你的目标是要一个像绿色软件一样单文件到处拷、双击就跑的exe那么PyInstaller的onefile模式直接做Django项目你会后悔的。原因很简单onefile每次启动都要将几十上百MB的依赖解压到临时目录启动时间少则几秒、多则十几秒杀毒软件对单文件大体积exe行为检测更严格误报概率更高一旦你想升级一个小功能整个文件要重新发布对于Web项目我推荐的是onedir模式生成一个目录里面有exe和配套文件。目录不丑而且后续做安装包时简直是天作之合。1.2 环境准备阶段最容易忽略的事情先把基础环境交代清楚。我这里以Windows 10/11 64位为例Python版本用的3.10。PyInstaller目前对Python版本有要求官方支持3.8到3.12但实测3.11最稳3.12在个别第三方依赖上会有兼容问题3.13暂时不推荐。安装PyInstaller一行命令pip install pyinstaller建议装到虚拟环境里不推荐全局装。原因后面讲依赖收集时会提到PyInstaller会把当前环境里所有包都扫描一遍虚拟环境能让打包体积小不少也不容易混入无关包。另外强烈建议先把项目所有依赖固定住生成一个干净的requirements.txtpip freeze requirements.txt这一步很多人嫌麻烦不做实际上打包过程如果依赖版本不一致经常出现在我电脑上跑得好好的打出来exe就报ModuleNotFoundError的问题。锁定依赖版本是打包Web项目最早的避坑动作。2. Django项目打包实操spec文件才是核心Django项目打包难难点不在命令本身而在资源收集和启动方式。我踩过的坑主要集中在这两块。先说结论不要试图用一行pyinstaller命令解决问题用spec文件来做。官方文档里明确写着复杂项目建议用spec文件。Django就是典型的复杂项目。2.1 手动写spec文件别图省事网上很多教程直接给一行命令pyinstaller -D --add-data templates;templates --add-data static;static manage.py这个写法在小Flask项目上可能跑通放到Django上基本会翻车因为Django的动态导入远比Flask复杂。它的ORM、管理命令、模板加载器、表单组件都有大量隐式导入PyInstaller静态分析经常漏掉。我建议的做法是在你项目的根目录下建立一个.spec文件。以下是我在完整打包一个Django项目时实际在用的配置你可以直接基于它改# myproject.spec # -*- mode: python ; coding: utf-8 -*- from PyInstaller.utils.hooks import collect_all datas [] binaries [] hiddenimports [] # 逐个收集对Django项目影响较大的包 for pkg in [ django, PIL, crispy_forms, # 如果你用了django-crispy-forms rest_framework, # 如果你用了DRF channels, # 如果你用了websocket ]: d, b, h collect_all(pkg) datas d binaries b hiddenimports h a Analysis( [run_server.py], pathex[], binariesbinaries, datasdatas, hiddenimportshiddenimports, hookspath[], runtime_hooks[], excludes[tkinter, matplotlib, pytest], noarchiveFalse, ) pyz PYZ(a.pure) exe EXE( pyz, a.scripts, exclude_binariesTrue, nameMyDjangoApp, debugFalse, bootloader_ignore_signalsFalse, stripFalse, upxTrue, consoleFalse, # False表示不显示黑窗口 ) coll COLLECT( exe, a.binaries, a.datas, stripFalse, upxTrue, nameMyDjangoApp, )这里最关键的是collect_all函数。它会自动收集指定包的所有子模块、数据文件、动态链接库相当于给PyInstaller开了个全收集模式。代价是体积会变大但对Django项目来说体积往往不是首要矛盾能跑起来才是。2.2 为什么入口不能直接用manage.py如果你直接打包manage.py最终exe运行起来会进入命令行交互模式等你输入命令。这显然不是交付给客户的产品形态。而且runserver是开发服务器Django官方明确说过它不适合生产环境性能差、并发能力弱还没有安全保护。你需要为打包单独写一个启动程序我命名为run_server.py放在项目根目录下。它要做四件事加载Django配置、自动执行数据库迁移、用生产级WSGI服务器启动服务、自动打开浏览器。# run_server.py import os import threading import webbrowser import socket import django from django.core.management import call_command os.environ.setdefault(DJANGO_SETTINGS_MODULE, myproject.settings) django.setup() # 确保数据目录存在重要见后文 from django.conf import settings data_dir os.path.join(os.path.expanduser(~), .myproject_data) os.makedirs(data_dir, exist_okTrue) # 如果SQLite数据库不存在自动执行迁移建表 db_path os.path.join(data_dir, db.sqlite3) if not os.path.exists(db_path): # 这里需要让Django知道数据库位置发生了改变 # 建议在settings.py里通过os.environ读取覆盖默认路径 call_command(migrate, interactiveFalse) # 用waitress启动生产服务器 from waitress import serve from myproject.wsgi import application port 8080 # 端口被占用时自动换下一个 while True: try: s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((127.0.0.1, port)) s.close() break except OSError: port 1 # 延迟2秒后打开浏览器给服务启动留点时间 threading.Timer(2, lambda: webbrowser.open(fhttp://127.0.0.1:{port})).start() serve(application, host127.0.0.1, portport, threads4)这段代码里藏着好几个打包后才会暴露出来的坑我逐个说一下第一为什么不用gunicorn或uwsgi这两个是Linux上最常用的WSGI服务器但它们是C扩展PyInstaller打包Windows可执行文件时非常麻烦。waitress是纯Python实现的WSGI服务器跨平台Windows下直接用pip安装打包时无任何额外负担。对中小型项目来说4个线程的处理能力足够应付内部工具站、管理后台、小规模商用了。第二为什么要把数据库放到用户目录这是打包Web项目最容易翻车的一个点。如果你不处理SQLite数据库文件会默认创建在项目目录下。当程序被安装到C:\Program Files\下时普通用户对这个目录只有读权限没有写权限数据库根本创建不出来程序直接崩溃。放到~/.项目名_data/目录下一切问题都解决了。第三为什么检测端口占用打包成exe后用户可能重复双击启动第一次启动的程序还占着8080端口第二次启动如果直接奔着8080去就会疯狂报错。自动检测并切换到下一个可用端口虽然不是最完美的方案但在桌面场景下足够实用。2.3 settings.py里的必要调整打包Django必须改settings.py否则exe就算生成了启动也会撞墙。我整理了几处最容易踩雷的地方# settings.py import os # 关掉调试模式必须改成False DEBUG False # 允许本机访问exe场景下通常是给本机用户用 ALLOWED_HOSTS [127.0.0.1, localhost] # 从环境变量读取数据库路径让run_server.py能控制其位置 import os DATA_DIR os.path.join(os.path.expanduser(~), .myproject_data) os.makedirs(DATA_DIR, exist_okTrue) DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: os.path.join(DATA_DIR, db.sqlite3), } } # 静态文件绝对不要放在项目内部依赖相对路径打包后目录结构会变 STATIC_ROOT os.path.join(DATA_DIR, static) STATIC_URL /static/ # SECRET_KEY在打包后也要固定否则每次启动session都失效 SECRET_KEY 你的固定密钥这里特别说一下SECRET_KEY。如果你原来的代码里写的是SECRET_KEY os.environ.get(SECRET_KEY, 开发用密钥)在开发环境没问题但打包后你不可能去设置环境变量最终会用默认值。只要默认值是固定的就没问题。但如果它在你开发时每次启动都重新随机那打包后每次启动session都会失效用户登录状态无法保持非常影响体验。静态文件的问题也要提前处理。Django项目的admin后台自带一套CSS、JS、图片如果你不做任何处理直接打包admin页面打开时是一堆没有样式的纯HTML能看但很痛苦。正确的做法是在执行collectstatic后把生成的静态文件目录手动复制到run_server.py同级的staticfiles目录里然后在spec文件的datas参数中显式加进去# spec文件中追加 datas [ (staticfiles, staticfiles), ]2.4 执行打包命令做好了spec文件后打包命令异常简单pyinstaller myproject.spec执行完成后dist/MyDjangoApp/目录下就是全套可执行文件。把这个目录直接拷到任何一台64位Windows电脑上双击MyDjangoApp.exe应该就能看到浏览器自动打开进入你的应用。如果哪台电脑报缺MSVCP140.dll或者VCRUNTIME140.dll说明那台机器缺少Visual C Redistributable运行库。PyInstaller正常情况下会带上这个DLL但个别精简版系统确实会缺。解决办法一是用--add-binary显式带上运行库文件二是在安装包里集成VC运行库这个后面讲安装包脚本的时候会涉及。3. Flask项目打包比Django简单但坑位不一样Flask打包总体比Django省心得多。它的轻量特性决定了依赖收集不像Django那样复杂模板和静态文件的处理方式也更直观。但有一个问题是Flask特有的sys._MEIPASS路径机制。理解不了这个机制打包后的Flask项目十有八九会找不到模板文件。3.1 核心原理sys._MEIPASS是打包后的文件新家先解释一下sys._MEIPASS是个什么东西。PyInstaller在onedir模式下exe所在的目录就是资源目录在onefile模式下程序启动时会先把所有资源解压到一个临时文件夹这个临时文件夹的路径就存储在sys._MEIPASS里。你写的代码里如果用了相对路径./templates开发时没问题但打包后当前工作目录并不一定是资源目录就会找不到文件。解决方式很统一写一个获取资源路径的辅助函数# utils.py import os import sys def base_dir(): if getattr(sys, frozen, False): # 运行在打包后的exe环境 return sys._MEIPASS else: # 运行在开发环境 return os.path.dirname(os.path.abspath(__file__))然后在Flask应用初始化时显式指定模板和静态文件目录# app.py import os from flask import Flask from utils import base_dir app Flask( __name__, template_folderos.path.join(base_dir(), templates), static_folderos.path.join(base_dir(), static), ) # 关闭调试模式打包后不能再开debug app.debug False if __name__ __main__: # 生产环境用waitress而不是app.run() from waitress import serve serve(app, host127.0.0.1, port5000, threads4)注意这里我同样选择了waitress而不是app.run()。Flask自带的开发服务器在同一条消息里只能处理一个请求一旦有浏览器在加载页面时同时发起多个请求HTML、CSS、JS、图片并行加载页面就会明显卡顿。waitress的多线程模型完美解决这个问题和一个真实的Web服务器表现完全一致。3.2 Flask项目的构建脚本Flask项目依赖少用命令行直接打包问题不大。但为了保证可复现性我还是建议做一个build脚本pyinstaller -D \ --name FlaskTool \ --add-data templates;templates \ --add-data static;static \ --hidden-import flask_cors \ --hidden-import waitress \ --exclude-module tkinter \ --exclude-module matplotlib \ --noconsole \ app.py这段命令里--add-data templates;templates的格式要注意分号前是源路径开发环境中的路径分号后是目标路径打包后exe目录中相对根目录的路径。如果模板里有子目录它会自动递归复制不需要一个个列出来。如果你用了flask_sqlalchemy、flask_login、flask_admin这类扩展建议在命令里都加上--hidden-import。这些扩展有些是按需导入的PyInstaller可能检测不到。Flask项目打包时还有个常见现象本地开发跑得好好的打包后执行exe访问页面报404。这个坑很多人会忽略原因是Flask蓝图注册路径与模板查找机制在打包后的差异。实际上并不是路径错了而是你忘了把蓝图对应的模板目录打进去。用--add-data把整个模板目录打包后通常就好了。3.3 Flask项目的数据文件处理Flask项目常常需要处理上传文件、生成临时Excel、读写配置文件。这些运行时产生的文件和前面说的打包时带进去的资源文件是两回事处理逻辑大不一样。打包时带进去的资源应该走sys._MEIPASS读取运行时产生的文件应该写到用户数据目录绝对不要写到exe所在目录。原因和Django项目的数据库问题一样如果exe被安装到C:\Program Files\下普通用户没有写权限任何写操作都会静默失败或者抛异常。import os from pathlib import Path # 用户数据目录在Windows上是 C:\Users\用户名\.flask_tool\ data_root Path(os.path.expanduser(~)) / .flask_tool data_root.mkdir(parentsTrue, exist_okTrue) # 上传文件存到这里 UPLOAD_FOLDER data_root / uploads UPLOAD_FOLDER.mkdir(exist_okTrue) app.config[UPLOAD_FOLDER] str(UPLOAD_FOLDER)把程序代码目录和用户数据目录彻底分开是打包桌面应用最基本的设计原则。这个原则越早落实后面的坑越少。4. 从exe到安装包用Inno Setup做专业交付物exe做出来了但离交付还有一步路。你总不能让客户自己去解压ZIP包然后双击exe吧。更专业的做法是做一个安装程序让用户像安装普通软件一样进行操作、有开始菜单快捷方式、有桌面图标、用完还能在控制面板-程序和功能里卸载。这里我推荐的工具是Inno Setup。它是Windows上主流的免费安装包制作工具脚本语法简单打包出来的安装程序专业感很强而且一个脚本可以反复维护。4.1 Inno Setup脚本详解先说思路Inno Setup本质上就是把dist\MyDjangoApp\整个目录内容压到安装包里安装时释放到指定位置再创建快捷方式。核心脚本如下; setup.iss [Setup] ; 应用基础信息 AppNameMyDjangoApp AppVersion1.0.0 AppPublisher你的名字或公司 DefaultDirName{autopf}\MyDjangoApp DefaultGroupNameMyDjangoApp UninstallDisplayIcon{app}\MyDjangoApp.exe ; 输出文件名 OutputBaseFilenameMyDjangoApp_Setup ; 压缩配置 Compressionlzma2 SolidCompressionyes ; 支持64位系统 ArchitecturesInstallIn64BitModex64compatible [Tasks] Name: desktopicon; Description: 创建桌面快捷方式; GroupDescription: 附加任务; Flags: unchecked [Files] ; 把dist\MyDjangoApp下的所有文件递归复制到安装目录 Source: dist\MyDjangoApp\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] ; 开始菜单快捷方式 Name: {group}\MyDjangoApp; Filename: {app}\MyDjangoApp.exe ; 桌面快捷方式受Tasks控制 Name: {autodesktop}\MyDjangoApp; Filename: {app}\MyDjangoApp.exe; Tasks: desktopicon [Run] ; 安装完成后默认勾选运行程序 Filename: {app}\MyDjangoApp.exe; Description: 立即运行 MyDjangoApp; Flags: nowait postinstall skipifsilent这个脚本里值得注意的有几点{autopf}宏代表系统默认的Program Files目录。Inno Setup会自动判断32/64位把程序装到正确的位置。**Flags: ignoreversion recursesubdirs createallsubdirs**这三个标志一个都不能少。ignoreversion表示不检查文件版本号直接覆盖否则老版本文件可能导致安装失败recursesubdirs表示递归子目录createallsubdirs表示空目录也要创建。UninstallDisplayIcon指定了在卸载或更改程序面板中显示的图标没有这行的话卸载面板里的图标会是默认的空白图标显得很不专业。4.2 数据与程序分离的安装包设计在上面Django项目部分我特意把数据库放到了用户目录~/.myproject_data/。之所以这么做和安装包的设计强相关。如果数据库直接放在Program Files下的程序目录里会带来三个问题第一普通用户对Program Files只有读权限没有写权限数据库根本创建不了。第二Windows的UAC权限机制会提示你需要管理员权限才能运行那你交付给用户的就不是一个双击就能跑的应用了。第三用户卸载程序时如果连数据库一起删了数据全丢这算是重大事故了。数据放用户目录后安装在Program Files的程序文件和用户的业务数据彻底分开。卸载软件时用户数据完好保留下次重装程序后数据依然还在。这种设计才是桌面应用的正规形态。如果项目里有一些运行时生成的日志、临时文件、上传附件同样建议全部指向用户目录。4.3 安装包版本更新问题软件不是一次性交付就完事后续迭代升级是必然的。Inno Setup天然支持升级安装你只需要在新版本安装包中使用相同的AppName和AppVersion或更高版本Inno Setup会检测到旧版本并把文件覆盖更新同时保留用户数据。升级更省事的做法是直接在Inno脚本里加上[Setup] ; 检测到旧版本时自动覆盖且默认安装到同一目录 UsePreviousAppDiryes钉死了UsePreviousAppDiryes用户升级时只会看到安装向导一闪而过旧目录下的数据库文件和上传文件都原样保留。这种体验已经接近商用软件的标准了。5. 高频踩坑实录从现象到根因的完整排查链路打包这条路我走的并不顺利中间折腾出一堆问题。这一节我把最有代表性的几个坑从头到尾展示一遍包括当时的报错现象、排查思路和最终解决办法帮助你在自己打包时少走弯路。5.1 报错Failed to execute script main要怎么查这是PyInstaller打包最著名的通用报错。你双击exe弹出一个黑窗口或者直接闪退什么有用信息都没有。有太多人卡在这一步就放弃了。正确的排查思路是这样第一步把consoleFalse改成consoleTrue重新打包。这会让exe启动时保留黑窗口错误信息和traceback会直接打印在黑窗口里这是定位问题的第一手资料。第二步看traceback最后一行。最常见的两类错误是ModuleNotFoundError和FileNotFoundError。如果是ModuleNotFoundError说明有第三方库的模块没被PyInstaller收集进去。解决办法就是在spec文件的hiddenimports列表里追加这个模块或者用collect_all把对应的包整体收集。如果是FileNotFoundError那么一定是代码里有读取外部文件的逻辑比如Django加载模板、Flask加载配置文件、Pandas读取数据源而且这个文件不在打包范围内。解决办法就是找到这个文件把它加到datas里。举一个实际例子我曾经打包一个用了django-cors-headers的项目运行时报ModuleNotFoundError: No module named corsheaders。解决方案就是在spec文件里加一行hiddenimports [corsheaders]几乎所有的闪退问题都能通过恢复黑窗口看到traceback来定位。所以如果你的exe双击没反应第一反应不是问别人而是先改回console模式看报错。5.2 URL路径拼接错误或者页面样式全丢了Django/Flask项目里如果用了request.build_absolute_uri()、url_for()这类动态生成URL的代码开发时没问题打包后可能出现HTTP地址异常、静态资源加载404的情况。排查的方向也明确打包成exe后程序的工作目录不再是你的项目目录而是系统目录或程序所在目录。如果你代码里有相对路径./static/xxx.css开发时还行打包后浏览器访问的其实是http://127.0.0.1:8080/static/xxx.css但由于静态文件没有被正确映射请求打不进去样式文件。解决办法是静态文件路径务必用Flask的url_for(static, filenamexxx.css)或Django模板里的{% static xxx.css %}去生成不要硬编码相对路径。同时确保打包命令里--add-data或spec文件的datas里正确收集了static目录。5.3 admin后台没有样式登录表单光秃秃Django项目打包后admin后台样式丢失是高频问题。原因刚才说过Django的admin静态文件不跟着templates走它们藏在Django包内部PyInstaller默认收集不到。如果你不想手动collectstatic还有一个相对省力的路子在spec文件里用collect_all(django)把这整个包所有文件全打进去admin的静态文件也就自然带上了。缺点是很耗体积但对于本地工具站这不算致命问题。我用这个方式实测下来admin后台的样式、交互、富文本组件都能正常显示效果和开发环境里完全一致。5.4 杀毒软件报毒或误删exe这是PyInstaller用户最头疼的问题很多杀毒软件对PyInstaller生成的exe会产生误报。原因不复杂PyInstaller的引导器bootloader会把Python代码解压到内存执行这种自解压再执行的模式和部分恶意软件的行为模式有相似性杀毒软件的启发式扫描就会报警。实践下来有几个办法降低误报概率优先用onedir模式不用onefile。onefile模式启动时释放文件到临时目录的动作更容易被扫描器盯上onedir模式下所有文件都原样躺在目录里误报率明显降低。给exe加上版本信息和图标。使用--icon和--version-file参数。一个带完整版本信息和图标的exe在杀毒软件眼里可信度会高不少。用UPX压缩时谨慎。UPX压缩会改变exe的二进制结构据说也会提高误报率。如果误报严重可以去掉upxTrue代价是体积变大一点。真正正式的商业分发应该做代码签名不过那需要买证书个人项目阶段可以先不管。5.5 防火墙弹窗第一次启动被拦截这个坑很多打包Web项目的人都会忽略因为开发时不会遇到。第一次双击exeWindows防火墙会弹出提示Windows 安全中心已阻止此应用的部分功能。原因很简单你的Python程序在监听127.0.0.1的端口Windows把这种行为视为网络服务要你授权。如果用户点了取消程序照样能跑——因为监听的是127.0.0.1仅本机回环地址并不需要入站规则但如果监听的是0.0.0.0允许局域网内其他设备访问那么取消授权后别的电脑就无法访问你的服务了。要彻底避免这个弹窗一种方式是在Inno Setup安装脚本里直接加入防火墙放行规则[Run] ; 添加防火墙入站规则允许局域网访问 Filename: netsh; Parameters: advfirewall firewall add rule nameMyDjangoApp dirin actionallow program{app}\MyDjangoApp.exe enableyes; Flags: runhidden注意这段命令需要管理员权限所以安装包运行时Inno Setup会自动弹出UAC授权这是正常的。如果你只是本机使用不准备开放局域网127.0.0.1就不会触发防火墙弹窗可以不加这段。5.6 启动太慢双击后等半天pyinstaller生成exe的启动速度通常比源码运行要慢特别是onefile模式因为每次启动都要解压大量文件。实测一个中型Django项目onefile模式启动大约需要8到15秒onedir模式大约需要2到4秒。在能不能再快点这个问题上我有两个实用建议建议一加个启动欢迎界面。你用专业软件时用户双击exe后总得有几秒等待如果一点反馈都没有用户会以为没启动成功。可以在run_server.py最前面用ctypes弹一个简单的消息框或者干脆用tkinter做一个简易splash窗口服务启动完成后自动关闭。建议二把耗时任务放到后台线程。像数据库迁移、collectstatic这些操作如果放在主流程中同步执行会导致启动时间进一步拉长。可以改成后台线程执行将碎片时间吸收掉。我用onedir模式加一个简短的启动提示用户体验已经和正常的桌面软件没有明显差异了。5.7 生产环境不能用runserver的深层原因最后再强调一遍这个问题因为这是新手翻车率最高的一点。python manage.py runserver是Django开发时用的服务器它有几个致命问题不支持并发。默认是单线程阻塞模式一个请求处理完后才能处理下一个。哪怕只有两三个用户同时访问页面就会变得异常卡顿。没有生产级防护。runserver是给开发者调试用的它在报错页面里会输出详细的错误堆栈、环境变量、配置信息。打包后的程序如果直接暴露给真实用户这些信息等于把后门钥匙交出去了。性能差距。runserver每处理一个请求都有额外的调试检查内存和CPU开销都远高于生产级WSGI服务器。打包后完全可以用waitress替代runserver几十行代码就能接入性能和并发能力提升非常明显。这也是我在run_server.py中坚持用waitress的原因。6. 实战中的扩展把exe变成Windows服务如果你的技术支撑目标从给同事用升级到给客户提供7x24小时服务那么只做exe还不够。exe是用户登录后手动启动的东西服务器重启后没人登录服务就断了。这时候需要把程序注册为Windows服务让它开机自启、后台运行、崩溃自动重启。PyInstaller和waitress的组合配合一个名为pywin32的库可以实现这个目标。# service.py import servicemanager import socket import sys import win32event import win32service import win32serviceutil class MyDjangoService(win32serviceutil.ServiceFramework): _svc_name_ MyDjangoService _svc_display_name_ MyDjangoApp Service _svc_description_ MyDjangoApp 后台服务 def __init__(self, args): win32serviceutil.ServiceFramework.__init__(self, args) self.hWaitStop win32event.CreateEvent(None, 0, 0, None) socket.setdefaulttimeout(60) def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) win32event.SetEvent(self.hWaitStop) def SvcDoRun(self): servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE, servicemanager.PYS_SERVICE_STARTED, (self._svc_name_, )) self.main() def main(self): from waitress import serve from myproject.wsgi import application serve(application, host0.0.0.0, port8080, threads8) if __name__ __main__: win32serviceutil.HandleCommandLine(MyDjangoService)这个文件既可以当普通脚本运行调试模式也可以用python service.py install注册成Windows系统服务。客户那边如果要求开机以后什么都不用点服务自己就起来了这个方案是正解。在按照上面这一整套流程跑通之后我的感受相当深。PyInstaller本身不复杂真正复杂的是Django/Flask这类Web框架自身的特性和Windows桌面环境的差异。理解了代码目录、资源目录、用户数据目录这三者的区别打包的过程就变成了一个顺理成章的流程把代码打进去、把资源带上、把数据引导到用户目录、用生产级服务器启动、做成安装包进行分发。最后给一个实在的收尾建议打包Web项目的核心诉求是让别人用起来容易所以做安装包时一定要站在你最笨的用户角度检查一遍——他双击安装包、点下一步、点完成然后浏览器自动打开看见你的系统界面他就应该会用了。任何需要他手动操作、手动打开、手动输入地址的环节都是你的设计失败。从这个标准出发去优化你的打包产物你的Python项目才真正算完成了交付闭环。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑