2024年Python最新Python pyinstaller打包exe最完整教程(1),Python模块化面向接口编程

做了那么多年开发,自学了很多门编程语言,我很明白学习资源对于学一门新语言的重要性,这些年也收藏了不少的Python干货,对我来说这些东西确实已经用不到了,但对于准备自学Python的人来说,或许它就是一个宝藏,可以给你省去很多的时间和精力。

别在网上瞎学了,我最近也做了一些资源的更新,只要你是我的粉丝,这期福利你都可拿走。

我先来介绍一下这些东西怎么用,文末抱走。


(1)Python所有方向的学习路线(新版)

这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

最近我才对这些路线做了一下新的更新,知识体系更全面了。

在这里插入图片描述

(2)Python学习视频

包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。

在这里插入图片描述

(3)100多个练手项目

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。

在这里插入图片描述

(4)200多本电子书

这些年我也收藏了很多电子书,大概200多本,有时候带实体书不方便的话,我就会去打开电子书看看,书籍可不一定比视频教程差,尤其是权威的技术书籍。

基本上主流的和经典的都有,这里我就不放图了,版权问题,个人看看是没有问题的。

(5)Python知识点汇总

知识点汇总有点像学习路线,但与学习路线不同的点就在于,知识点汇总更为细致,里面包含了对具体知识点的简单说明,而我们的学习路线则更为抽象和简单,只是为了方便大家只是某个领域你应该学习哪些技术栈。

在这里插入图片描述

(6)其他资料

还有其他的一些东西,比如说我自己出的Python入门图文类教程,没有电脑的时候用手机也可以学习知识,学会了理论之后再去敲代码实践验证,还有Python中文版的库资料、MySQL和HTML标签大全等等,这些都是可以送给粉丝们的东西。

在这里插入图片描述

这些都不是什么非常值钱的东西,但对于没有资源或者资源不是很好的学习者来说确实很不错,你要是用得到的话都可以直接抱走,关注过我的人都知道,这些都是可以拿到的。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='my_app_name',

)


这里面包含一些特殊的类,比如Analysis, PYZ, EXE等,文件夹模式下还多了一个COLLECT类。只有当pyinstaller运行时才会被定义,很显然你不能在python解释器中直接调用它们。 这些类的参数与pyinstaller的命令行参数并不一样。


接下来将针对Spec文件中的这些对象进行介绍


### 5.2 Analysis对象


Analysis类包含一些分析信息,它分析模块的导入以及一些依赖文件。


这个类的常用参数介绍如下。




|  |  |  |  |
| --- | --- | --- | --- |
| **参数名** | **默认值** | **描述** | **(常用参数)示例** |
| scripts | 必选参数,无默认值 | 需要分析的文件路径列表(一般就是需要打包的文件) | ["myscript.py"] |
| pathex | None | 需要额外进行分析模块导入的文件(夹)路径,包含命令行--path参数指定内容 | ["C:/Python310/Lib/site-packages", "C:/my\_module] |
| binaries | None | 需要嵌入的二进制文件列表,包含命令行--add-binary参数指定内容 |  |
| datas | None | 需要嵌入的非二进制文件(夹),包含命令行--add-data参数指定内容 | [("assets", "assets"), ("music/\*.mp3", "music")] |
| hiddenimport | None | 需要额外导入的模块列表 | ["module1", "module2"] |
| hookspath | None | 钩子文件路径列表(钩子文件用于配置一些模块特殊的导入,后文详解) |  |
| hooksconfig | None | 一个字典,包含钩子的配置信息 |  |
| excludes | None | 需要被忽略,不进行导入的模块列表 |  |
| runtime\_hooks | None | 运行时的钩子列表,指定为一系列文件名 |  |
| noarchive | False | 如果设为True,则不会将源代码放到一个存档中进行存储,而是作为多个单独的文件 |  |


在完成分析后,需要将一些属性传递给PYZ类。Analysis对象包含了以下属性,你可以不必了解它们:




|  |  |
| --- | --- |
| **属性名** | **描述** |
| scripts | 同参数中的scripts |
| pure | 需要一起打包的纯python模块 |
| pathex | 同参数中的pathex |
| binaries | 同参数中的binaries |
| datas | 同参数中的datas |


### 5.3 PYZ对象


完成分析后,将Analysis对象的一些属性传递给PYZ类。PYZ相当于一个压缩包,里面储存了所有的依赖文件。



pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)


### 5.4 EXE对象


定义PYZ对象后,接下来需要定义EXE对象,也就是可执行文件对象。


不同打包模式(单文件或文件夹)的EXE对象参数略有不同。其中常用参数如下:




|  |  |  |  |
| --- | --- | --- | --- |
| **参数** | **默认值** | **描述** | **(常用参数)示例** |
| console | True | 是否显示控制台,相当于命令行-w参数 |  |
| disable\_windowed\_traceback | False | 是否禁用异常提示,相当于命令行--disable-windowed-traceback参数 |  |
| name | None | 可执行文件的名称。在Windows上会自动添加".exe"后缀 | "my\_app\_name" |
| icon | None | 可执行文件的图标路径 | "icon.ico" |


### 5.5 COLLECT对象(仅-D文件夹模式)


使用文件夹模式打包时还会有一个COLLECT对象,该对象用于创建文件夹。它有一个常用的关键字参数name,表示文件夹的名称。


### 5.6 Bundle对象(仅macOS系统)


如果你要在macOS上创建应用程序,且你的应用程序是无控制台的,那么在exe构建完成之后还需要添加一些代码。



app = BUNDLE(exe,
name=‘my_app_name.app’,
icon=“icon.ico”,
bundle_identifier=None)


### 5.7 Splash对象


如果你想要在应用中添加启动画面(图片和文本都可以),需要在Spec文件中额外添加一个Splash对象进行控制。


在分析完代码后,创建Splash对象:



a = Analysis(…)

splash = Splash(‘splash.png’,
binaries=a.binaries,
datas=a.datas,
text_pos=(10, 50),
text_size=12,
text_color=‘black’)


然后在EXE中绑定splash对象。注意:单文件模式和文件夹模式方式略有不同。


以下是单文件模式绑定splash对象的方法。



splash = Splash(…)

exe = EXE(pyz,
a.scripts,
splash, # <-- both, splash target
splash.binaries, # <-- and splash binaries
…)


以下是文件夹模式的方法。



splash = Splash(…)

exe = EXE(pyz,
splash, # <-- splash target
a.scripts,
…)
coll = COLLECT(exe,
splash.binaries, # <-- splash binaries
…)


下面介绍Splash对象的一些参数。注意:由于Splash窗口基于Tcl/Tk(和python tkinter一样),所以里面有一些用法与Tcl/Tk(tkinter)的用法很像,但不重要。




|  |  |  |  |
| --- | --- | --- | --- |
| **参数** | **默认值** | **描述** | **(常用参数)示例** |
| image\_file | 必选参数,无默认值 | 图片文件路径,必须是PNG格式(如果你安装了pillow模块,可以用pillow模块支持的其他格式) | "splash.png" |
| binaries | 必选参数,无默认值 | Analysis对象的binaries属性 |  |
| datas | 必选参数,无默认值 | Analysis对象的datas属性 |  |
| text\_pos | None | 闪屏文本相对于闪屏图片的显示位置(是一个(x, y)元组,锚点为文本左下角)。如果不指定,则禁用文本显示 | (500, 400) |
| text\_size | 12 | 文本大小 |  |
| text\_font | "TkDefaultFont" | 文本使用的字体(必须是系统上安装了的字体),如果不指定则设为系统默认字体 | "宋体" |
| text\_color | "black" | 文本颜色,颜色格式可以是颜色名称字符串或者十六进制颜色字符串,如"#ff00ff"(注意:不支持(r, g, b)元组形式) |  |
| text\_default | "Initializing" | 默认显示的文本(后面可以用pyi\_splash.update\_text来更新显示的文本) | "加载中……" |
| max\_img\_size | (760, 480) | 最大闪屏图片尺寸。如果超出尺寸,那么闪屏图片将会被按纵横比缩放,容纳到该尺寸中。可以设为None不缩放 |  |
| always\_on\_top | True | 闪屏窗口是否置顶,如果置顶,其位于其他窗口之上 |  |
| rundir | "\_\_splash" | 设置运行闪屏时,用于存放一些相关文件的文件夹名称。使用这个参数主要是为了避免命名冲突,一般不会使用 |  |


下面就以一个示例来演示Splash的文本显示。使用的代码还是上一章节使用的。


在开头添加以下代码:



try:
import pyi_splash
import time

for i in range(100):
    text = f"加载中……进度{i}%"
    time.sleep(0.1) # 模拟一个速度比较慢的加载过程
    
    pyi_splash.update_text(text) # 更新显示的文本

pyi_splash.close() # 关闭闪屏

except ImportError:
pass


然后通过pyi-makespec生成对应的Spec文件:



pyi-makespec -w -F --add-data assets;assets --splash splash.png my_app_name.py


由于Splash的文本显示只能在Spec文件中进行配置,所以我们先打开my\_app\_name.spec,将Splash对象的代码进行修改,如下所示:



splash = Splash(
‘splash.png’,
binaries=a.binaries,
datas=a.datas,
text_pos=(30, 270),
text_size=12,
minify_script=True,
always_on_top=True,
)


然后进行打包:



pyinstaller my_app_name.spec


运行效果如下:


![](https://img-blog.csdnimg.cn/6b5ffceb3c8e46a9ada7c9452bed1a94.gif)


可以看到,首先显示文本被设定为加载的各个依赖文件,然后变成update\_text中自己设定的加载内容。


### 5.8 多包捆绑(打包多个exe)



> 
> 有些产品由几个不同的应用程序组成,每个应用程序可能依赖于一组通用的第三方库,或者以其他方式共享一部分代码。在打包这样的产品时,如果单独对待每个应用程序,将其与所有依赖项捆绑在一起,那就太可惜了,因为这意味着要存储代码和库的副本。
> 
> 
> 此时,我们可以使用多包特性来捆绑一组可执行应用程序,以便它们共享库的单个副本。我们可以在单文件或单文件夹应用程序中做到这一点。
> 
> 
> 


比如有两个应用都使用了tkinter模块,且这两个应用相关,需要在发布时放到一起(比如一个应用专门用于图片剪裁,另外一个专门用于图片滤镜,它们可能共用了部分功能)。如果分别打包,那么每个应用都会包含一个tkinter模块的依赖文件,而且都储存相同的内容,这就很浪费存储空间。如果用多包捆绑的话,只会有一个tkinter模块的依赖文件,两个应用都可以调用相同的依赖。


#### 文件夹模式的多包捆绑


如果采用文件夹模式,想要捆绑多个应用程序,那么只需要共享一个COLLECT对象。假如有hello1.py, hello2.py,将这两个应用进行捆绑,可以将它们的Spec文件进行一些组合。


首先通过pyi-makespec分别生成hello1.py, hello2.py的Spec文件。


然后将其中的Analysis, PYZ, EXE, Splash等对象分别以不同的变量名放入同一个Spec文件,然后将它们的COLLECT对象组合起来。



hello1_a = Analysis([‘hello1.py’], …)
hello1_pyz = PYZ(hello1_a.pure, hello1_a.zipped_data, …)
hello1_exe = EXE(hello1_pyz,
hello1_a.scripts,
…)

hello2_a = Analysis([‘hello2.py’], …)
hello2_pyz = PYZ(hello2_a.pure, hello2_a.zipped_data, …)
hello2_exe = EXE(hello2_pyz,
hello2_a.scripts,
…)

coll = COLLECT(hello1_exe,
hello1_a.binaries,
hello1_a.zipfiles,
hello1_a.datas,

           hello2_exe,
           hello2_a.binaries,
           hello2_a.zipfiles,
           hello2_a.datas,
           ...
           name='hello')

这样,将会生成同一个文件夹,该文件夹下包含两个文件hello1.exe, hello2.exe。 它们共享一部分的依赖文件。


#### 单文件模式的多包捆绑


单文件模式下,多包捆绑会生成多个单独的exe,其中一个exe包含它们共有的依赖文件。


比如打包hello1.py和hello2.py,设置hello1包含共有的依赖文件,最后生成hello1.exe, hello2.exe。生成的hello1.exe由于包含两个exe共有的依赖文件,其文件大小会大于hello2.exe。


运行hello1.exe时与单独打包效果相同。但是运行hello2.exe时,它会在hello1.exe中搜索它需要的依赖文件,速度会稍慢。


如果将hello2.exe移动到别的地方,或者将hello1.exe改名,那么hello2.exe将无法运行,因为它找不到hello1.exe,从而无法找到所需的依赖文件。


以下是hello1.py和hello2.py两个程序文件,将以它们为例进行打包。



hello1.py

while True:
input(“hello1”)

hello2.py

while True:
input(“hello2”)


首先通过pyi-makespec生成对应的Spec文件。完成后,将两个Spec文件的Analysis类汇总到一个文件中,并进行改名。



a1 = Analysis(
[‘hello1.py’],

)

a2 = Analysis(
[‘hello2.py’],

)


接下来在下面调用MERGE函数。这个函数会分析两个文件中重复的依赖项,将结果放到分析类的dependencies属性中。MERGE中位于第一个的程序将会包含共有的依赖项。



MERGE((a1, “hello1”, “hello1”), (a2, “hello2”, “hello2”))


然后将两个文件的ZIP和EXE进行汇总。汇总时需要额外向EXE类传递一个参数Analysis.dependencies。



pyz1 = PYZ(…)
exe1 = EXE(pyz1,

a1.dependencies, ####

a1.scripts, 
a1.binaries,
a1.zipfiles,
a1.datas, ...)

pyz2 = PYZ(…)
exe2 = EXE(
pyz2,

a2.dependencies, ####

a2.scripts,
a2.binaries,
a2.zipfiles,
a2.datas, ...)

 保存文件,然后通过pyinstaller打包。


![](https://img-blog.csdnimg.cn/c82c3b1184f34b80937d6b8ebc77bff2.png)


最后生成两个文件,可以看到hello1.exe的文件大小比hello2.exe大了很多,这是由于hello1.exe中包含了它们共有的依赖库。如果不使用多包捆绑,而是分别单独进行打包,那么两个文件的大小将都会超过5000KB。


## 6 钩子


有一些特殊的模块,它们存在一些特殊的依赖文件(比如ico, json等等)。而pyinstaller的导入分析无法检测到这些特殊的依赖文件,这就导致运行后出现问题。于是,pyinstaller引入了“钩子”。钩子文件其实就是一种python文件,后缀名为\*.py即可(和Spec文件的实质是一样的)。钩子文件中指定了某个特殊模块所需要的所有依赖文件。通过传递钩子文件,pyinstaller就能找到那些“隐藏”的依赖文件。


虽然钩子文件的作用也可以被--hiddenimport, --datas这些命令行参数替代,但是使用钩子显然更加方便。


pyinstaller有一些内置的“钩子”,提供了一些常用模块的钩子文件,它们包含Django, pickle, pyqt, scipy等等。


钩子文件的常用命名格式是:hook-module.py(其中module是模块名)。(当然你也可以按自己喜好命名)


### 6.1 钩子文件中的全局变量


钩子文件中可以包含以下全局变量(有一些变量可以不被写在文件中):




|  |  |  |
| --- | --- | --- |
| **属性** | **描述** | **(常用属性)示例** |
| hiddenimports | 需要额外导入的模块列表,相当于命令行--hidden-import参数 | ["sys", "pygame.mixer"] |
| excludedimports | 需要被排除,不被自动导入的模块列表(如果有一些模块在其他地方被导入,那么仍然会导入它) | ["tkinter"] |
| datas | 需要备添加的非二进制文件或文件夹,相当于命令行--add-data参数 | [('/usr/share/icons/education\_\*.png', 'assets') ] |
| binaries | 需要备添加的二进制文件,相当于命令行--add-binary参数 |  |


以下是一个钩子文件的示例:



hiddenimports = [“re”, “os”]
datas = [(“assets”, "assets)]


### 6.2 PyInstaller.utils.hooks


pyinstaller提供了一些方法用于钩子文件的制作。这些方法位于PyInstaller.utils.hooks模块。首先需要在钩子文件导入该模块。(注意pyinstaller的P和I是大写的,这是pyinstaller作为模块时的名称)



import PyInstaller.utils.hooks as hooks


下面介绍该模块中的常用函数。




---


**is\_module\_satisfies(requirements, version=None, version\_attr='\_\_version\_\_')**


检验模块版本是否达到requirements的要求,返回一个布尔值。关于requirements的相关格式,详见[PEP 440]( )。version\_attr参数指定该模块中版本属性的名称,默认是"\_\_version\_\_"。


下面是一些requirements的例子:



“pygame >= 2.2.1dev1” # 大于2.1.1dev1版本的pygame模块
“PIL == 2.9.*” # 版本以2.9.开头的PIL模块
“sphinx >= 1.3.1; sqlalchemy != 0.6” # 同时满足两个要求


**collect\_submodules(package, filter=<function <lambda>>, on\_error='warn once')**


返回一个模块的所有子模块。filter是一个筛选函数,接收模块名作为参数,返回一个布尔值表示是否要加入这个模块到返回值中。on\_error表示筛选出现异常时的处理,可以是:"raise"(抛出异常并停止pyinstaller构建),"warn"(只抛出警告,不停止pyinstaller构建),"warn once"(只警告一次,后续与之相同的警告被忽略),"ignore"(忽略,不抛出任何警告或异常)


例如:



收集Sphinx的所有子模块(名字中不包含test)

hiddenimports = collect_submodules(
“Sphinx”, filter=lambda name: ‘test’ not in name)


**collect\_data\_files(package, include\_py\_files=False, subdir=None, excludes=None, includes=None)**


返回一个模块使用的所有非二进制文件。include\_py\_files表示返回的文件列表中是否应该含有\*.py格式的文件。subdir是相对于要搜索的包的子目录。excludes, includes分别是需要被排除和被包含的文件列表,可以指定它们来判断是否要保留或移除某些格式的文件。


**collect\_dynamic\_libs(package, destdir=None, search\_patterns=['\*.dll', '\*.dylib', 'lib\*.so'])**


返回一个模块使用的所有二进制动态库文件。


**collect\_all(package\_name, include\_py\_files=True, filter\_submodules=None, exclude\_datas=None, include\_datas=None, on\_error='warn once')**


相当于上面的collect前缀的几个函数的综合。例如:



datas, binaries, hiddenimports = collect_all(‘my_module_name’)




---


使用hooks模块可以更加方便地制作钩子。


### 6.3 为自己的模块提供钩子


如果自己创建的模块需要钩子,那么可以自己定义一个文件,并储存到自己的模块中。


如果你有一个名为module\_name的模块文件夹,首先在自己模块的setup.cfg中(与setuptools模块相关,可自行搜索)添加如下代码(注意里面的module\_name):



[options.entry_points]
pyinstaller40 =
hook-dirs = module_name.__pyinstaller:get_hook_dirs
tests = module_name.__pyinstaller:get_PyInstaller_tests


然后在module\_name中添加名字为\_\_pyinstaller的文件夹(与上面hook-dirs和tests里面的命名相一致即可)。


最后可以在\_\_pyinstaller文件夹中添加hook文件。


## 7 反编译与加密


pyinstaller制作的应用,可能会被反编译(即根据生成的exe得到这个程序的源代码)。同时,也有一些方法来预防反编译,或者增加反编译的难度。


需要注意的是,反编译代码的结果大多数时候并不准确,只能得到大概的代码,可能需要后期处理。


### 7.1 通过pyinstxtractor进行反编译


pyinstxtractor是专门针对pyinstaller的反编译工具(也就是说,其他的打包工具,比如py2exe,cx\_Freeze打包的程序无法被这个工具反编译,需要通过别的反编译工具)。


#### 下载工具


首先通过以下链接下载pyinstxtractor: 


[PyInstaller Extractor download | SourceForge.net]( )


也可以通过github下载(推荐上面的方法,毕竟github访问较慢):


[GitHub - extremecoders-re/pyinstxtractor: PyInstaller Extractor]( )


下载完成后,得到pyinstxtractor.py。


还需要下载pycdc,链接如下:


[pycdc.exe · Python-ZZY - Gitee.com]( )


想要反编译一个pyinstaller打包的应用,流程是这样的:


1. 先用pyinstxtractor将\*.exe文件反编译成\*.pyc文件
2. 用十六进制编辑器修改\*.pyc文件中的magic number
3. 使用pycdc工具将\*.pyc转换为最终的\*.py文件


下面还是以这个程序为例作为演示:



‘’’
一个简单的应用
‘’’

import tkinter as tk # 导入tkinter

root = tk.Tk() # 创建窗口
root.title(“我的应用程序”) # 更改标题

image = tk.PhotoImage(file=“assets/image.gif”)
label = tk.Label(root, text=“你好,用户!”, image=image, compound=“top”)
label.pack() # 显示图片

root.mainloop() # 保持窗口运行


为了方便演示,采用单文件模式进行打包:pyinstaller -F my\_app\_name.py。打包完成后,将assets文件夹放到exe所在文件夹中。


#### 反编译exe


下面进入反编译环节。进入exe的文件夹,将下载的pyinstxtractor.py放到\*.exe所在文件夹下。


![](https://img-blog.csdnimg.cn/direct/28ae00c89c964d54b65d1a1857ee3bc5.png)​


在exe所在文件夹启动cmd,并输入以下命令:




python pyinstxtractor.py my_app_name.exe




![](https://img-blog.csdnimg.cn/direct/49ae9f30118840f1b376eba83e1aa46b.png)​


运行完成后,可以看到生成了一个xxxx.exe\_extracted的文件夹


![](https://img-blog.csdnimg.cn/direct/cdf7c35b70c84143adad7bc5d8b6eb3d.png)​


进入此文件夹,可以找到一个文件名和应用名称相同,但是没有后缀的文件,这就是得到的\*.pyc文件(虽然生成的时候没有后缀,不过这并不妨碍它本身的文件类型)。xxxx.exe\_extracted文件夹中的其他文件则是一些依赖程序文件,等等。


![](https://img-blog.csdnimg.cn/direct/f026fb09b5b44012b88090110f4c131c.png)​


#### 添加magic number


接下来一步很关键,需要在my\_app\_name这个文件中添加magic number,也就是一些python版本相关的信息。这里需要使用十六进制编辑器(有很多,不一一介绍了,sublimetext就可以用来编辑) 


magic number前面一部分与python版本有关,可以通过下面的代码查看当前python版本所对应的magic number的十六进制形式:



import importlib
print(importlib.util.MAGIC_NUMBER.hex())


 ![](https://img-blog.csdnimg.cn/direct/7c2864037505418bbdf22b00f1299353.png)


如果不知道编写这个应用所使用的python版本,可能要受到一点阻碍,网上有各python版本对应的magic number表,有需要可自行搜索。


在十六进制编辑器中打开my\_app\_name的pyc文件。


![](https://img-blog.csdnimg.cn/direct/e19c73fa9404425f8e14b8ea37856307.png)


然后将上面代码得到的magic number添加到此文件的最前面(表示版本信息,很重要)。


![](https://img-blog.csdnimg.cn/direct/2ac3e4ede65845288e310376b275a696.png)


然后再补充24个0(这些东西代表时间、代码大小等信息,没什么用,可以全部用0填充)


![](https://img-blog.csdnimg.cn/direct/19236f5611a745a786e2be90bcc1cfce.png)


最后保存文件。


如果不进行这一步,那么下一步反编译pyc时将会报错,提示magic number有误。


#### 反编译pyc


将pycdc.exe和my\_app\_name的pyc文件放到同一文件夹下。


![](https://img-blog.csdnimg.cn/direct/fa3cabe0897e48279f38879ed5aa4f5c.png)


在当前文件夹下启动cmd,输入:



pycdc my_app_name>final_my_app_name.py


当前文件夹下生成了final\_my\_app\_name.py,这就是反编译的结果。事实上,结果并不完美,存在很多错误,需要后期进行调整。


 ![](https://img-blog.csdnimg.cn/direct/867d931ab445499583a21206c69d0d9c.png)


除了pycdc,常用于反编译pyc文件的还有uncompyle6,但是目前(截至2023.12.17)不支持python3.9以上的版本。还有一个在线反编译pyc工具(有限制):[python反编译 - 在线工具]( )


#### 反编译依赖库


以上的方法用于反编译主文件exe,如果想要反编译这个应用依赖的python模块,可以进入xxxx.exe\_extracted文件夹下的PYZ-00.pyz\_extracted,里面包含了这个应用所需的依赖模块的pyc文件。按照上一节的方法即可进行反编译。


### 7.2 编译为pyd文件以防止反编译


在打包前将一些依赖的\*.py文件编译成\*.pyd文件,可以大大增加反编译的难度。\*.pyd是动态链接库,它可以像python模块一样调用但不能直接运行。使用\*.pyd不仅可以增加反编译难度,还能提升代码速度。


作者根据[python源码打包成exe、exe反编译、pyd加密防止反编译\_unknown magic number 227 in-CSDN博客]( )


的方法进行了尝试但是效果并不好,以下仅给出方法。


#### 调整代码


在开始之前,我们需要先对代码进行调整。\*.pyd文件只能被导入但不能直接运行,所以主程序不能进行pyd编译。所以,这样做以后依赖文件不会被反编译,但主程序还是会被反编译。我们可以进行一些改变,在主文件中留下那些不重要的代码,让反编译者看不到什么宝贵的信息,将重要的程序内容放到一个模块中,在主文件中只进行调用。


在my\_app\_name.py文件夹下新建一个\*.py文件(命名是随意的,这里把它命名为module.py),在这个module.py文件中写入原本是my\_app\_name.py中的内容,不过做了一些改变,加了一个main函数用于运行。



import tkinter as tk # 导入tkinter

def main():
root = tk.Tk() # 创建窗口
root.title(“我的应用程序”) # 更改标题

image = tk.PhotoImage(file="assets/image.gif")
label = tk.Label(root, text="你好,用户!", image=image, compound="top")
label.pack() # 显示图片

root.mainloop() # 保持窗口运行

再来修改my\_name\_app.py。这个入口程序直接从module.py(编译后就变成module.pyd了)中导入main函数,然后运行。



‘’’
一个简单的应用
‘’’

from module import * # 这里不能写成from module import main,原因见8.1

if name == “main”:
main()


这样一来,反编译后也只能看到几行与main相关的内容,根本无法获取有用的代码信息。 



import tkinter
from module import main

if name == “main”:
main()


#### 下载工具


先用pip下载Cython:



pip install Cython


此外还需要配置Visual Studio的C++开发工具。


先在下方链接下载Visual Studio:


[Microsoft C++ 生成工具 - Visual Studio]( )


安装生成工具时,勾选“使用C++的桌面开发”并点击右下方安装。如果你已经安装了生成工具但没有勾选这一项,可以启动安装包后,点击“修改”按钮进行更改。


![](https://img-blog.csdnimg.cn/direct/f367983d6dd24cd4a1cf0b078a9d791a.png)


![](https://img-blog.csdnimg.cn/direct/85cf5221da3e49a48d084b931ace25e0.png)


然后等待安装完成。


#### 编译成pyd


在my\_app\_name.py的文件夹下新建setup.py,并输入以下代码:



from distutils.core import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize([“module.py”, ]))


用ext\_modules参数指定需要进行编译的文件。如果还有别的python文件需要编译,可以在列表中修改。


保存文件后,启动cmd,运行以下命令:



python setup.py build_ext --inplace


完成编译后,文件夹中会生成一些新的文件,从中找出与模块名同名的\*.pyd文件,其他没用的文件可以删掉。


![](https://img-blog.csdnimg.cn/direct/a3a297fb35c0453085acbd05d5093b30.png)


这个文件名中有一些诸如cp310-win...的东西,表示这个python版本、系统等信息,需要手动将它们删除,只保留模块名和后缀名。


![](https://img-blog.csdnimg.cn/direct/8c0a1fb3797a486394ba903da975ee7a.png)


 生成pyd文件后,无需管原来的文件module.py,因为python导入时会优先选择pyd后缀的模块。


如果输入setup的命令后,出现了一些报错信息,提示需要Microsoft Visual C++,则代表上一步没有正确完成。


#### 进行exe打包


完成以上步骤后,可以通过pyinstaller进行打包了。


## 8 注意事项与常见问题


### 注意事项:导入


作者似乎发现,在导入python模块时,如果使用from xxx import func的形式,那么pyinstaller只会把导入的func包含到打包的应用里面,而不是整个xxx模块。并且这个过程中,pyinstaller不会管xxx模块中还依赖于哪些模块,这就可能导致func函数根本无法运行。


比如我有同一目录下的main.py和module.py:



‘’‘module.py’‘’
import tkinter

def main():
tkinter.Tk()
tkinter.mainloop()



‘’‘main.py’‘’
from module import main

if name == “main”:
main()


如果用pyinstaller对main.py进行编译,那么最后就会因为找不到tkinter模块而闪退。这是因为main.py中只导入了module.py中的main函数,pyinstaller会进行优化,只打包def main()函数的这一部分,而不会打包module.py中的其他内容(包括import tkinter这一重要的一句)


直接用python运行main.py时就不会出现以上问题。因为python解释from xxx import func这一句时,还是会先把xxx模块运行一次,但是只保留了一个func的变量名。


这里提供三种解决方案:


* 换一种导入方式,使用import xxx或from xxx import \*(推荐)
* 在主程序中把模块所需的依赖全导进来(在上面的示例中,就是main.py中添加import tkinter这一句)
* 打包时指定--hidden-import等参数(不推荐)


### 编译时报错:不是可运行的命令或程序


首先检查pyinstaller是否被成功安装。在cmd输入pip list,看安装列表中是否存在pyinstaller,如果没有则重新安装,根据安装信息进行处理。


如果显示未找到pyinstaller,则应用绝对路径指定pyinstaller。首先进入所在的文件夹,然后复制路径。打包时,将pyinstaller替换为pyinstaller.exe的绝对路径。(pyi-makespec找不到同理)


* Windows: Python目录下的Scripts文件夹
* GNU/Linux: /usr/bin
* macOS (using the default Apple-supplied Python) /usr/bin
* macOS (using Python installed by homebrew) /usr/local/bin
* macOS (using Python installed by macports) /opt/local/bin


例如,你想要执行pyinstaller myscript.py,但是提示找不到pyinstaller.exe。在你的电脑上,pyinstaller.exe安装在了C:\Python\Python310\Scripts这个位置,那么执行:




C:\Python\Python310\Scripts\pyinstaller.exe myscript.py



还有一种解决方法,将pyinstaller.exe所在的文件夹添加到系统环境变量(推荐)。Windows上添加环境变量办法如下:


右击“此电脑” -> 单击“属性” -> 单击“高级系统设置” -> 单击“环境变量” -> 在用户变量的位置单击“Path” -> 单击“编辑” -> 在“编辑环境变量”的窗口单击“新建” -> 写入pyinstaller.exe的所在路径 -> 一路“确定”进行保存。


### 运行后报错:找不到某些模块或文件


找不到某些模块,需要修改命令行参数--hidden-import,加入找不到的模块。


如果提示找不到sys, os两个模块,那么命令行参数修改为:




pyinstaller --hidden-import “sys” --hidden-import “os” …



如果使用Spec打包,则应在Analysis类的hiddenimport参数的列表中添加找不到的模块。


如果只是找不到某些模块中的部分文件,则需要为该模块添加钩子,或者将这些文件传递到命令行参数--add-data, --add-binary中。


### 运行后报错:失去标准输入




RuntimeError: input(): lost sys.stdin



某些程序打包后出现以上报错内容。这是由于代码中使用了input这样的函数让用户进行输入,但是打包时却设置了隐藏控制台。于是,运行打包后的应用后就没有一个控制台让用户进行输入,就会报错。(失去stdout并不会报错,但是print内容不会显示)


### 运行时闪退


闪退是由于程序中出现了致命的异常,可能由多种原因导致,需要根据引发的异常进行处理。这里提供一种方法来看到造成闪退的报错信息。


在当前文件夹下启动cmd(方法:将文件资源管理器窗口上方的文件路径修改为"cmd",然后回车),然后运行:



my_app_name.exe


此方法通过命令行运行这个应用。如果程序闪退,命令行窗口不会关闭,上面就留下了报错信息。


如果命令行可以正常运行,但是直接运行exe会闪退,考虑下面两种情况:



现在能在网上找到很多很多的学习资源,有免费的也有收费的,当我拿到1套比较全的学习资源之前,我并没着急去看第1节,我而是去审视这套资源是否值得学习,有时候也会去问一些学长的意见,如果可以之后,我会对这套学习资源做1个学习计划,我的学习计划主要包括规划图和学习进度表。



分享给大家这份我薅到的免费视频资料,质量还不错,大家可以跟着学习

![](https://img-blog.csdnimg.cn/img_convert/21b2604bd33c4b6713f686ddd3fe5aff.png)



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618317507)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值