记PyInstaller在win10下使用

PyInstaller是一款将python脚本打包成exe可运行文件的工具,支持linux/Windows/MacOS等多个系统。

版本

PyInstaller 3.2.1 + Python 3.5.2 + Windows10 64bit,用 pyinstaller -v 查看版本

PyInstaller 安装

pip install pyinstaller

hello world

尝试打包 hello_world.py,文件内容:

1
print('Hello World!')

用命令

1
2
cd path\to\hello_world.py
pyinstaller hello_world.py

生成dist\hello_world目录,其中包含可执行文件hello_world.exe。可用--onefile参数:pyinstaller --onefile hello_world.py,将所有文件压缩在exe中。

注:如果报错UnicodeDecodeError,根据这篇blog的建议,在cmd中执行命令chcp 65001

PyInstaller打包数据,记soundflie库

PyInstaller会自动打包import语句引用的包,其实只包括包下的python脚本。但如果脚本依赖于数据文件或dll链接库等,PyInstaller无法自动分析出这些依赖,需要配置参数。

用tmp.py生成exe。tmp.py内容:

1
2
3
import soundfile # version: 0.9.0.post1
sample, sr = soundfile.read('sample.wav') # 读当前目录下的音频sample.wav
print(sample.shape)

生成exe后运行,报错找不到libsndfile64bit.dll,因为soundfile依赖这个库,而PyInstaller并没有打包。

  • 那么,首先告诉PyInstaller打包:
    参考PyInstaller手册关于使用spec文件,执行命令pyi-makespec tmp.py(同样可加上--onefile参数),生成tmp.spec配置文件;
  • 在spec文件中,向Analysis构造器传入参数datas列表,添加二元组('X:\\path\\to\\site-packages\\_soundfile_data\\libsndfile64bit.dll', '_soundfile_data'),元组第一个元素指示dll文件路径,后者指示运行时存放的目录。在上一步如果指定了--onefile,dll文件会在运行时被解压到一个临时目录(以_MEI开头,见下个步骤)的_soundfile_data目录下,如果未指定,会被复制到exe同目录的_soundfile_data目录下;
  • 修改soundfile.py源码如下,最后执行命令pyinstaller tmp.spec
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 原始代码
    _snd = _ffi.dlopen(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)), '_soundfile_data', _libname))
    # 修改
    try:
    # with --onefile, PyInstaller creates a temp folder and stores path in _MEIPASS
    base_path = sys._MEIPASS
    except Exception:
    base_path = _os.path.dirname(_os.path.abspath(__file__))
    _snd = _ffi.dlopen(_os.path.join(base_path, '_soundfile_data', _libname))

记librosa库

librosa依赖的resampy库需要打包site-packages\resampy\data下的两个数据文件,与对soundfile的处理一样,向datas添加元组('X:\\path\\to\\site-packages\\resampy\\data', 'data'),然后修改resampy包下filters.py源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# librosa version: 0.5.1
# resampy version: 0.1.5
# 原始代码
fname = os.path.join('data',.path.extsep.join([filter_name, 'npz']))
data = np.load(pkg_resources.resource_filename(__name__, fname))

# 修改:
try:
base_path = sys._MEIPASS
except Exception:
base_path = '.'
fname = os.path.join(base_path,'data', os.path.extsep.join([filter_name, 'npz']))
data = np.load(pkg_resources.resource_filename(__name__, fname))

此外,有几个包或模块没有被PyInstaller打包,向spec文件Analysis构造器传入参数 hiddenimports=['cython', 'sklearn.neighbors.typedefs']