|
1简介python提供了多种方法用于将普通的*.py程序文件编译成exe文件(有时这里的“编译”也称作“打包”)。exe文件即可执行文件,打包后的*.exe应用不用依赖python环境,可以在他人的电脑上运行。pyinstaller是一个第三方模块,专用于python程序的exe打包。此外python还有一些别的方法进行打包,但是pyinstaller打包最强大而且好用。pyinstaller的官网是:https://pyinstaller.org/ 2安装可以通过pip进行安装。首先启动cmd,输入以下内容后回车:pipinstallpyinstaller安装完成后,验证是否成功安装:pyinstaller--version如果显示找不到“pyinstaller”,请转到最后一章“常见问题”3原理和打包效果3.1原理概述在开始打包前,读者有必要先了解pyinstaller的打包原理。如果你只在乎打包结果而不在乎细节,你可以跳过第3章,直接进入下面的打包环节。但是,当你打包时遇到问题时,还是建议你先把打包原理看完,可能你的问题会得到解决。pyinstaller先读取你需要打包的python文件,然后搜索其中使用的模块,然后将所需的模块以及Python解释器放到一起,并通过一些操作构建exe,最终形成你的应用程序。3.2搜索模块当然,在搜索模块的时候必然会遇到一些问题。pyinstaller只会搜索import语句,然后根据import得到的模块再进行搜索。如果编程者使用了一些特殊的导入方式,比如使用__import__()函数,使用importlib里面的导入函数,那么pyinstaller很可能找不到你所需要的模块。这时,你可以通过参数来指定你所需要的模块,也可以使用“钩子”等等(这是后话)。3.3打包效果概述pyinstaller打包后会形成一个文件夹或单个的exe(可以用参数指定)。但不论是哪一种情况,都会包含一个exe文件,用户可以双击它运行该应用程序。假如你要打包myscript.py,那么打包完成后运行这个myscript.exe,效果就是运行myscript.py后的效果。默认情况下,打包会形成一个黑色的控制台(cmd的样子),也可以设置隐藏这个控制台。这个控制台用于为python提供标准输入(stdin),标准输出(stdout),标准错误(stderr)。也就是说,这个控制台上显示了print函数的输出,用于接收input函数的输入,还会输出python的异常。如果你隐藏了这个控制台,程序中的print就无法显示(但是不会报错),报错信息也无法被用户直接看到(pyinstaller有一些选项来控制显示异常,后文详解);需要注意的是,此时不能使用input,否则会报错:RuntimeError:input():lostsys.stdinpython文件有一种后缀名*.pyw,这样的程序执行时默认会隐藏控制台。如果将文件后缀命名为pyw,那么pyinstaller也会认为它隐藏了控制台,不需要通过额外的选项来指定。当你制作GUI程序的时候,最好选择隐藏控制台,来提升用户体验。 打包后的文件可能会被反编译(即通过exe文件得到原来的代码),可以通过一些方法进行加密(后文详解)。3.4打包成单个文件夹下面介绍一下打包完成后形成的文件夹。这个文件夹的名字是你提供的,一般是你要求打包的python文件的名称。文件夹中包含一个exe文件,以及其他一些依赖文件(比如一些dll文件,可能还有你的应用所需要的图片等素材)。你只需要将该文件夹压缩就能发给别人运行了。当你运行里面的exe文件后,pyinstaller其实只是启动了解释器,然后通过解释器运行你的主程序。优点打包成单个文件夹的形式便于调试,因为你可以清楚地看到pyinstaller将哪些模块文件放到了文件夹中。当你更改代码,需要用户更新应用时,只需要让用户对于部分内容进行修改。如果你只修改了主程序,没有使用多余的模块,那么就只需要让用户替换里面的exe文件,而不用全部替换(因为更新前后使用的模块是一致的,它们都以多文件的形式放到了文件夹中)。单个文件夹的状态下,程序的启动速度和打包前差不多。缺点打包成单个的文件夹后,文件大小可能会更大一些,因为大部分依赖文件没有进行压缩。3.5打包成单个exe单个exe模式下,pyinstaller只会生成一个单独的exe文件,所有的依赖文件都会被压缩到exe文件中。和上面的文件夹模式类似,exe启动后,pyinstaller也是通过调用python解释器来运行主程序的。优点启动单个exe非常简单,用户只需要点击exe文件就能运行,而无需在一大堆的依赖文件中找到exe文件。并且在经过压缩后,这个exe文件的文件大小会大大减小。缺点单个exe的启动速度比较慢(通常会慢几秒,且只是启动时的速度,不是运行后的速度),这是因为pyinstaller会在这一段时间中将一些依赖文件写入到一个临时的文件夹(后文介绍该文件夹的调用方式)。如果你希望添加一些附带文件(比如使用说明README),你还需要额外新建文件夹并将其放进去。4打包在了解相关原理后,下面正式进入打包环节。本章介绍通过命令行参数进行打包,这种方式比较初级,适用于一般的打包方式。4.1基本语法打包需要通过cmd进行,语法和大多数工具一样。pyinstaller最简单的打包方式是:pyinstallermyscript.py其中myscript.py是你想要打包的程序。如果这一步提示找不到myscript.py,请检查路径是否正确;如这一步提示找不到pyinstaller工具,请参考最后一章“常见问题”。如果直接传递文件名,pyinstaller会生成一个spec文件将一些打包参数放到里面,然后进行打包。打包完成后,你会在你的目录下找到一个dist文件夹,里面存储了打包后的结果。pyinstaller还会生成一个build文件夹并写入一些日志信息。当然,你也可以自己构建一个*.spec文件(后文介绍),然后交给pyinstaller进行处理。4.2参数总览本节只是列举并简要介绍常用的参数,并不过多展开,将在下面的部分对于一些重点参数举例介绍。如有不熟悉命令行参数和命令行使用的读者可自行搜索,或者参考下面的介绍:pyinstaller-D-i"icon.ico"myscript.py调用命令时,首先给出工具名称(比如上面的pyinstaller),然后提供相关参数,有一些参数是可选的但不需要附带任何值(比如上面的-D),有一些参数是必选的(比如上面的myscript.py),有一些参数需要附带一个值(比如上面的-i"icon.ico")。其中有一些参数可以简写(比如-i就是--icon的简写)。位置参数位置参数在打包时放在最后,而且无需指定关键字。pyinstaller的位置参数是需要打包的文件路径,或是spec文件路径。可选参数下面是比较有用的参数,读者可以自行了解,也可以跳过这部分,打包时用于参考:参数名描述-D文件夹模式。在打包完成后生成一个文件夹,其中包含一个exe文件和一个包含若干依赖文件的文件夹(详见上文)。(默认)-F单文件模式。在打包完成后只会生成一个单独的exe文件(详见上文)。--add-data 指定一个文件夹或文件(非二进制),将其嵌入到exe中。--add-binary 和--add-data类似,不过指定的文件夹或文件是二进制的-p DIR--paths DIR提供一个路径进行搜索并且导入里面的模块(不同的路径使用路径分隔符os.pathsep分隔开,或者多次使用这个参数)。这可以解决有时候第三方模块找不到的问题。--hidden-importMODULENAME--hiddenimportMODULENAME需要进行额外导入的模块。当pyinstaller在程序中找不到一些模块时,需要你额外指定。这个参数可以多次使用,可以解决一些模块找不到的问题。--splash IMAGE_FILE添加一个启动画面(图片文件)路径,在程序运行前显示指定的启动图片,起到加载提示的效果。-c,--console,--nowindowed打包程序运行后出现一个黑色的控制台窗口(默认)-w,--windowed,--noconsole打包程序运行后隐藏控制台窗口-i --icon 设置打包后exe程序的图标(只能在Windows和macOS上使用)--disable-windowed-traceback禁用异常提示(只能在Windows和macOS上使用)--help,-h打印pyinstaller的帮助信息并退出4.3 单文件和文件夹模式打包/隐藏控制台窗口下面是一个程序示例,将创建一个窗口并显示一张图片image.gif和一段提示。读者无需了解其代码细节。接下来将以这个程序为例进行一个简单的打包示范。'''一个简单的应用'''importtkinterastk#导入tkinterroot=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_app-assets-image.gif-my_app_name.py由于这个应用不需要进行print和input这样的控制台类操作,所以我选择隐藏控制台。打开cmd并进入程序文件所在的文件夹my_app,打包时添加-w参数:pyinstaller-wmy_app_name.py接下来会出现若干个INFO提示,如果没有错误,那么打包就成功了。完成打包后,生成了build和dist文件夹,以及一个spec文件;dist文件夹包含打包的结果,build文件夹中是一些日志信息,spec文件里面是用于打包的配置信息。接下来是重要的一步。由于打包时没有绑定任何的资源文件,所以此时运行时会报错,提示找不到image.gif。此时,应该把程序文件夹下的assets文件夹(参见上方的文件夹层级)复制到dist文件夹中的程序文件夹,和exe文件位于同一位置。接下来,再试一下单文件模式的打包,只需添加-F参数:pyinstaller-w-Fmy_app_name.py打包后,生成了一个单个的my_app_name.exe,而没有其他文件。同样也需要将assets文件夹复制到与该exe文件的同一位置。 4.4资源嵌入exe经常需要复制文件夹不仅麻烦,而且还无法防止里面的内容被用户修改。此时,我们可以使用pyinstaller的--add-data参数,将assets文件夹里面的资源嵌入到exe文件中。资源嵌入exe只在单文件模式下使用。文件夹模式下,资源文件夹不会嵌入到exe中,但是会被复制到exe所在的文件夹。使用资源嵌入后,资源文件夹的路径发生了变化,我们不能使用一般的相对路径来调用assets这样的内嵌资源文件夹。前面已经讲过,pyinstaller单文件模式下的exe启动后,会将嵌入的资源文件放到一个临时的文件夹中,这个文件夹的名字不是固定的,叫做_MEIxxxxx,其中xxxxx是随机数。这个文件夹的路径在打包后会被放到sys._MEIPASS这个变量里面,只需要调用sys._MEIPASS就可以获得这个路径文件夹。于是,我们通过以下函数返回正确的路径:defget_path(relative_path):try:base_path=sys._MEIPASS#pyinstaller打包后的路径exceptAttributeError:base_path=os.path.abspath(".")#当前工作目录的路径returnos.path.normpath(os.path.join(base_path,relative_path))#返回实际路径'运行运行这个函数通过一个相对的路径返回实际的绝对路径。需要注意:sys._MEIPASS这个属性只有在打包成exe后才被创建,以py代码执行的时候这个属性是不存在的,所以要通过try...except...代码块捕获异常。如果不是pyinstaller模式,那么就使用py文件所在的文件夹的路径作为基本路径。我们不必担心这个函数的工作原理(虽然者不难理解),这个函数可以直接拿来用(是一位叫做davidpendergast的大佬写的)。于是,我们将代码改成这样(省略了部分内容):...importsysimportosdefget_path(relative_path):try:base_path=sys._MEIPASSexceptAttributeError:base_path=os.path.abspath(".")returnos.path.normpath(os.path.join(base_path,relative_path))...image=tk.PhotoImage(file=get_path("assets/image.gif"))...接下来进行打包:pyinstaller-w-F--add-dataassets;assetsmy_app_name.py打包完成后会生成一个包含嵌入资源的单独的exe,无需将资源文件放到同一文件夹下也能正常运行。--add-data的参数由源文件名src和目标文件名dest组成。路径的源文件名和目标文件名用文件分隔符进行分隔,源文件名是该文件或文件夹的原本的路径,目标文件名是该文件夹嵌入到exe后的放入的文件夹名。文件分隔符:在Windows系统上是分号,大部分unix系统上是冒号,可以通过os.pathsep来查看当前系统上的文件分隔符。例如:>>>importos>>>os.pathsep';'比如--add-data"assets;assets"就表示将原本assets里面的所有文件,放入打包后的assets文件夹。再比如--add-data"assets/*.mp3;music"表示将原本assets里面的所有mp3文件,放入打包后的music文件夹。 4.5更改图标打包完成后,默认的程序图标是一个“蛇”形,但我们也可以进行更改。(根据官方文档,该功能只能在Windows和macOS上使用)--icon或-i参数用于设置图标,该参数的值默认为"NONE",表示使用默认的图标;也可以指定为一个*.ico格式的Windows图标文件路径;*.icns的Mac图标文件路径;或者一个其他图片文件(需安装pillow模块,会通过pillow模块将其转换成标准的ico/icns格式)。首先添加一个图标文件。图标文件在Windows上格式为*.ico,Mac上是*.icns。-my_app-assets-image.gif-my_app_name.py-icon.ico这个图标文件其实放在哪里都可以,因为打包完成后其实它也相当于嵌入了exe。但为了方便,还是把它放到同一文件夹下比较好。pyinstaller-iicon.icomy_app_name.py为了方便看,之前设置的-w,-F这些选项都省略了。最后生成了一个图标与icon.ico相一致的exe。4.6启动画面(闪屏)pyinstaller单文件模式启动速度较慢,所以可能需要一个启动画面(闪屏)进行过渡,提示用户正在进行加载。这个启动画面可以是单张图片,也可以是文本(默认情况下文本禁用,使用方式参见第5章)。这个启动画面的实现基于Tcl/Tk(和pythontkinter模块一样),打包时会附带约1.5MB的额外文件来支持这个功能。支持闪屏,需要先准备一张图片,必须是PNG格式(如果你安装了pillow模块,可以用pillow模块支持的其他格式)。然后,在打包时加上--splash参数,并传入图片路径。pyinstaller--splashsplash.pngmy_app_name.py控制闪屏可以通过pyi_splash模块,这个模块和上一节的sys._MEIPASS属性一样,在没有通过pyinstaller打包成exe后是不起作用的,所以必须带上try...except...代码。pyi_splash.close()方法用于关闭闪屏。一般放在程序开头即可,因为只要运行到程序开头,说明pyinstaller的加载就基本完成了。于是,在程序开头部分添加以下代码:try:importpyi_splashpyi_splash.close()exceptImportError:pass'运行运行如果不用这段代码进行关闭,那么闪屏将一直显示。打包后,闪屏效果如下。至于pyi_splash还有一个update_text方法,用于在闪屏画面上显示加载文本,将在5.7节介绍。 4.7禁用异常提示--disable-windowed-traceback参数用于禁用异常提示。如果不添加这个参数,将会在非控制台程序出错(似乎仅限非致命的错误)时弹出一个窗口报告异常信息(注意:仅在隐藏控制台模式下弹出异常报告窗口)。为了测试,我在代码第一行添加了raiseException,运行打包后的exe后效果如图所示。5使用Spec文件当你调用以上的打包方式时,会在脚本的文件夹下生成一个*.spec文件。*.spec文件包含了打包需要使用的所有配置信息。直接在命令行中将*.spec文件路径传给pyinstaller,也可以进行打包。比如:pyinstallermy_app_name.spec(其中my_app_name.spec是根据my_app_name.py生成的Spec文件) 这样,当你多次打包同一个项目时,就无需每次都传入那么多参数,只需要传入*.spec文件的路径即可。*.spec文件也比较好处理,直接使用python编辑器或记事本就能编辑。5.1生成Spec文件使用pyi-makespec工具可以根据pyinstaller的命令行参数生成Spec文件。用法很简单,在原先使用pyinstaller的打包命令中,把"pyinstaller"换成"pyi-makespec"就可以生成一个Spec文件。例如:pyi-makespec-w-F--add-dataassets;assetsmy_app_name.py要更改Spec文件的生成路径,可以指定参数--specpath。如果报错提示找不到pyi-makespec,转到最后一章:常见问题。当你使用*.spec文件进行pyinstaller打包时,大部分的打包参数都不可用,需要预先在*.spec文件中预先设定。pyinstaller会将*.spec里面的内容当做代码执行。单文件模式和文件夹模式的*.spec文件略有不同。下面是一个*.spec文件(单文件模式打包)的例子。#-*-mode:python;coding:utf-8-*-block_cipher=Nonea=Analysis(['my_app_name.py'],pathex=[],binaries=[],datas=[('assets','assets')],hiddenimports=[],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='my_app_name',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,)下面是一个文件夹模式的*.spec文件的例子:#-*-mode:python;coding:utf-8-*-block_cipher=Nonea=Analysis(['my_app_name.py'],pathex=[],binaries=[],datas=[('assets','assets')],hiddenimports=[],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,[],exclude_binaries=True,name='my_app_name',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=False,disable_windowed_traceback=False,argv_emulation=False,target_arch=None,codesign_identity=None,entitlements_file=None,)coll=COLLECT(exe,a.binaries,a.zipfiles,a.datas,strip=False,upx=True,upx_exclude=[],name='my_app_name',)这里面包含一些特殊的类,比如Analysis,PYZ,EXE等,文件夹模式下还多了一个COLLECT类。只有当pyinstaller运行时才会被定义,很显然你不能在python解释器中直接调用它们。 这些类的参数与pyinstaller的命令行参数并不一样。接下来将针对Spec文件中的这些对象进行介绍5.2Analysis对象Analysis类包含一些分析信息,它分析模块的导入以及一些依赖文件。这个类的常用参数介绍如下。参数名默认值描述(常用参数)示例scripts必选参数,无默认值需要分析的文件路径列表(一般就是需要打包的文件)["myscript.py"]pathexNone需要额外进行分析模块导入的文件(夹)路径,包含命令行--path参数指定内容["C:/Python310/Lib/site-packages","C:/my_module]binariesNone需要嵌入的二进制文件列表,包含命令行--add-binary参数指定内容datasNone需要嵌入的非二进制文件(夹),包含命令行--add-data参数指定内容[("assets","assets"),("music/*.mp3","music")]hiddenimportNone需要额外导入的模块列表["module1","module2"]hookspathNone钩子文件路径列表(钩子文件用于配置一些模块特殊的导入,后文详解)hooksconfigNone一个字典,包含钩子的配置信息excludesNone需要被忽略,不进行导入的模块列表runtime_hooksNone运行时的钩子列表,指定为一系列文件名noarchiveFalse如果设为True,则不会将源代码放到一个存档中进行存储,而是作为多个单独的文件在完成分析后,需要将一些属性传递给PYZ类。Analysis对象包含了以下属性,你可以不必了解它们:属性名描述scripts同参数中的scriptspure需要一起打包的纯python模块pathex同参数中的pathexbinaries同参数中的binariesdatas同参数中的datas5.3PYZ对象完成分析后,将Analysis对象的一些属性传递给PYZ类。PYZ相当于一个压缩包,里面储存了所有的依赖文件。pyz=PYZ(a.pure,a.zipped_data,cipher=block_cipher)5.4EXE对象定义PYZ对象后,接下来需要定义EXE对象,也就是可执行文件对象。不同打包模式(单文件或文件夹)的EXE对象参数略有不同。其中常用参数如下:参数默认值描述(常用参数)示例consoleTrue是否显示控制台,相当于命令行-w参数disable_windowed_tracebackFalse是否禁用异常提示,相当于命令行--disable-windowed-traceback参数nameNone可执行文件的名称。在Windows上会自动添加".exe"后缀"my_app_name"iconNone可执行文件的图标路径"icon.ico"5.5COLLECT对象(仅-D文件夹模式)使用文件夹模式打包时还会有一个COLLECT对象,该对象用于创建文件夹。它有一个常用的关键字参数name,表示文件夹的名称。5.6Bundle对象(仅macOS系统)如果你要在macOS上创建应用程序,且你的应用程序是无控制台的,那么在exe构建完成之后还需要添加一些代码。app=BUNDLE(exe,name='my_app_name.app',icon="icon.ico",bundle_identifier=None)5.7Splash对象如果你想要在应用中添加启动画面(图片和文本都可以),需要在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,#=2.2.1dev1"#大于2.1.1dev1版本的pygame模块"PIL==2.9.*"#版本以2.9.开头的PIL模块"sphinx>=1.3.1;sqlalchemy!=0.6"#同时满足两个要求'运行运行collect_submodules(package,filter=>,on_error='warnonce')返回一个模块的所有子模块。filter是一个筛选函数,接收模块名作为参数,返回一个布尔值表示是否要加入这个模块到返回值中。on_error表示筛选出现异常时的处理,可以是:"raise"(抛出异常并停止pyinstaller构建),"warn"(只抛出警告,不停止pyinstaller构建),"warnonce"(只警告一次,后续与之相同的警告被忽略),"ignore"(忽略,不抛出任何警告或异常)例如:#收集Sphinx的所有子模块(名字中不包含test)hiddenimports=collect_submodules("Sphinx",filter=lambdaname:'test'notinname)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='warnonce')相当于上面的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_dirstests=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: yInstallerExtractordownload|SourceForge.net也可以通过github下载(推荐上面的方法,毕竟github访问较慢):GitHub-extremecoders-re/pyinstxtractoryInstallerExtractor下载完成后,得到pyinstxtractor.py。还需要下载pycdc,链接如下:pyinstaller/pycdc.exe·Python-ZZY/CSDN-articles-Gitee.com想要反编译一个pyinstaller打包的应用,流程是这样的:先用pyinstxtractor将*.exe文件反编译成*.pyc文件用十六进制编辑器修改*.pyc文件中的magicnumber使用pycdc工具将*.pyc转换为最终的*.py文件下面还是以这个程序为例作为演示:'''一个简单的应用'''importtkinterastk#导入tkinterroot=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-Fmy_app_name.py。打包完成后,将assets文件夹放到exe所在文件夹中。反编译exe下面进入反编译环节。进入exe的文件夹,将下载的pyinstxtractor.py放到*.exe所在文件夹下。在exe所在文件夹启动cmd,并输入以下命令:pythonpyinstxtractor.pymy_app_name.exe运行完成后,可以看到生成了一个xxxx.exe_extracted的文件夹进入此文件夹,可以找到一个文件名和应用名称相同,但是没有后缀的文件,这就是得到的*.pyc文件(虽然生成的时候没有后缀,不过这并不妨碍它本身的文件类型)。xxxx.exe_extracted文件夹中的其他文件则是一些依赖程序文件,等等。添加magicnumber接下来一步很关键,需要在my_app_name这个文件中添加magicnumber,也就是一些python版本相关的信息。这里需要使用十六进制编辑器(有很多,不一一介绍了,sublimetext就可以用来编辑) magicnumber前面一部分与python版本有关,可以通过下面的代码查看当前python版本所对应的magicnumber的十六进制形式:importimportlibprint(importlib.util.MAGIC_NUMBER.hex()) 如果不知道编写这个应用所使用的python版本,可能要受到一点阻碍,网上有各python版本对应的magicnumber表,有需要可自行搜索。在十六进制编辑器中打开my_app_name的pyc文件。然后将上面代码得到的magicnumber添加到此文件的最前面(表示版本信息,很重要)。然后再补充24个0(这些东西代表时间、代码大小等信息,没什么用,可以全部用0填充)最后保存文件。如果不进行这一步,那么下一步反编译pyc时将会报错,提示magicnumber有误。反编译pyc将pycdc.exe和my_app_name的pyc文件放到同一文件夹下。在当前文件夹下启动cmd,输入:pycdcmy_app_name>final_my_app_name.py当前文件夹下生成了final_my_app_name.py,这就是反编译的结果。事实上,结果并不完美,存在很多错误,需要后期进行调整。 除了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加密防止反编译_unknownmagicnumber227in-CSDN博客的方法进行了尝试但是效果并不好,以下仅给出方法。调整代码在开始之前,我们需要先对代码进行调整。*.pyd文件只能被导入但不能直接运行,所以主程序不能进行pyd编译。所以,这样做以后依赖文件不会被反编译,但主程序还是会被反编译。我们可以进行一些改变,在主文件中留下那些不重要的代码,让反编译者看不到什么宝贵的信息,将重要的程序内容放到一个模块中,在主文件中只进行调用。在my_app_name.py文件夹下新建一个*.py文件(命名是随意的,这里把它命名为module.py),在这个module.py文件中写入原本是my_app_name.py中的内容,不过做了一些改变,加了一个main函数用于运行。importtkinterastk#导入tkinterdefmain():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函数,然后运行。'''一个简单的应用'''frommoduleimport*#这里不能写成frommoduleimportmain,原因见8.1if__name__=="__main__":main()这样一来,反编译后也只能看到几行与main相关的内容,根本无法获取有用的代码信息。 importtkinterfrommoduleimportmainif__name__=="__main__":main()下载工具 先用pip下载Cython:pipinstallCython此外还需要配置VisualStudio的C++开发工具。先在下方链接下载VisualStudio:MicrosoftC++生成工具-VisualStudio安装生成工具时,勾选“使用C++的桌面开发”并点击右下方安装。如果你已经安装了生成工具但没有勾选这一项,可以启动安装包后,点击“修改”按钮进行更改。然后等待安装完成。编译成pyd在my_app_name.py的文件夹下新建setup.py,并输入以下代码:fromdistutils.coreimportsetupfromCython.Buildimportcythonizesetup(ext_modules=cythonize(["module.py",]))用ext_modules参数指定需要进行编译的文件。如果还有别的python文件需要编译,可以在列表中修改。保存文件后,启动cmd,运行以下命令:pythonsetup.pybuild_ext--inplace完成编译后,文件夹中会生成一些新的文件,从中找出与模块名同名的*.pyd文件,其他没用的文件可以删掉。这个文件名中有一些诸如cp310-win...的东西,表示这个python版本、系统等信息,需要手动将它们删除,只保留模块名和后缀名。 生成pyd文件后,无需管原来的文件module.py,因为python导入时会优先选择pyd后缀的模块。如果输入setup的命令后,出现了一些报错信息,提示需要MicrosoftVisualC++,则代表上一步没有正确完成。进行exe打包完成以上步骤后,可以通过pyinstaller进行打包了。8注意事项与常见问题注意事项:导入 作者似乎发现,在导入python模块时,如果使用fromxxx importfunc的形式,那么pyinstaller只会把导入的func包含到打包的应用里面,而不是整个xxx模块。并且这个过程中,pyinstaller不会管xxx模块中还依赖于哪些模块,这就可能导致func函数根本无法运行。比如我有同一目录下的main.py和module.py:'''module.py'''importtkinterdefmain():tkinter.Tk()tkinter.mainloop()'运行运行'''main.py'''frommoduleimportmainif__name__=="__main__":main()如果用pyinstaller对main.py进行编译,那么最后就会因为找不到tkinter模块而闪退。这是因为main.py中只导入了module.py中的main函数,pyinstaller会进行优化,只打包defmain()函数的这一部分,而不会打包module.py中的其他内容(包括importtkinter这一重要的一句)直接用python运行main.py时就不会出现以上问题。因为python解释fromxxximportfunc这一句时,还是会先把xxx模块运行一次,但是只保留了一个func的变量名。这里提供三种解决方案:换一种导入方式,使用importxxx或fromxxximport*(推荐)在主程序中把模块所需的依赖全导进来(在上面的示例中,就是main.py中添加importtkinter这一句)打包时指定--hidden-import等参数(不推荐)编译时报错:不是可运行的命令或程序首先检查pyinstaller是否被成功安装。在cmd输入piplist,看安装列表中是否存在pyinstaller,如果没有则重新安装,根据安装信息进行处理。如果显示未找到pyinstaller,则应用绝对路径指定pyinstaller。首先进入所在的文件夹,然后复制路径。打包时,将pyinstaller替换为pyinstaller.exe的绝对路径。(pyi-makespec找不到同理)Windowsython目录下的Scripts文件夹GNU/Linux:/usr/binmacOS(usingthedefaultApple-suppliedPython) /usr/binmacOS(usingPythoninstalledbyhomebrew) /usr/local/binmacOS(usingPythoninstalledbymacports) /opt/local/bin例如,你想要执行pyinstallermyscript.py,但是提示找不到pyinstaller.exe。在你的电脑上,pyinstaller.exe安装在了C:\Python\Python310\Scripts这个位置,那么执行:C:\Python\Python310\Scripts\pyinstaller.exemyscript.py还有一种解决方法,将pyinstaller.exe所在的文件夹添加到系统环境变量(推荐)。Windows上添加环境变量办法如下:右击“此电脑”->单击“属性”->单击“高级系统设置”->单击“环境变量”->在用户变量的位置单击“Path”->单击“编辑”->在“编辑环境变量”的窗口单击“新建”->写入pyinstaller.exe的所在路径->一路“确定”进行保存。运行后报错:找不到某些模块或文件如果找不到某些模块,可以尝试修改命令行参数--hidden-import,加入找不到的模块。需要这样的原因往往是那个模块在程序中的导入位置无法被pyinstaller检测到。例如,如果提示找不到sys,os两个模块,那么命令行参数修改为:pyinstaller--hidden-import"sys"--hidden-import"os"...如果使用Spec打包,则应在Analysis类的hiddenimport参数的列表中添加找不到的模块。如果是第三方模块找不到,有可能是pyinstaller没有搜索到储存第三方模块的文件夹。这个文件夹一般是python安装目录下的Lib/site-packages。此时,打包时可以指定-p参数,将这个文件夹路径传递给pyinstaller进行打包,这样pyinstaller就会在site-packages中搜索相关的第三方模块。pyinstaller-p.../Lib/site-packages... 如果只是找不到某些模块中的部分文件,则需要为该模块添加钩子,或者将这些文件传递到命令行参数--add-data,--add-binary中。运行后报错:失去标准输入RuntimeError:input():lostsys.stdin某些程序打包后出现以上报错内容。这是由于代码中使用了input这样的函数让用户进行输入,但是打包时却设置了隐藏控制台。于是,运行打包后的应用后就没有一个控制台让用户进行输入,就会报错。(失去stdout并不会报错,但是print内容不会显示)运行时闪退闪退是由于程序中出现了致命的异常,可能由多种原因导致,需要根据引发的异常进行处理。这里提供一种方法来看到造成闪退的报错信息。在当前文件夹下启动cmd(方法:将文件资源管理器窗口上方的文件路径修改为"cmd",然后回车),然后运行:my_app_name.exe此方法通过命令行运行这个应用。如果程序闪退,命令行窗口不会关闭,上面就留下了报错信息。如果命令行可以正常运行,但是直接运行exe会闪退,考虑下面两种情况:程序只有print输出,输出结束就自动退出了,来不及看到输出内容。解决方法:在程序末尾加一行input(),这样最后输出完会停在那里还有可能是受到部分杀毒软件的影响(尤其是文件夹模式下的打包,很容易出现杀毒软件误报),比如火绒、360等,参见下一节运行后杀毒软件提示存在木马/无权限访问出现运行后杀毒软件报木马可能是杀毒软件误报导致的,并不代表一定真的有木马。此外,exe直接运行时异常闪退,但是exe在命令行仍能正常运行,也有可能是受到杀毒软件的影响。杀毒软件报错可能是因为打包后的程序显得不可信,一般可以采取以下措施:使用单文件模式打包(-F)而不是文件夹模式给程序指定一个图标(--iconxxx.ico)如果不是命令行程序,打包时尽量把命令行隐藏(添加-w参数)如果杀毒软件报错是由于对应的某个模块造成的,可以自行搜索避免的方法由于文档内容较多,很难涵盖每一个点,如果你认为还有本文没有提及的重点,请指出,欢迎任何建议者。
|
|