在使用Go时,我经常会羡慕Go的出色的静态编译能力,更具体的说,纯Go写的程序会编译成一个零依赖的二进制文件,同时也可以非常轻松的做到交叉编译。甚至是,我可以把一些资源文件也编译到这个文件中。比如在 DailyGakki 中,我便通过go-bindata把图片塞到了二进制文件中。暂且不提这样对文件大小、对性能有什么影响,只有一个文件就能过实现全部功能,是不是听起来很棒。
那么python能做到这点吗?当然也是能的啦,只是不是原生支持的啦。Python有一个东西叫做 pyinstaller,它会把python运行时的东西都打包起来,比如我们有这样一个程序test.py
:
import time def main(): print("Hello world at ", time.asctime()) if __name__ == '__main__': main()
要打包成一个文件,就会用到-F
参数:
pyinstaller -F test.py
pyinstaller 会生成一个spec
文件,然后会在dist
目录下生成一个test
(对应windows就是test.exe
),直接运行这个exe就好了。把这个exe拷贝到同平台其他电脑上,也是可以运行的。
当然,需要注意的是,别看这里也直接生成了二进制文件,但是这个二进制文件并不是加密的哦,用一些工具可以很容易的找到源代码的。
ModuleNotFoundError
由于Python是动态语言,我们可能会在运行时使用importlib
或者__import__
来import一些模块,花式操作。比如有如下代码:
# test_lib.py def even(): print(0, 2, 4, 6, 8) def odd(): print(1, 3, 5, 7, 9) # test.py import time import importlib def main(): module = importlib.import_module("test_lib") if time.localtime().tm_sec % 2 == 0: func_name = "even" else: func_name = "odd" fun = getattr(module, func_name) fun() if __name__ == '__main__': main()
没问题吧,动态导入module嘛,常规操作
❯ python3 test.py ⏎ 1 3 5 7 9
当我们使用pyinstaller时是会无报错完成的,但是当我们运行时:
❯ dist/test Traceback (most recent call last): File "test.py", line 26, in <module> main() File "test.py", line 15, in main module = importlib.import_module("test_lib") File "importlib/__init__.py", line 127, in import_module File "<frozen importlib._bootstrap>", line 1030, in _gcd_import File "<frozen importlib._bootstrap>", line 1007, in _find_and_load File "<frozen importlib._bootstrap>", line 984, in _find_and_load_unlocked ModuleNotFoundError: No module named 'test_lib' [91513] Failed to execute script test
我们会发现无法执行
其实原因很简单啦。pyinstaller在build binary时,会有一个analysis的步骤去分析都有哪些module,使用__import__
和importlib
,甚至是exec
、eval
的都是无法被检测到的,所以在运行时就会报错啦。想要解决也很简单,hidden-imports
即可:
pyinstaller -F --hidden-import=test_lib test.py ❯ dist/test 1 3 5 7 9
这时就会发现没问题啦。
当然啦,这个参数也可以写在那个spec中的,后面会介绍的哦。
嵌入资源文件
如果我想把一些资源文件,比如html、css,甚至是sqlite嵌入到这个binary中,该怎么做呢?
pyinstaller提供了一个叫做 data 的东西,可以用于嵌入任何东西哦。当然了你要是把一段电影放进去,那咱也没什么意见,就运行时慢呗,毕竟还是要解压缩的。
考虑如下代码,text.txt
是同级目录的一个普通文本文件
with open("text.txt") as f: text = f.read() print(text) ❯ ./test_bin Traceback (most recent call last): File "test_bin.py", line 10, in <module> FileNotFoundError: [Errno 2] No such file or directory: 'text.txt' [98107] Failed to execute script test_bin
我们会发现,哎呀,文件不在,找不到~
那么一种方法是,binary同级目录塞一个text.txt
,这个方法就很简单了。另外一种做法是我们把这个txt塞进binary里!
命令行参数使用 --add-data
,同时我们需要小改一下代码来使用绝对路径
import os root_path = os.path.dirname(__file__) file_path = os.path.join(root_path, "text.txt") with open(file_path) as f: text = f.read() print(text) # 然后这样哦 pyinstaller -F --add-data "text.txt:." test_bin.py
--add-data
的参数格式是 src:dst
,.
的含义是与py文件同级,如果你的脚本是打开的test/test.txt
,那么就要 test/test.txt:test/
哦。
benny@BennyのMBP:~/PycharmProjects/untitled/dist $ ./test_bin this is a text file 1234
没问题哦。当然啦,只要你想玩,可以用*
做通配符的哦。
spec文件
虽然说,在命令行中加参数总是能够解决我们的问题,但是这样每次输入长长的命令也真的是太累啦。有没有更简单的方法呢?当然有啦。比如你看是不是有一个test.spec?每次在你运行 pyinstaller xxx.py
时都会生成一个这样的文件。在这个spec中会有datas和hidenimport的选项,那就简单啦!
a = Analysis(['test.py'], pathex=['/Users/benny/PycharmProjects/untitled'], binaries=[], datas=[ ('text.txt', '.'), ], hiddenimports=["test_lib"], hookspath=[], runtime_hooks=[], excludes=[], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False)
没问题,让我们重新运行一次!请记得要用这个spec文件而不是py文件哦。
pyinstaller -F test.spec ❯ ./test 0 2 4 6 8 this is a text file 1234 [benny@BennyのMBP] ~/PycharmProjects/untitled/dist ❯ ./test 1 3 5 7 9 this is a text file 1234
一点问题都没有呐!这个data我们也可以弄得很复杂,比如这样的
[ ( 'src/README.txt', '.' ), ( '/mygame/data', 'data' ), ( '/mygame/sfx/*.mp3', 'sfx' ) ]
一些后话
我们可以用datas选项嵌入文件,我们其实也可以用binaries选项嵌入一些dll/so/dylib文件。比如说,我可以用msf那一套生成一个反弹的dll,然后……
binaries=[ ( 'reverse_shell.dll', '.' ) ]
🤣🤣🤣🤣
实践
我的YYeTsWeb就使用这种方式嵌入了很多html/css/js,以及SQLite数据库。也就是只要下载回来这个文件,双击一下,你的本地就可以有一个能够运行的yyets啦!别怪我没说,运行陌生的exe文件是很危险的,说不准就有人丢了一个奇怪的dll文件进去。不过我没有这么做啦,信不信由你好了。
yyetsweb依赖一些环境变量,所以直接双击start_windows.bat
就好了;macOS用户可以用start_unix.sh
。至于Linux用户,你都用Linux了,还用得着我弄的小白一键启动?自己搞去。
这个exe可能启动会比较慢,boostrap过程解压缩比较耗时,毕竟是会冒出来一个几百兆的SQLite文件呢。macOS上由于GateKeeper机制的存在,估计就更慢了吧🙄️
运行效果,偷懒,我就直接用了旧版页面,没有把新版也搞过来。
资源页面也是旧版的
反正怎么说,可以离线使用了,哪天万一网站挂了,手里有这个文件也不慌呀~
下载地址可以看 GitHub Release
参考资料
Using Spec Files https://pyinstaller.readthedocs.io/en/stable/spec-files.html
When Things Go Wrong https://pyinstaller.readthedocs.io/en/stable/when-things-go-wrong.html
--本评论由Telegram Bot回复~❤️
--本评论由Telegram Bot回复~❤️