- pyinstaller打包django项目踩坑 -

工作需要把django应用的环境打包,生成exe
花了一下午踩坑pyinstaller打包,稍微memo一下
(Django==2.2.5 | Python==3.8.5 | win)

  • 项目文件结构,以下称“本GM项目”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    ├─.vscode
    ├─apps
    │ ├─common
    │ ├─DailyReport
    │ ├─GM_system
    │ ├─performance
    │ ├─user
    │ ├─。。。。。。。。。
    │ └─__pycache__
    ├─config
    ├─core
    │ └─__pycache__
    ├─download
    ├─GM
    │ └─__pycache__
    ├─log
    ├─mysql
    ├─static
    │ ├─css
    │ ├─images
    │ ├─js
    │ ├─plugins
    │ ├─resources
    ├─templates
    │ ├─dailyReport
    │ ├─layout
    │ ├─login
    │ ├─performance
    │ ├─public
    │ └─user
    │ └─。。。。。。。
    └─__pycache__

安装pyinstaller

pip安装pyinstaller库

1
pip install pyinstaller

生成打包配置文件specfile

cd进入项目根目录,执行命令生成spec文件,文件位于当前目录下

1
pyi-makespec -D manage.py

生成的文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['manage.py'],
pathex=['D:\\SVN\\GMs\\03_Source\\trank\\GMsV3.0'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
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,
[],
exclude_binaries=True,
name='manage',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='manage')

其中exe项里的name值为生成exe的文件名
console项表示运行是否显示控制台终端
若需要指定生成exe文件的图标,则可以在其中新增一项icon,如icon='favicon.ico'

打包生成exe

  • 运行如下命令打包项目生成exe
    1
    pyinstaller manage.spec

    打包时似乎会将当前python环境里安装的所有包都打包进去
    可能报一些和最终生成无关的其他包的依赖错误等,打包时会较慢
    并且可能会由于打包进了很多无关项目的包而导致最终生成的目录大小较大
    虽说可能无关紧要可以忽略,但强迫症可能就难受了,可以事先准备一个没有多余包的纯净python环境

生成后项目目录结构将会多出build与dist目录
dist目录即为生成的打包项目,exe生成在dist\manage\目录里

  • 运行项目
    1
    2
    rem cd进入manage.exe的目录下
    .\manage.exe runserver

踩坑Start

hiddenimports配置

直接生成exe后运行报错提示如No module named XXX
这是因为打包后的Django有些module不会自动收集,需要手动添加
需要在spec文件中修改hiddenimports列表,将未导入的包添加进去
如下为这次在GM项目中修改的hiddenimports

1
2
3
hiddenimports=["core.perms", "corsheaders", "corsheaders.middleware", "django.middleware.common",
"django.contrib.auth.templatetags", "apps.suloss.templatetags", "apps.efwpf.context_processors",
"django.contrib",],

TemplateDoesNotExist

解决上面的问题之后应该就能正常启动项目
但是打开网页就会报TemplateDoesNotExist,找不到前端模板
由于打包程序只讲python代码部分打包,所以template需要自行复制到打包后的项目目录中
根据错误的提示消息,把项目中的模板文件templates拷贝到相应的位置,刷新页面即可。

于GM项目,我将templates目录复制到dist\manage\下即可

xx静态资源xx lost

既然template都没有被打包进去,那么项目引用的静态资源更不必说了
解决完上述问题后网页或许可以打开,但引用的本地的静态css和js都会404 not found
这里的解决方法如下

  1. 首先在项目的settings.py文件中添加如下代码,其中static是项目中的静态文件位置,static_root是static下的一个空文件夹,然后执行python manage.py collectstatic命令将静态文件收录到static_root中
    1
    STATIC_ROOT = os.path.join(BASE_DIR, 'static', 'static_root')
  2. 然后在项目的urls.py文件中添加如下代码
    1
    2
    3
    4
    5
    from django.conf.urls import static
    from project_1 import settings
    # project_1记得改成项目settings所在的目录名

    urlpatterns += static.static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

到这里之后再重新打包生成项目即可√

关于STATIC_ROOT配置,以及python manage.py collectstatic命令的作用等
建议看一下下面链接的文章,我就不在这里再写了(lazy…)
django中STATIC_ROOT和STATICFILES_DIRS区别

简单理解为STATICFILES_DIRS是开发环境的静态资源目录配置,STATIC_ROOT是生产环境的静态资源目录配置,二者目录不可相同

由于项目中导出xlsx等功能不好再将静态资源放在static\static_root\目录下,
我最后的做法是:在打包项目时,将原来的STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'), )配置注释掉,
STATIC_ROOT配置改为STATIC_ROOT = os.path.join(BASE_DIR, 'static')
这样打包后的静态资源目录仍然是static\

anything else

下面问题可能只与本GM项目相关,仅供参考

log目录

解决上述问题后启动仍报错:ValueError: Unable to configure handler 'collect',出在log相关的代码处
上面也报有FileNotFoundError: [Errno 2] No such file or directory: 'D:\\SVN\\GMs\\03_Source\\trank\\GMsV3.0\\dist\\manage\\log\\collect.log'的错误,想必是并没有生成log目录,也无法在不存在的目录下写入collect.log等log

解决方法即照报错信息,在打包项目目录下新建log目录即可

conf目录

启动时报错[16508] Failed to execute script manage
具体报错FileNotFoundError: [Errno 2] No such file or directory: 'D:\\SVN\\GeneralMills\\03_Source\\trank\\GeneralMillsV3.0\\dist\\manage\\config\\email.json'
报错处代码读取该文件失败,观察打包的项目中并无config目录,更不会有email.json文件

GM项目中数据库配置与email配置放在config目录中未被打包,将原项目中config目录复制到打包的项目中即可
然而坑爹的是,可能与项目代码读取配置文件的方式有关,读取的目录总是在启动项目命令执行的当前目录中,若在桌面或其他目录中使用exe的绝对路径启动项目,项目会在当前执行的目录中尝试读取config目录的文件
这点会影响到之后批处理/运行脚本的编写,即需要cd到exe所在目录中后再运行

静态资源请求被中间件拦截

上面两个问题简单来说就是“缺啥补啥”,然而本项目中更坑爹的事情出现了。。
进入页面之后退出登录,进入login界面时,页面竟然渲染不了样式和js,F12查看所有资源均为200正常请求资源,这就很奇怪了
后来点进去那些请求回来的静态资源,发现无论css或者是js全部变成了
<html><body onLoad="alert('登录过期, 请重新登录!');window.top.location.href='/login'" ></body></html>
同时提醒后发现,退出登录状态后,验证权限的中间件将静态资源请求也拦截了下来,
原本用django manage.py直接启动时,django会处理本地静态资源所以并不会被拦截
然而打包的exe启动后,所有的静态资源请求也变成了“外部请求”,请求会走中间件,于是被拦截
解决方法即最后在中间件中判断若请求为/static/开头则放行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def process_request(self, request):
request_url = request.path_info
# print(request_url)
for url in settings.SAFE_URL:
if re.match(url, request_url):
return None

######
if request_url.startswith('/static/'):
return None
######

userInfo = request.session.get('userInfo', None)

if not userInfo:
return self.is_ajax_return(request, "登录过期, 请重新登录!")

参考链接

Django框架14 /pyinstaller打包django项目成exe文件
pyinstaller打包django项目
bat脚本创建桌面快捷方式