Skip to content

商务合作:vTikTok


公众号:



前言


请慎入,pyinstaller的坑多的令人发指,它会自动打包代码中相关的依赖,类似于Android的gradle,但也会出现类似于Android中动态加载,反射,so库未打入导致运行中异常的情况。

pyinstaller打包

pyinstaller不仅可以打包成exe,还可以打包成app,linux下的可执行文件,mac下的可执行文件。 同样PyQT也是使用pyinstaller打包的,pyinstaller很智能,可以在同一个项目中根据入口main.py的引用来打相关依赖,所以包体积跟着依赖项而有所不同,完全不需要建立不同的工程项目,就可以打不同的小工具。

Python打包EXE程序

  1. pip3 install pyinstaller

  2. 我们来将这个.py的文件打包成一个exe,我们直接cmd切换到这个脚本的目录,执行命令:pyinstaller-F setup.py ps: -F参数表示覆盖打包,这样在打包时,不管我们打包几次,都是最新的,这个记住就行,固定命令。

  3. 不显示控制台,并设置图标,pyinstaller -F -w -i wind.ico xx.py

pyinstaller打包时候经常遇到的问题

首先python的版本要和pyinstaller兼容 这里python35和pyInstaller-3.2.1对应

安装pyInstaller-3.2.1

pip install pyinstaller

打包

pyinstaller [option] mypython.py

以下是pyinstaller的一些option参数说明:

-F 表示生成单个可执行文件 
-w 表示去掉控制台窗口,这在GUI界面时非常有用。不过如果是命令行程序的话那就把这个选项删除吧! 
-p 表示你自己自定义需要加载的类路径,一般情况下用不到 
-i 表示可执行文件的图标
-F 表示生成单个可执行文件 
-w 表示去掉控制台窗口,这在GUI界面时非常有用。不过如果是命令行程序的话那就把这个选项删除吧! 
-p 表示你自己自定义需要加载的类路径,一般情况下用不到 
-i 表示可执行文件的图标

问题一

pyinstaller test.py 出现 failed to create process

解决方法是找到pyinstaller-script.py,把第一行的路径用引号括起来,最终是下面这个样子就对了

#!"c:\program files (x86)\python35-32\python.exe"

问题二

Paste_Image.png

这个错误是自己大意,因为刚从python36换成python35,没有安装requests模块,所以报的错。

Paste_Image.png

如何解决这个错误:参考文章

最终命令:

pyinstaller -F --hidden-import=queue test.py

修改生成exe的显示图标

--i app.ico

pyinstaller -F -i app.ico --hidden-import=queue test.py

注意:图标必须是ico格式,可以在线转换为ico

pyinstaller打包图片等资源

默认只能打包字节码,不会打包资源,需要手动添加

试过很多次网上所说的方案都失败了。 这时建议不再生成一个文件的方式打包,而是目录形式,把相关图片文件拷贝进去就行了,不要太纠结,很浪费时间。

所以我建议一个命令全搞定 自定义ico图标 且 不带命令行的 多文件版 App

pyinstaller -w --i app.ico gui_tkinter.py

直接看这里,打包很简单

打包方式只有两种:

  1. pyinstaller -D --add-data 'config_debug.yaml:.' --add-data 'config.yaml:.' --hidden-import=main main.py
  2. pyinstaller main.spec(第一种方式会生成main.spec文件,那么后续可以直接根据这个配置文件来打包)

注意事项:

  1. 只能在MAC系统打MAC包,在windows系统打windows包,不能一个平台打包另一个平台的包。
  2. window建议使用Python3.6.8版本

使用教程

安装

pip install pyinstaller

打包

方式一:pyinstall -D test.py(直接生成一个dist包【包含了多个文件】,这种包执行速度比单个bundle的包要快一些) 方式二:pyinstall -F test.py(生成一个单个的可执行文件,这种包执行速度比多个dist包要慢一些)

文件如何引用不容易出错?

  1. 首先要设置一个项目的根路径,其他路径都是相对这个路径下的,否则不好维护。
  2. 如果是多文件,使用经常用的os.path.dirname(os.path.abspath(file)) 获取当前文件的绝对路径,然后再拼接上相对路径。
  3. 如果是单文件,实际Python会把我们的单文件解压到一个临时目录,然后执行,这时候的目录路径就不是我们原来存放的位置了,可以根据这个来判断:
python
if getattr(sys, 'frozen', False):
    BASE_DIR = os.path.dirname(sys.exequtable)
else:
    BASE_DIR = os.path.dirname (os.path.abspath(__file__))
if getattr(sys, 'frozen', False):
    BASE_DIR = os.path.dirname(sys.exequtable)
else:
    BASE_DIR = os.path.dirname (os.path.abspath(__file__))

那么一个大型项目,pyinstaller也是可以自动识别并打包的,只要你的项目结构是清晰的。除了那种动态引入模块的方式需要我们手动配置,如:card = importlib.import_module ("utils.card"),这种方式pyinstaller是无法识别的,需要我们手动配置。不过大多数项目,一般都不需要我们配置。

pyinstaller简单,而强大!!

非常有用的技巧

pip freeze > requirements.txt 这个命令可以把当前环境的所有包都写入到requirements.txt文件中,然后在新环境中使用pip install -r requirements.txt就可以安装所有的包了。

最终一行打包命令

pyinstaller -w --i app.ico app.py

打包非bundle形式的exe文件时候,默认不会追加配置文件,如果需要把config.yaml也打包进去,执行下面:

pyinstaller -D --add-data 'config_debug.yaml:.' --add-data 'config.yaml:.' main.py
pyinstaller -D --add-data 'config_debug.yaml:.' --add-data 'config.yaml:.' main.py

注意事项

pyinstaller 执行打包命令如:

pyinstaller -D app.py,那么打包后会在当前项目路径下生成 dist/app/...

pyinstaller -D main.py,那么打包后会在当前项目路径下生成 dist/main/...

打包FastAPI后,双击会报了下面的错误:

ERROR: Error loading ASGI app. Could not import module "main".

使用下面的方式可解决:

pyinstaller -D --add-data 'config_debug.yaml:.' --add-data 'config.yaml:.' --hidden-import=main main.py

pyinstaller打包时如何设置图标(AI辅助)

助手: 在使用PyInstaller打包Python程序时,可以通过-i参数来设置程序的图标。具体操作如下:

  1. 准备一个.ico格式的图标文件,例如myicon.ico

  2. 在命令行中,使用以下命令来打包程序:

bash
pyinstaller -F -w -i myicon.ico your_script.py
pyinstaller -F -w -i myicon.ico your_script.py

其中:

  • -F表示生成单个可执行文件。
  • -w表示窗口化应用程序,不显示控制台窗口。
  • -i myicon.ico表示使用myicon.ico作为程序的图标。
  • your_script.py是你要打包的Python脚本。

注意:图标文件必须是.ico格式的,其他格式(如.png.jpg等)是不支持的。如果你只有其他格式的图标,可以使用在线工具将其转换为.ico格式。

为什么有的地方好像用了--i,这个是正确的吗?

--分割线--

助手: 不正确,PyInstaller的参数应该是-i,而不是--i。在大多数命令行工具中,单破折线-后面跟的是单字母参数,双破折线--后面跟的是全称参数。例如,-i--icon在PyInstaller中是等价的,都是用来指定程序图标的参数。所以,你应该使用-i或者--icon,而不是--i

使用--icon参数来指定程序图标的方法和使用-i参数是一样的。你只需要将-i替换为--icon即可。具体操作如下:

  1. 准备一个.ico格式的图标文件,例如myicon.ico

  2. 在命令行中,使用以下命令来打包程序:

bash
pyinstaller -F -w --icon=myicon.ico your_script.py
pyinstaller -F -w --icon=myicon.ico your_script.py

其中:

  • -F表示生成单个可执行文件。
  • -w表示窗口化应用程序,不显示控制台窗口。
  • --icon=myicon.ico表示使用myicon.ico作为程序的图标。
  • your_script.py是你要打包的Python脚本。

注意:图标文件必须是.ico格式的,其他格式(如.png.jpg等)是不支持的。如果你只有其他格式的图标,可以使用在线工具将其转换为.ico格式。

那么为什么-i没有用=,而--icon却用了=号,这是什么用法?

--分割线--

助手: 在命令行参数中,-i--icon都可以用来指定图标文件,它们的用法稍有不同:

  • 对于-i参数,你可以用空格将参数和值分开,例如-i myicon.ico
  • 对于--icon参数,你可以用等号将参数和值连接起来,例如--icon=myicon.ico

这是命令行参数的常见用法,不同的参数可以有不同的连接方式。在PyInstaller中,-i--icon都是可以接受的,你可以根据自己的习惯选择使用哪一种。

pyinstaller打包pywebview程序

且附带前端资源文件

--分割线--

助手: PyInstaller可以将Python程序打包成一个独立的可执行文件,这样就可以在没有Python环境的机器上运行。下面是如何使用PyInstaller打包pywebview程序的步骤:

  1. 首先,你需要安装PyInstaller。在命令行中输入以下命令安装PyInstaller:

    pip install pyinstaller
    pip install pyinstaller
  2. 打包你的程序。在命令行中,切换到你的程序所在的目录,然后输入以下命令:

    pyinstaller --add-data 'gui:gui' your_program.py
    pyinstaller --add-data 'gui:gui' your_program.py

    这里,your_program.py是你的程序的文件名,gui:gui表示将gui目录打包到程序中,并在程序运行时将其作为gui目录访问。

    如果你的程序有多个Python文件,你应该在your_program.py后面添加它们的文件名。

    如果你的程序需要一些额外的库,你可以使用--hidden-import选项添加它们。例如,如果你的程序需要requests库,你可以这样打包你的程序:

    pyinstaller --add-data 'gui:gui' --hidden-import requests your_program.py
    pyinstaller --add-data 'gui:gui' --hidden-import requests your_program.py
  3. 等待PyInstaller完成打包。打包完成后,你会在dist目录下找到一个名为your_program的目录,里面有一个可执行文件和一些依赖库。你可以将这个目录整个复制到目标机器上运行。

注意:在Windows系统上,--add-data参数的路径分隔符是分号;,而在Linux和Mac系统上,路径分隔符是冒号:。例如,在Windows上,你应该这样打包你的程序:

pyinstaller --add-data 'gui;gui' your_program.py
pyinstaller --add-data 'gui;gui' your_program.py

终极命令(尽量不要用单文件,多文件的包效果更好一点)

sh
pyinstaller -w --icon=favicon.ico --add-data='config_debug.yaml:.' --add-data='config.yaml:.' --hidden-import=main main.py
pyinstaller -w --icon=favicon.ico --add-data='config_debug.yaml:.' --add-data='config.yaml:.' --hidden-import=main main.py

排坑时刻

pyinstaller相比py2app的好处是可以自动识别import方式导入的包,但有些第三方的库在编写代码时并不是用这种方式导入的,而是用了一种叫做动态导入/动态加载的方式,很多开源项目会用这种来实现动态加载/动态插件,pyinstaller是无法识别此类导入的,比如Django、opencv-python等等。

动态加载:importlib.import_module(module_name) 其中OpenCV和MoviePy大量用了这种写法,需要特别注意。为了解决这个问题,可以新建一个moviepy_import.py文件,然后把需要的库都导入,然后在main.py入口文件中:import moviepy_import,解决moviepy批量导入的骚写法。

解决方法:

  1. pyinstaller命令中增加参数或者编写spec文件来给出额外信息(不友好,弃用)
  2. 使用hook,实现动态替换,hook指的是在不改变原库的基础上,把动态加载映射为我们正常开发中用到的import的导入方式

pyinstaller文档中给出了hook的开发细节,不过,pyinstaller的社区已经将一些知名库的hook都开发好了,当你安装好pyinstaller时,相应的hook库其实也安装好了,叫pyinstaller-hooks-contrib。

opencv-python和MoviePy都用到了ffmpeg.dll或ffmpeg.so,需要观察这个库是否被自动打进来,如果没有,打包时就需要指定要打包它的位置。

MoviePy打包

官方作者反馈:属于动态导包问题

如何打印MoviePy中所有的隐藏包,不需要修改源码,idea可以直接修改源代码。

python
import moviepy.audio.fx.all as afx
import moviepy.video.fx.all as vfx
import moviepy.audio.fx.all as afx
import moviepy.video.fx.all as vfx

文件:/ElectronPython/venv/lib/python3.9/site-packages/moviepy/video/fx/all/__init__.py

js
import pkgutil

import moviepy.video.fx as fx

__all__ = [name for _, name, _ in pkgutil.iter_modules(
    fx.__path__) if name != "all"]

for name in __all__:
    exec("from ..%s import %s" % (name, name))
    print("from ..%s import %s" % (name, name))
import pkgutil

import moviepy.video.fx as fx

__all__ = [name for _, name, _ in pkgutil.iter_modules(
    fx.__path__) if name != "all"]

for name in __all__:
    exec("from ..%s import %s" % (name, name))
    print("from ..%s import %s" % (name, name))

注释掉:

js
for name in __all__:
    exec("from ..%s import %s" % (name, name))
for name in __all__:
    exec("from ..%s import %s" % (name, name))

替换import方式的正常导入,完整代码如下:

js
"""
Loads all the fx !
Usage:
import moviepy.video.fx.all as vfx
clip = vfx.resize(some_clip, width=400)
clip = vfx.mirror_x(some_clip)
"""

import pkgutil

import moviepy.video.fx as fx

__all__ = [name for _, name, _ in pkgutil.iter_modules(
    fx.__path__) if name != "all"]

# for name in __all__:
#     exec("from ..%s import %s" % (name, name))

from ..accel_decel import accel_decel
from ..blackwhite import blackwhite
from ..blink import blink
from ..colorx import colorx
from ..crop import crop
from ..even_size import even_size
from ..fadein import fadein
from ..fadeout import fadeout
from ..freeze import freeze
from ..freeze_region import freeze_region
from ..gamma_corr import gamma_corr
from ..headblur import headblur
from ..invert_colors import invert_colors
from ..loop import loop
from ..lum_contrast import lum_contrast
from ..make_loopable import make_loopable
from ..margin import margin
from ..mask_and import mask_and
from ..mask_color import mask_color
from ..mask_or import mask_or
from ..mirror_x import mirror_x
from ..mirror_y import mirror_y
from ..painting import painting
from ..resize import resize
from ..rotate import rotate
from ..scroll import scroll
from ..speedx import speedx
from ..supersample import supersample
from ..time_mirror import time_mirror
from ..time_symmetrize import time_symmetrize
"""
Loads all the fx !
Usage:
import moviepy.video.fx.all as vfx
clip = vfx.resize(some_clip, width=400)
clip = vfx.mirror_x(some_clip)
"""

import pkgutil

import moviepy.video.fx as fx

__all__ = [name for _, name, _ in pkgutil.iter_modules(
    fx.__path__) if name != "all"]

# for name in __all__:
#     exec("from ..%s import %s" % (name, name))

from ..accel_decel import accel_decel
from ..blackwhite import blackwhite
from ..blink import blink
from ..colorx import colorx
from ..crop import crop
from ..even_size import even_size
from ..fadein import fadein
from ..fadeout import fadeout
from ..freeze import freeze
from ..freeze_region import freeze_region
from ..gamma_corr import gamma_corr
from ..headblur import headblur
from ..invert_colors import invert_colors
from ..loop import loop
from ..lum_contrast import lum_contrast
from ..make_loopable import make_loopable
from ..margin import margin
from ..mask_and import mask_and
from ..mask_color import mask_color
from ..mask_or import mask_or
from ..mirror_x import mirror_x
from ..mirror_y import mirror_y
from ..painting import painting
from ..resize import resize
from ..rotate import rotate
from ..scroll import scroll
from ..speedx import speedx
from ..supersample import supersample
from ..time_mirror import time_mirror
from ..time_symmetrize import time_symmetrize

INFO: UPX is not available.

这个也不算错误,UPX是一个压缩工具,可以将可执行文件压缩,减小体积,但是这个工具只能在Windows下使用,所以在Linux下是无法使用的,所以会提示这个警告,不影响使用。

pyinstaller打包后运行报错

pycharm运行正常,进入包内执行也正常,但是单独双击就闪退!!!

js
def get_ffmpeg_exe():
    """
    Get the ffmpeg executable file. This can be the binary defined by
    the IMAGEIO_FFMPEG_EXE environment variable, the binary distributed
    with imageio-ffmpeg, an ffmpeg binary installed with conda, or the
    system ffmpeg (in that order). A RuntimeError is raised if no valid
    ffmpeg could be found.
    """

    # 1. Try environment variable. - Dont test it: the user is explicit here!
    exe = os.getenv("IMAGEIO_FFMPEG_EXE", None)
    if exe:
        return exe

    # Auto-detect
    exe = _get_ffmpeg_exe()
    if exe:
        return exe
def get_ffmpeg_exe():
    """
    Get the ffmpeg executable file. This can be the binary defined by
    the IMAGEIO_FFMPEG_EXE environment variable, the binary distributed
    with imageio-ffmpeg, an ffmpeg binary installed with conda, or the
    system ffmpeg (in that order). A RuntimeError is raised if no valid
    ffmpeg could be found.
    """

    # 1. Try environment variable. - Dont test it: the user is explicit here!
    exe = os.getenv("IMAGEIO_FFMPEG_EXE", None)
    if exe:
        return exe

    # Auto-detect
    exe = _get_ffmpeg_exe()
    if exe:
        return exe

MoviePy使用的是imageio_ffmpeg中的ffmpeg,留意MoviePy在上述ffmpeg是否安装的查找过程。

一直无法排查错误:

js
try:
    vClip = VideoFileClip(PathUtils.get_desk_top("temp.m4v")).subclip(0, 5)
    vClip.write_videofile("temp.mp4", audio_codec="aac", threads=50)
except Exception as e:
    # 写入文件
    open(PathUtils.get_desk_top("垃圾.md"), "w").write(str(e))
try:
    vClip = VideoFileClip(PathUtils.get_desk_top("temp.m4v")).subclip(0, 5)
    vClip.write_videofile("temp.mp4", audio_codec="aac", threads=50)
except Exception as e:
    # 写入文件
    open(PathUtils.get_desk_top("垃圾.md"), "w").write(str(e))

或者:

js
import sys
import traceback

def log_exception(exc_type, exc_value, exc_traceback):
    # 将异常信息写入文件
    with open("error_log.txt", "a") as f:
        f.write("Unhandled exception:\n")
        traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)

# 设置异常处理钩子
sys.excepthook = log_exception

# 你的应用程序代码从这里开始
def main():
    # 你的应用程序逻辑
    # ...

if __name__ == "__main__":
    main()
import sys
import traceback

def log_exception(exc_type, exc_value, exc_traceback):
    # 将异常信息写入文件
    with open("error_log.txt", "a") as f:
        f.write("Unhandled exception:\n")
        traceback.print_exception(exc_type, exc_value, exc_traceback, file=f)

# 设置异常处理钩子
sys.excepthook = log_exception

# 你的应用程序代码从这里开始
def main():
    # 你的应用程序逻辑
    # ...

if __name__ == "__main__":
    main()

看到报错:

js
[Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file tempTEMP_MPY_wvf_snd.mp4:

b'tempTEMP_MPY_wvf_snd.mp4: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).
[Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file tempTEMP_MPY_wvf_snd.mp4:

b'tempTEMP_MPY_wvf_snd.mp4: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).

无法写入文件(奇奇怪怪-Fuck)

js
[Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file \Users\xx\Desktop\dist2TEMP_MPY_wvf_snd.mp4:

b'\\Users\\xx\\Desktop\\TEMP_MPY_wvf_snd.mp4: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).
[Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file \Users\xx\Desktop\dist2TEMP_MPY_wvf_snd.mp4:

b'\\Users\\xx\\Desktop\\TEMP_MPY_wvf_snd.mp4: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).

moviepy的issue

conda install -c conda-forge ffmpeg

上面的错误是由于FFmpeg版本问题,默认MoviePy使用的FFmpeg是 venvs/lib/python3.9/site-packages/imageio_ffmpeg/binaries/ffmpeg-osx64-v4.2.2下的ffmpeg,写合成视频的时候,竟然不支持音频导出。

js
vClip = VideoFileClip(PathUtils.get_desk_top("temp.mp4"), audio=False).subclip(0, 5)
vClip.write_videofile(PathUtils.get_desk_top("dist.mp4"))
vClip = VideoFileClip(PathUtils.get_desk_top("temp.mp4"), audio=False).subclip(0, 5)
vClip.write_videofile(PathUtils.get_desk_top("dist.mp4"))

设置audio=False后,程序运行正常。另外vClip.write_videofile("dist.mp4"),注意这里是不能使用相对路径的,否则也会出现/dist之类无权写入的目录。

注意打包环境

在venv中打包,py2app和pyinstaller都这样比较好,在anaconda环境打包,包体积非常大。比如一个20M,一个120M。

注意打包缓存问题

sudo rm -rf build dist

清除旧的目录,防止对新一次的打包产生影响,Android经常出现这种坑爹的坑。

python
a = Analysis(
    ['main_local.py'],
    pathex=["venv/lib/site-packages/cv2"],
    binaries=[("venv/lib/site-packages/cv2/opencv_videoio_ffmpeg.so", ".")],
    datas=[('h5', 'h5')],
    ...
a = Analysis(
    ['main_local.py'],
    pathex=["venv/lib/site-packages/cv2"],
    binaries=[("venv/lib/site-packages/cv2/opencv_videoio_ffmpeg.so", ".")],
    datas=[('h5', 'h5')],
    ...
  1. pathex: 这个选项用于指定 PyInstaller 在分析代码时应该考虑的额外路径。通常情况下,PyInstaller 会自动搜索 Python 脚本中导入的模块的位置。但在某些情况下,如果模块位于非标准路径,或者由于某种原因 PyInstaller 未能自动找到它们,就需要手动指定这些路径。
  2. binaries: 这个选项用于明确指定应该包含在可执行文件中的二进制文件。这些通常是 DLL 或者 SO 文件,对于应用程序的运行至关重要,但不是 Python 模块。比如上面的配置指的是将 opencv_videoio_ffmpeg.so 这个 DLL 文件包含在打包后的应用程序中,并且将其放在根目录(由 "." 指定)。这通常用于确保应用程序能够访问必要的依赖,尤其是那些 PyInstaller 可能无法自动检测到的依赖。

总的来说,pathex 用于添加额外的搜索路径以便找到 Python 模块,而 binaries 用于确保必要的非 Python 文件(如动态链接库)被包含在最终的打包文件中。

打包信息出现14799 INFO: EXE target arch: x86_64

先确定Python的架构:

方式一:

python
if __name__ == "__main__":
    os.environ["IMAGEIO_FFMPEG_EXE"] = "/Users/xx/MyLib/ffmpeg"
    print(platform.machine())
if __name__ == "__main__":
    os.environ["IMAGEIO_FFMPEG_EXE"] = "/Users/xx/MyLib/ffmpeg"
    print(platform.machine())

方式二:

file $(which python)

方式三:

file $(which python3)

同理,查看FFmpeg的版本信息:

file $(which ffmpeg)/Users/alien/.conda/envs/alien_conda/bin/ffmpeg -version

查看当前运行的Python版本,果然,使用的也是x86_64。下载最新的Python程序:https://www.python.org/downloads/macos/,支持arm64

依赖项兼容性:在 M1 Mac 上,确保所有的 Python 依赖项都是为 ARM 架构编译的,或者确保您的 Python 解释器运行在与依赖项兼容的模式下。 虚拟环境:如果您在虚拟环境中工作,确保该环境也是针对正确的架构设置的。 理解 Python 在您的 M1 Mac 上的运行模式对于解决兼容性问题非常重要,尤其是当涉及到需要特定平台二进制文件的库(如 ffmpeg)时。

阅读源码解决

很无奈,即使尝试很多方法,依然无效,只能通过源码查看具体是哪一部出现了错误,main.py下面有导入moviepy的包,如下:

py
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.io.VideoFileClip import VideoFileClip

上面的导入间接引用了下面的代码,也就是因为MoviePy的导入,导致启动非常慢,所以尽量把导入延迟到点击事件里面。

  1. from moviepy.audio.io.AudioFileClip import AudioFileClip
  2. from moviepy.audio.AudioClip import AudioClip
  3. from moviepy.audio.io.ffmpeg_audiowriter import ffmpeg_audiowrite
  4. from moviepy.config import get_setting
  5. ElectronPython/venv/lib/python3.11/site-packages/imageio -> imageio_ffmpeg.get_ffmpeg_exe()
  6. exe = os.getenv("IMAGEIO_FFMPEG_EXE", None)
  7. 假如os.getenv找不到,会尝试从:bin_dir = resource_filename("imageio_ffmpeg", "binaries")找,'ElectronPython/venv/lib/python3.11/site-packages/imageio_ffmpeg/binaries',要特别注意这里,找的名称是根据平台的,{'osx64': 'ffmpeg-osx64-v4.2.2', 'win32': 'ffmpeg-win32-v4.2.2.exe', 'win64': 'ffmpeg-win64-v4.2.2.exe', 'linux64': 'ffmpeg-linux64-v4.2.2', 'linuxaarch64': 'ffmpeg-linuxaarch64-v4.2.2'},我的平台是osx64,所以找的文件名称是:ffmpeg-osx64-v4.2.2。可以看出imageio_ffmpeg是与固定版本的ffmpeg-4.2.2绑定的,所以可以尝试不同的imageio_ffmpeg版本来使用不同的ffmpeg,也可以单独下载放到它指定的目录。
  8. 如果还是没找到,继续找 exe = os.path.join(sys.prefix, "bin", "ffmpeg"),其中我用的是venv,所以sys.prefix也是/Users/xx/../ElectronPython/venv。
  9. 如果连上面的还没找到,exe = "ffmpeg",使用系统的,Try system ffmpeg command。

MoviePy(imageio_ffmpeg)获取平台名称的代码如下:

py
def get_platform():
    bits = struct.calcsize("P") * 8
    if sys.platform.startswith("linux"):
        architecture = platform.machine()
        if architecture == "aarch64":
            return "linuxaarch64"
        return "linux{}".format(bits)
    elif sys.platform.startswith("freebsd"):
        return "freebsd{}".format(bits)
    elif sys.platform.startswith("win"):
        return "win{}".format(bits)
    elif sys.platform.startswith("cygwin"):
        return "win{}".format(bits)
    elif sys.platform.startswith("darwin"):
        return "osx{}".format(bits)
    else:  # pragma: no cover
        return None
def get_platform():
    bits = struct.calcsize("P") * 8
    if sys.platform.startswith("linux"):
        architecture = platform.machine()
        if architecture == "aarch64":
            return "linuxaarch64"
        return "linux{}".format(bits)
    elif sys.platform.startswith("freebsd"):
        return "freebsd{}".format(bits)
    elif sys.platform.startswith("win"):
        return "win{}".format(bits)
    elif sys.platform.startswith("cygwin"):
        return "win{}".format(bits)
    elif sys.platform.startswith("darwin"):
        return "osx{}".format(bits)
    else:  # pragma: no cover
        return None

既然知道了上面的查找顺序和原理,那么我们就可以自定义MoviePy使用的FFmpeg路径了,但要注意,os.environ一定要设置在MoviePy的import之前。

py
os.environ["IMAGEIO_FFMPEG_EXE"] = "/Users/alien/Desktop/ffmpeg-osx64-v4.2.2"
from moviepy.video.io.VideoFileClip import VideoFileClip
os.environ["IMAGEIO_FFMPEG_EXE"] = "/Users/alien/Desktop/ffmpeg-osx64-v4.2.2"
from moviepy.video.io.VideoFileClip import VideoFileClip

Pycharm上面创建的venv和命令行创建的竟然不一致

这也是一个很奇怪的现象,使用Pycharm创建的venv,使用pip install -r requirements.txt没有自动在对于的site-packages下载so库,而命令行则正常下载了ffmpeg-osx64-v4.2.2,暂不知原因。

音频临时文件导致的问题,坑爹中的坑爹

知道了上面的原理,我以为问题应该可以解决了,但事实是,Fucking!以为一定是ffmpeg没有导入正确的版本问题,在错误的排查路上越走越远。。。万马奔腾...

不断尝试了三天都没有解决,无奈几乎要放弃,只能再次查看源码。因为打包后问题的调试只能通过写文件,所以需要一些技巧去把错误信息打印出来。

Pycharm中运行正常,进入打包后的终端(这里指的是:main_tinker.app/Contents/MacOS/main_tinker)也正常,偏偏双击运行图标的不正常,见鬼!! 几乎搜遍了整个Google可以查到该错误的问答,以及Github的issue都没有找到真正的解决答案,也正常,因为在使用MoviePy来实现高效脚本化的剪辑的时候,整个Github这么深入的项目都寥寥无几。

近乎要放弃的时候,再借助一次强大的GPT4吧,于是花了20美金购买了GPT4来寻求一些线索,遗憾的是,也许是这方面资料比较匮乏,GPT4提供的一个正确方向的指引。

最后,继续缩小调试范围结合调试源码来猜测这个坑的源头吧,注意排查的方向一定不能错,一个方向一直没有结果,极大可能是研究的方向偏了。

把这个坑爹的错误贴出来,让各位看看眼:

python
Traceback (most recent call last):
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 74, in write_frames
BrokenPipeError: [Errno 32] Broken pipe

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "main_tinker.py", line 70, in cut_video
  File "<decorator-gen-55>", line 2, in write_videofile
  File "moviepy/decorators.py", line 54, in requires_duration
  File "<decorator-gen-54>", line 2, in write_videofile
  File "moviepy/decorators.py", line 135, in use_clip_fps_by_default
  File "<decorator-gen-53>", line 2, in write_videofile
  File "moviepy/decorators.py", line 22, in convert_masks_to_RGB
  File "moviepy/video/VideoClip.py", line 293, in write_videofile
  File "<decorator-gen-45>", line 2, in write_audiofile
  File "moviepy/decorators.py", line 54, in requires_duration
  File "moviepy/audio/AudioClip.py", line 206, in write_audiofile
  File "<decorator-gen-9>", line 2, in ffmpeg_audiowrite
  File "moviepy/decorators.py", line 54, in requires_duration
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 170, in ffmpeg_audiowrite
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 117, in write_frames
OSError: [Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file fuckTEMP_MPY_wvf_snd.mp3:

b'fuckTEMP_MPY_wvf_snd.mp3: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).
Traceback (most recent call last):
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 74, in write_frames
BrokenPipeError: [Errno 32] Broken pipe

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "main_tinker.py", line 70, in cut_video
  File "<decorator-gen-55>", line 2, in write_videofile
  File "moviepy/decorators.py", line 54, in requires_duration
  File "<decorator-gen-54>", line 2, in write_videofile
  File "moviepy/decorators.py", line 135, in use_clip_fps_by_default
  File "<decorator-gen-53>", line 2, in write_videofile
  File "moviepy/decorators.py", line 22, in convert_masks_to_RGB
  File "moviepy/video/VideoClip.py", line 293, in write_videofile
  File "<decorator-gen-45>", line 2, in write_audiofile
  File "moviepy/decorators.py", line 54, in requires_duration
  File "moviepy/audio/AudioClip.py", line 206, in write_audiofile
  File "<decorator-gen-9>", line 2, in ffmpeg_audiowrite
  File "moviepy/decorators.py", line 54, in requires_duration
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 170, in ffmpeg_audiowrite
  File "moviepy/audio/io/ffmpeg_audiowriter.py", line 117, in write_frames
OSError: [Errno 32] Broken pipe

MoviePy error: FFMPEG encountered the following error while writing file fuckTEMP_MPY_wvf_snd.mp3:

b'fuckTEMP_MPY_wvf_snd.mp3: Read-only file system\n'

In case it helps, make sure you are using a recent version of FFMPEG (the versions in the Ubuntu/Debian repos are deprecated).

这里写视频的时候多出来一个临时的mp3文件,然后报错源是:ffmpeg_audiowriter.py

音频的写出:

py
class FFMPEG_AudioWriter:

    def __init__(self, filename, fps_input, nbytes=2,
                 nchannels=2, codec='libfdk_aac', bitrate=None,
                 input_video=None, logfile=None, ffmpeg_params=None):

        self.filename = filename
        self.codec = codec

        if logfile is None:
            logfile = sp.PIPE

        cmd = ([get_setting("FFMPEG_BINARY"), '-y',
                "-loglevel", "error" if logfile == sp.PIPE else "info",
                "-f", 's%dle' % (8*nbytes),
                "-acodec",'pcm_s%dle' % (8*nbytes),
                '-ar', "%d" % fps_input,
                '-ac', "%d" % nchannels,
                '-i', '-']
               + (['-vn'] if input_video is None else ["-i", input_video, '-vcodec', 'copy'])
               + ['-acodec', codec]
               + ['-ar', "%d" % fps_input]
               + ['-strict', '-2']  # needed to support codec 'aac'
               + (['-ab', bitrate] if (bitrate is not None) else [])
               + (ffmpeg_params if ffmpeg_params else [])
               + [filename])
class FFMPEG_AudioWriter:

    def __init__(self, filename, fps_input, nbytes=2,
                 nchannels=2, codec='libfdk_aac', bitrate=None,
                 input_video=None, logfile=None, ffmpeg_params=None):

        self.filename = filename
        self.codec = codec

        if logfile is None:
            logfile = sp.PIPE

        cmd = ([get_setting("FFMPEG_BINARY"), '-y',
                "-loglevel", "error" if logfile == sp.PIPE else "info",
                "-f", 's%dle' % (8*nbytes),
                "-acodec",'pcm_s%dle' % (8*nbytes),
                '-ar', "%d" % fps_input,
                '-ac', "%d" % nchannels,
                '-i', '-']
               + (['-vn'] if input_video is None else ["-i", input_video, '-vcodec', 'copy'])
               + ['-acodec', codec]
               + ['-ar', "%d" % fps_input]
               + ['-strict', '-2']  # needed to support codec 'aac'
               + (['-ab', bitrate] if (bitrate is not None) else [])
               + (ffmpeg_params if ffmpeg_params else [])
               + [filename])

视频的写出:

python
class FFMPEG_VideoWriter:
    def __init__(self, filename, size, fps, codec="libx264", audiofile=None,
                 preset="medium", bitrate=None, withmask=False,
                 logfile=None, threads=None, ffmpeg_params=None):

        if logfile is None:
            logfile = sp.PIPE

        self.filename = filename
        self.codec = codec
        self.ext = self.filename.split(".")[-1]

        # order is important
        cmd = [
            get_setting("FFMPEG_BINARY"),
            '-y',
            '-loglevel', 'error' if logfile == sp.PIPE else 'info',
            '-f', 'rawvideo',
            '-vcodec', 'rawvideo',
            '-s', '%dx%d' % (size[0], size[1]),
            '-pix_fmt', 'rgba' if withmask else 'rgb24',
            '-r', '%.02f' % fps,
            '-an', '-i', '-'
        ]
        if audiofile is not None:
            cmd.extend([
                '-i', audiofile,
                '-acodec', 'copy'
            ])
        cmd.extend([
            '-vcodec', codec,
            '-preset', preset,
        ])
        if ffmpeg_params is not None:
            cmd.extend(ffmpeg_params)
        if bitrate is not None:
            cmd.extend([
                '-b', bitrate
            ])
class FFMPEG_VideoWriter:
    def __init__(self, filename, size, fps, codec="libx264", audiofile=None,
                 preset="medium", bitrate=None, withmask=False,
                 logfile=None, threads=None, ffmpeg_params=None):

        if logfile is None:
            logfile = sp.PIPE

        self.filename = filename
        self.codec = codec
        self.ext = self.filename.split(".")[-1]

        # order is important
        cmd = [
            get_setting("FFMPEG_BINARY"),
            '-y',
            '-loglevel', 'error' if logfile == sp.PIPE else 'info',
            '-f', 'rawvideo',
            '-vcodec', 'rawvideo',
            '-s', '%dx%d' % (size[0], size[1]),
            '-pix_fmt', 'rgba' if withmask else 'rgb24',
            '-r', '%.02f' % fps,
            '-an', '-i', '-'
        ]
        if audiofile is not None:
            cmd.extend([
                '-i', audiofile,
                '-acodec', 'copy'
            ])
        cmd.extend([
            '-vcodec', codec,
            '-preset', preset,
        ])
        if ffmpeg_params is not None:
            cmd.extend(ffmpeg_params)
        if bitrate is not None:
            cmd.extend([
                '-b', bitrate
            ])

上面两个文件只关注 self.filename = filename即可,均是路径信息,不同的是FFMPEG_VideoWriter是我们自己传递的绝对路径,FFMPEG_AudioWriter是MoviePy自动创建的相对路径,按正常来讲,绝对路径和相对路径都是没有问题的,正常运行都可以生成视频输出。But,它确实是这里不正常,最后猜测应该是双击运行的方式不允许在当前目录创建文件吧,那么也许换一个音频临时路径应该就可以了,注意:一定要加文件后缀,最后验证一下,果然可以正常生成视频了,万马奔腾~

python
def cut_video(self):
    try:
        finalClip = VideoFileClip(PathUtils.get_desk_top("temp.mp4")).subclip(0, 5)
        audio_path = "/tmp/temp_audio.wav"
        finalClip.write_videofile(PathUtils.get_desk_top("dist.mp4"), temp_audiofile=audio_path, remove_temp=True, logger=None, threads=50)
    except Exception as e:
        # 打印并写入文件
        error_message = traceback.format_exc()
        open(PathUtils.get_desk_top("错误日志.md"), "w").write(error_message)
def cut_video(self):
    try:
        finalClip = VideoFileClip(PathUtils.get_desk_top("temp.mp4")).subclip(0, 5)
        audio_path = "/tmp/temp_audio.wav"
        finalClip.write_videofile(PathUtils.get_desk_top("dist.mp4"), temp_audiofile=audio_path, remove_temp=True, logger=None, threads=50)
    except Exception as e:
        # 打印并写入文件
        error_message = traceback.format_exc()
        open(PathUtils.get_desk_top("错误日志.md"), "w").write(error_message)

参考

pyinstaller 打包Python项目pyinstaller简洁教程疑难错误官方解决方案MoviePy的各种坑