Django 项目打包exe本地运行
记一次离谱的需求
其实本来觉得Django项目架构比较清晰,代码逻辑也简单,没打算记笔记,结果遇到离谱需求折腾了很久
开发了一个Django项目,到交付的时候了,客户说自己没有服务器…
没服务器还要登录功能😓
没办法,甲方最大,整吧
第一步,迁移数据库
项目数据库是基于服务器上的pgsql,先迁移到本地sqlite
先把数据导出到本地文件
python manage.py dumpdata
理论上是用这个命令,但是我好像报了编码错误,所以编写了下边这个python脚本
import os
from django.core import serializers
from django.apps import apps
import json
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'efplctp.settings')
import subprocess
import sys
import os
# 获取虚拟环境中 Python 解释器的完整路径
# 如果你使用的是 Windows,路径可能是 venv\Scripts\python.exe
# 如果你使用的是 Unix/Linux/macOS,路径可能是 venv/bin/python
python_executable = os.path.join(os.path.dirname(sys.executable), 'python')
# 构造完整的命令
command = [python_executable, "manage.py", "dumpdata"]
# 运行命令并捕获输出
try:
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8")
if result.returncode == 0:
# 将输出保存到文件
with open("data.json", "w", encoding="utf-8") as f:
f.write(result.stdout)
print("数据已成功导出到 data.json")
else:
print("导出数据时出错:", result.stderr)
except Exception as e:
print("运行命令时发生错误:", e)
修改项目数据库配置
导出了数据库文件,就可以修改setting.py里的数据库配置了,改成
DATABASES = {
"default": {
"ENGINE": 'django.db.backends.sqlite3',
"NAME": BASE_DIR / 'db.sqlite3',
}
}
创建数据库
python manage.py makemigrations
这个命令是基于模型类代码来检查模型的变化,并生成相应的迁移文件
python manage.py migrate
在数据库中创建相应的表和字段
导入数据
执行以下命令即可
python manage.py loaddata data.json
第二步,修改项目配置
修改静态资源文件和模板文件访问路径
大概修改这几个就够了
STATIC_URL = "static/"
STATIC_ROOT = os.path.join(BASE_DIR, "static/")
TEMPLATE_DIRS = [
os.path.join(BASE_DIR, "templates")
]
DEBUG = False
静态资源文件访问路径重定向
在urls.py
文件末尾添加代码
urlpatterns += re_path(r'static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT})
这样就可以在http请求静态资源文件时重定向到本地文件了~
这里要注意,修改的urls.py文件位置!!应该是和setting.py同级,这里有同事给我挖坑,害我折腾好久调不出来
第三步,编写运行脚本
先让项目跑起来
根目录下编写脚本 run.py
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'efplctp.settings')
# 手动设置 sys.argv
sys.argv = ['manage.py', 'runserver', '0.0.0.0:8000', '--noreload']
with open('log.txt', 'w') as f:
# 将标准输出重定向到文件对象,
# 这一步是为了在打包后讲打印输出到这里
#(其实输出也没什么用,但是不这么写的话,
# 在打包成运行无命令行窗口的模式
#( pyi-makespec --onefile --windowed run.py )时会报错)
sys.stdout = f
# 初始化 Django
django.setup()
# 执行命令
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()
执行命令打包运行脚本
pyi-makespec --onefile --windowed run.py
会生成一个spec文件,然后手动修改datas,用于目录映射,修改name指定输出文件名称
运行pyinstaller.exe .\run.spec
以生成exe文件,可能会有import报错,哪个模块报错就把哪个模块加到hiddenimports
里,最后得到以下run.spec
文件
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['run.py'],
pathex=[],
binaries=[],
datas=[('static', './static'),('biz/templates', './templates'),('efplctp/templates', './templates'),('db.sqlite3', './')],
hiddenimports=[
"common.middlewares",
"django.contrib.sessions",
"biz",
"django.contrib.staticfiles",
"django.contrib.contenttypes",
"common.middlewares",
"django.contrib.sessions.templatetags",
'django.templatetags.future',
'django.templatetags.i18n',
'django.templatetags.l10n',
'django.contrib.admin.templatetags.log',
'django.contrib.admin.templatetags.admin_urls',
'django.contrib.admin.templatetags.admin_modify',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='efplctp',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
至此打包初步完成
双击运行后自动打开浏览器
在run.py
文件main
方法之前添加以下代码
url = 'http://localhost:8000'
webbrowser.open_new(url)
防止重复运行导致端口占用
在run.py
文件main
方法之前添加以下代码
def kill_process(*pids):
for pid in pids:
a = os.kill(pid, signal.SIGILL)
print('已杀死pid为%s的进程, 返回值是:%s' % (pid, a))
def get_pid(*ports):
# 其中\"为转义"
pids = []
for port in ports:
str = os.popen("netstat -ano | findstr :%s" % (port)).read()
tmp = [i for i in str.split(' ') if i != '']
if len(tmp) > 4:
pid = tmp[4]
pids.append(int(pid))
print(str)
print(pids)
return pids
# 杀死占用端口号的ps进程
ps_ports = ["8000"]
kill_process(*get_pid(*ps_ports))