Skip to content

Django 开发笔记

Django 项目打包exe本地运行

记一次离谱的需求

其实本来觉得Django项目架构比较清晰,代码逻辑也简单,没打算记笔记,结果遇到离谱需求折腾了很久

开发了一个Django项目,到交付的时候了,客户说自己没有服务器...

没服务器还要登录功能😓

没办法,甲方最大,整吧

第一步,迁移数据库

项目数据库是基于服务器上的pgsql,先迁移到本地sqlite

先把数据导出到本地文件

shell
python manage.py dumpdata

理论上是用这个命令,但是我好像报了编码错误,所以编写了下边这个python脚本

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里的数据库配置了,改成

python
DATABASES = {
    "default": {
        "ENGINE": 'django.db.backends.sqlite3',
        "NAME": BASE_DIR / 'db.sqlite3',
    }
}

创建数据库

shell
python manage.py makemigrations

这个命令是基于模型类代码来检查模型的变化,并生成相应的迁移文件

shell
python manage.py migrate

在数据库中创建相应的表和字段

导入数据

执行以下命令即可

shell
 python manage.py loaddata data.json

第二步,修改项目配置

修改静态资源文件和模板文件访问路径

大概修改这几个就够了

python
STATIC_URL = "static/"

STATIC_ROOT = os.path.join(BASE_DIR, "static/")

TEMPLATE_DIRS = [
    os.path.join(BASE_DIR, "templates")
]

DEBUG = False

静态资源文件访问路径重定向

urls.py文件末尾添加代码

python
urlpatterns += re_path(r'static/(?P<path>.*)$', serve, {'document_root': settings.STATIC_ROOT})

这样就可以在http请求静态资源文件时重定向到本地文件了~

这里要注意,修改的urls.py文件位置!!应该是和setting.py同级,这里有同事给我挖坑,害我折腾好久调不出来

第三步,编写运行脚本

先让项目跑起来

根目录下编写脚本 run.py

python
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()

执行命令打包运行脚本

shell
 pyi-makespec --onefile --windowed run.py

会生成一个spec文件,然后手动修改datas,用于目录映射,修改name指定输出文件名称

运行pyinstaller.exe .\run.spec以生成exe文件,可能会有import报错,哪个模块报错就把哪个模块加到hiddenimports里,最后得到以下run.spec文件

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方法之前添加以下代码

python
url = 'http://localhost:8000'
webbrowser.open_new(url)

防止重复运行导致端口占用

run.py文件main方法之前添加以下代码

python
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))