使用nuitka打包python代碼為exe可執行程序


@


前言

在項目中,我負責開發一個用於圖像處理的可視化圖形界面。人生苦短,我用python。python首選的GUI框架當然是pyqt5啦!項目很快就完成了,現在需要將我寫的程序打包成exe文件分發給客戶。python寫代碼一時爽,打包交付卻沒有C++版的QT方便。現有的python打包方式主要是使用pyinstaller,但是在調研過程中,發現pyinstaller打包的程序一般都非常大,而且運行的速度很慢。然后我就注意到了一個較新的工具nuitka,據說打包速度快,打包完的程序也不大,剛好符合我現有的需求——這時我還沒意識到我面臨的問題。由於這是一個較新的工具,在打包過程中我遇到了很多坑,而且可參考的解決方法並不多,然后我就開始了自己的填坑之路……也學到了教訓——新技術不要輕易嘗試,除非你有解決問題的能力。


一、nuitka是什么?

網上關於nuitka介紹一開始是看下面幾個帖子:
Python打包exe(32/64位)-Nuitka再下一城
Nuitka之乾坤大挪移-讓天下的Python都可以打包
nuitka使用參考
看了幾遍總覺得稀里糊塗,一篇文章能說清楚的內容分成了好幾篇文章,還相互引用,好像什么也沒說清楚,只是告訴你怎么做,我按照同樣的方法總會出現各種各樣的問題。最后想明白了,只有理解了內容才能因地制宜地解決問題,最好的使用一個工具的教程往往是官方文檔,其次才是別人總結的博客。

nuitka是一個用來將python代碼打包為exe可執行文件,方便其在沒有相關環境的windows系統上運行的工具(貌似也支持打包成linux系統下的可執行程序,沒需求暫未嘗試)。其原理為:將部分python代碼(自己寫的部分)轉換成C代碼,以提高運行的速度;import的第三方包不進行編譯,在運行時,通過一個python3x.dll的動態鏈接庫執行第三方包的python代碼,通過這樣的方式減少exe包的大小。

二、nuitka打包流程

我的python環境

conda 4.7.12
Python 3.6.13
numpy 1.16.4
pyqt5 5.15.4

1.下載C編譯器

nuitka的原理就是將部分代碼轉換為C,然后進行編譯,所以需要先下載C編譯器。
(1)下載MinGW64 8.1,目前為止還是這個版本最穩定。下載地址:https://sourceforge.net/projects/mingw-w64/files
百度網盤下載 密碼:8888
在這里插入圖片描述
(2)將文件3 MinGW64 8.1 解壓到C盤,並添加環境變量
在這里插入圖片描述
在這里插入圖片描述
(3)打開cmd命令,使用gcc.exe --version測試是否添加上。一個坑:之前如果安裝過c編譯器可能添加過gcc環境變量導致MinGW64 8.1的環境變量被覆蓋,早期的gcc版本在編譯代碼中可能會出現bug。
在這里插入圖片描述
(4)其他兩個文件在安裝Nuitka時會用上

2.下載Nuitka

(1)pip install nuitka 或者 conda install nuitka
python環境下載工具應該是很基本的內容,速度慢可以添加鏡像源,這一部分不再贅述

3.使用nuitka簡單打包python代碼

(1)新建一個簡單的python文件,測試運行沒有出錯
(2)使用nuitka xxx.py命令進行打包。在打包過程中會有提示下載一個包到***\nuitka\***這樣一個文件夾中,下載進度條可能不動或者很慢,就可以使用 ctrl + C終止進程,手動將百度雲下載的文件1解壓到提示的這個文件家中
(3)重新使用nuitka xxx.py命令進行打包。還會提示下載另一個包,同樣的方式將文件2解壓放入
(4)重新使用nuitka xxx.py命令進行打包,這次應該就沒問題了

4.使用nuitka打包pyqt5項目

先介紹以下nuitka的打包命令:

--mingw64 默認為已經安裝的vs2017去編譯,否則就按指定的比如mingw(官方建議)
--standalone 獨立環境,這是必須的(否則拷給別人無法使用)
--windows-disable-console 沒有CMD控制窗口
--output-dir=out 生成exe到out文件夾下面去
--show-progress 顯示編譯的進度,很直觀
--show-memory 顯示內存的占用
--include-qt-plugins=sensible,styles 打包后PyQt的樣式就不會變了
--plugin-enable=qt-plugins 需要加載的PyQt插件
--plugin-enable=tk-inter 打包tkinter模塊的剛需
--plugin-enable=numpy 打包numpy,pandas,matplotlib模塊的剛需
--plugin-enable=torch 打包pytorch的剛需
--plugin-enable=tensorflow 打包tensorflow的剛需
--windows-icon-from-ico=你的.ico 軟件的圖標
--windows-company-name=Windows下軟件公司信息
--windows-product-name=Windows下軟件名稱
--windows-file-version=Windows下軟件的信息
--windows-product-version=Windows下軟件的產品信息
--windows-file-description=Windows下軟件的作用描述
--windows-uac-admin=Windows下用戶可以使用管理員權限來安裝
--linux-onefile-icon=Linux下的圖標位置
--onefile 像pyinstaller一樣打包成單個exe文件(2021年我會再出教程來解釋)
--include-package=復制比如numpy,PyQt5 這些帶文件夾的叫包或者輪子
--include-module=復制比如when.py 這些以.py結尾的叫模塊
--show-memory 顯示內存
--show-progress 顯示編譯過程
--follow-imports 全部編譯
--nofollow-imports 不選,第三方包都不編譯
--follow-stdlib 僅選擇標准庫
--follow-import-to=MODULE/PACKAGE 僅選擇指定模塊/包編譯
--nofollow-import-to=MODULE/PACKAGE 選擇指定模塊/包不進行編譯

命令比較多,根據需要進行選擇。我的需求是,編譯包含pyqt5的代碼,需要console進行調試(代碼中的print會顯示在console中),我的項目結構為:

- package
	- file1.py
	- file2.py

- utils
	- file3.py

- start.py

打包思路:
start.py作為主窗口的啟動器,只引入pyqt5這一個第三方庫。對start.py進行編譯,package和utils兩個包以及第三方包都不編譯,后期直接將它們作為依賴放在exe可執行文件同一個文件夾下。

缺點:package和utils兩個文件沒有編譯也沒有加密,以源代碼的方式直接使用python解釋器執行(其實比較蠢,相當於直接把源代碼給別人了,但是我的代碼里貌似沒有需要加密的內容,單純為了讓我的代碼可以在沒有環境的windows電腦上運行)。當然為了保密將其也進行編譯也可以,方法后面介紹。

優點:可以避免幾乎所有打包的坑。同時,方便改代碼,打包好的入口程序可以直接調用我的python代碼,我的python代碼如果有修改,直接覆蓋原來的.py即可,不用再編譯。

我使用的命令為:

nuitka --standalone --mingw64 --show-progress --show-memory --nofollow-imports --plugin-enable=qt-plugins --include-qt-plugins=sensible,styles  --output-dir=out --windows-icon-from-ico=favicon.ico 軟件的圖標 start.py
// --standalone環境獨立
// --mingw64選擇之前下載的C編譯器
// --show-progress --show-memory顯示進度和內存
// --nofollow-imports所有包都不編譯
// --plugin-enable=qt-plugins --include-qt-plugins=sensible,styles 添加qt插件,導入相關包
//  --output-dir=out --windows-icon-from-ico=favicon.ico 導出路徑以及圖標

打包完成后會生成一個文件夾,包含xx.build和xx.dist兩個目錄,前一個無用。后一個就是我們需要的打包好的文件夾,里面有一個exe可執行文件。

調試添加包的過程:
(1)在xx.dist目錄下打開cmd或者powershell(shift+鼠標右鍵,點擊打開powershell)
(2)運行./xx.exe
(3)查看報錯信息,缺少的第三方包就從D:\Anaconda3\envs\QT5(虛擬環境路徑)下搜索關鍵字,將其復制保存到xx.dist目錄下。缺少的自己的包,也將其復制過來。一步一步調試,直到把所有的依賴包全都復制到這個目錄下,程序完美執行。(記得把這些依賴包復制備份)
(4)之前的命令打包的程序為了調試有一個console黑框框,正式打包的話不要console,在之前的命令中添加--windows-disable-console,具體命令為:

nuitka --standalone --mingw64 --show-progress --windows-disable-console --show-memory --nofollow-imports --plugin-enable=qt-plugins --include-qt-plugins=sensible,styles  --output-dir=out --windows-icon-from-ico=favicon.ico 軟件的圖標 start.py

(5)重新打包好的程序,將之前備份的依賴包復制到相同位置,運行./xx.exe,成功運行!
(6)如果后續需要程序方便分發給用戶,可以使用Inno Setup編輯器將exe封裝成安裝包的方式,使用方法參考怎么把exe程序制作成安裝包,傻瓜式操作即可。

關於自己代碼必須加密編譯的情況(本人沒有嘗試,后續有需求再說):
可以自己嘗試使用:

nuitka --mingw64 --module --show-progress --output-dir=o peewee.py
// --module是將需要加密部分代碼按照模塊進行編譯
// 會生成一個.pyd文件,這部分代碼可以放到Python3x\Lib\site-packages\目錄下,測試程序是否完美運行,再嘗試打包整個exe,它會把這個pyd一塊兒打進exe。
// 也可以放在最終打包的exe同目錄下,通過python3x.dll調用

5.打包過程遇到的坑

  • 首先nuitka對第三方包的導入很坑,除了--plugin-enable=qt-plugins --include-qt-plugins=sensible,styles對pyqt5相關包的導入編譯沒大的問題,對numpy,cv2,scipy等庫編譯都出現過問題,尤其是numpy(當然pandas,torch等庫我的代碼里沒有涉及)。所以我直接--nofollow-imports不包含所有包,包括自己的包。
  • 那些教程里會提到使用--follow-import-to=need把自己寫的包一同打入exe,但是他們都沒說清楚,使用這個方式必須得need包里的代碼沒有import numpy等第三方庫,否則也會把第三方包打進去!
  • 和上一個情況類似,start.py作為啟動的程序,代碼里也不要包含太多第三方庫(cv2和numpy不要同時有,原因我猜cv2有部分依賴numpy的代碼,打包后會生成numpy文件夾,導致自己導入numpy文件后會找不到numpy模塊有沖突的情況,具體錯誤代碼ModuleNotFoundError: No module named 'numpy._globals')。所以我干脆start.py啟動器只導入PyQt5包。
  • 路徑問題,代碼中出現的相對路徑在打包后會出現找不到資源文件的情況,可以使用下面的方式:
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 得到當前工作路徑
LAST_DIR = os.path.abspath(os.path.dirname(BASE_DIR)) # 當前上層路徑

# 調用資源文時,使用這種方式
filepath = os.path.join(LAST_DIR,"icon.png")

6.移植過程遇到的坑

關於打包好的exe可執行文件在自己電腦上可以完美運行,但是移植到其他電腦上卻出現問題。建議移植的時候還是使用帶console的測試版本,方便打印錯誤日志。
我遇到的相關的錯誤:
錯誤代碼:

Traceback (most recent call last):
  File "G:\調試\numpy\core\__init__.py", line 17, in <module>
    from . import multiarray
  File "G:\調試\numpy\core\multiarray.py", line 14, in <module>
    from . import overrides
  File "G:\調試\numpy\core\overrides.py", line 7, in <module>
    from numpy.core._multiarray_umath import (
ImportError: DLL load failed: 找不到指定的模塊。

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "G:\調試\start.py", line 13, in <module>
  File "G:\調試\ImageProcessing\MainWiget.py", line 20, in <module>
    from utils.myUtils import MyUtils
  File "G:\調試\utils\myUtils.py", line 4, in <module>
    import numpy as np
  File "G:\調試\numpy\__init__.py", line 142, in <module>
    from . import core
  File "G:\調試\numpy\core\__init__.py", line 47, in <module>
    raise ImportError(msg)
ImportError:

IMPORTANT: PLEASE READ THIS FOR ADVICE ON HOW TO SOLVE THIS ISSUE!

Importing the numpy c-extensions failed.
- Try uninstalling and reinstalling numpy.
- If you have already done that, then:
  1. Check that you expected to use Python3.6 from "G:\調試\python.exe",
     and that you have no directories in your PATH or PYTHONPATH that can
     interfere with the Python and numpy version "1.17.3" you're trying to use.
  2. If (1) looks fine, you can open a new issue at
     https://github.com/numpy/numpy/issues.  Please include details on:
     - how you installed Python
     - how you installed numpy
     - your operating system
     - whether or not you have multiple versions of Python installed
     - if you built from source, your compiler versions and ideally a build log

- If you're working with a numpy git repository, try `git clean -xdf`
  (removes all files not under version control) and rebuild numpy.

Note: this error has many possible causes, so please don't comment on
an existing issue about this - open a new one instead.

Original error was: DLL load failed: 找不到指定的模塊。

這個一開始我以為是numpy版本的問題,我是用conda numpy list命令發現有兩個numpy——numpy和'numpy-base'於是全卸載了,重裝了1.16.4版本,錯誤代碼也變了:

Traceback (most recent call last):
  File "G:\start.dist\start.py", line 13, in <module>
  File "G:\start.dist\ImageProcessing\MainWiget.py", line 20, in <module>
    from utils.myUtils import MyUtils
  File "G:\start.dist\utils\myUtils.py", line 4, in <module>
    import numpy as np
  File "G:\start.dist\numpy\__init__.py", line 140, in <module>
    from . import _distributor_init
  File "G:\start.dist\numpy\_distributor_init.py", line 34, in <module>
    from . import _mklinit
ImportError: DLL load failed: 找不到指定的模塊。

綜上,其實最根本的錯誤還是ImportError: DLL load failed: 找不到指定的模塊,跟版本沒有關系。最后想到移植程序跟環境路徑肯定有很大關系。添加環境路徑,表示不論在那個路徑下都可以調用這個路徑下的文件,會不會是在我的電腦上有環境變量代碼調用了神秘路徑上的文件,而其他人電腦上沒有。於是在系統環境路徑下各種嘗試,發現刪除某一個環境變量時,exe程序在我的電腦上也出現了相同的錯誤代碼。
破案了,就是這個路徑里的文件!原來是安裝annaconda自動為我添加的環境變量。
在這里插入圖片描述
解決方法: 將紅框路徑下的dll文件全部復制到打包好的xxx.exe路徑中,移植沒有問題了!當然,這里面依賴的dll文件具體是哪幾個還需要試一試才知道,如果不嫌棄包太大了就全部復制。根據我的經驗,numpy是需要nkl_開頭的和libiomp5md.dll這些動態鏈接庫。
在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM