登录
  • 人们都希望被别人需要 却往往事与愿违
  • 过早的优化是万恶之源 Premature optimization is the root of all evil@Donald Knuth (算法大牛 图灵奖得主)

使用pyinstaller嵌入静态资源

编程 Benny小土豆 3639次浏览 4100字 4个评论
文章目录[显示]

在使用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,甚至是execeval的都是无法被检测到的,所以在运行时就会报错啦。想要解决也很简单,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)

吐槽一下,data不是datum的复数形式嘛,这个datas是什么鬼哟哈哈哈

没问题,让我们重新运行一次!请记得要用这个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了,还用得着我弄的小白一键启动?自己搞去。

使用pyinstaller嵌入静态资源

这个exe可能启动会比较慢,boostrap过程解压缩比较耗时,毕竟是会冒出来一个几百兆的SQLite文件呢。macOS上由于GateKeeper机制的存在,估计就更慢了吧🙄️

运行效果,偷懒,我就直接用了旧版页面,没有把新版也搞过来。

使用pyinstaller嵌入静态资源

资源页面也是旧版的

使用pyinstaller嵌入静态资源

反正怎么说,可以离线使用了,哪天万一网站挂了,手里有这个文件也不慌呀~

下载地址可以看 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


文章版权归原作者所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/pyinstaller-static-files.html
喜欢 (21)
分享:-)
关于作者:
If you have any further questions, feel free to contact me in English or Chinese.
发表我的评论
取消评论

                     

去你妹的实名制!

  • 昵称 (必填)
  • 邮箱 (必填,不要邮件提醒可以随便写)
  • 网址 (选填)
(4)个小伙伴在吐槽
  1. pyibstaller打包有点不方便的地方就是不能交叉编译,而且win10打包的东西在win7不能用,不知道go的静态文件会不会这样?
    萌新2021-07-20 11:17 回复
    • 是的,要在低版本的系统上打包,这样会比较好。Go的话,如果能 CGO_ENABLED=0 那么没问题的。
      --本评论由Telegram Bot回复~❤️
      Benny小土豆2021-07-20 11:19 回复
  2. 好像人人的资源没速度了,每秒30-500kb左右 :???:
    阿飞2021-06-18 22:55 回复
    • 🤕你这已经不错了啦🤨
      --本评论由Telegram Bot回复~❤️
      Benny小土豆2021-06-18 22:56 回复