前言
请慎入,pyinstaller的坑多的令人发指,它会自动打包代码中相关的依赖,类似于Android的gradle,但也会出现类似于Android中动态加载,反射,so库未打入导致运行中异常的情况。
pyinstaller打包
pyinstaller不仅可以打包成exe,还可以打包成app,linux下的可执行文件,mac下的可执行文件。 同样PyQT也是使用pyinstaller打包的,pyinstaller很智能,可以在同一个项目中根据入口main.py的引用来打相关依赖,所以包体积跟着依赖项而有所不同,完全不需要建立不同的工程项目,就可以打不同的小工具。
Python打包EXE程序
pip3 install pyinstaller
我们来将这个.py的文件打包成一个exe,我们直接cmd切换到这个脚本的目录,执行命令:pyinstaller-F setup.py ps: -F参数表示覆盖打包,这样在打包时,不管我们打包几次,都是最新的,这个记住就行,固定命令。
不显示控制台,并设置图标,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 表示可执行文件的图标
2
3
4
问题一
pyinstaller test.py 出现 failed to create process
解决方法是找到pyinstaller-script.py,把第一行的路径用引号括起来,最终是下面这个样子就对了
#!"c:\program files (x86)\python35-32\python.exe"
问题二
这个错误是自己大意,因为刚从python36换成python35,没有安装requests模块,所以报的错。
最终命令:
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
直接看这里,打包很简单
打包方式只有两种:
- pyinstaller -D --add-data 'config_debug.yaml:.' --add-data 'config.yaml:.' --hidden-import=main main.py
- pyinstaller main.spec(第一种方式会生成main.spec文件,那么后续可以直接根据这个配置文件来打包)
注意事项:
- 只能在MAC系统打MAC包,在windows系统打windows包,不能一个平台打包另一个平台的包。
- window建议使用Python3.6.8版本
使用教程
安装
pip install pyinstaller
打包
方式一:pyinstall -D test.py(直接生成一个dist包【包含了多个文件】,这种包执行速度比单个bundle的包要快一些) 方式二:pyinstall -F test.py(生成一个单个的可执行文件,这种包执行速度比多个dist包要慢一些)
文件如何引用不容易出错?
- 首先要设置一个项目的根路径,其他路径都是相对这个路径下的,否则不好维护。
- 如果是多文件,使用经常用的os.path.dirname(os.path.abspath(file)) 获取当前文件的绝对路径,然后再拼接上相对路径。
- 如果是单文件,实际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__))
2
3
4
那么一个大型项目,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
参数来设置程序的图标。具体操作如下:
准备一个
.ico
格式的图标文件,例如myicon.ico
。在命令行中,使用以下命令来打包程序:
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
即可。具体操作如下:
准备一个
.ico
格式的图标文件,例如myicon.ico
。在命令行中,使用以下命令来打包程序:
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程序的步骤:
首先,你需要安装PyInstaller。在命令行中输入以下命令安装PyInstaller:
pip install pyinstaller
pip install pyinstaller
1打包你的程序。在命令行中,切换到你的程序所在的目录,然后输入以下命令:
pyinstaller --add-data 'gui:gui' your_program.py
pyinstaller --add-data 'gui:gui' your_program.py
1这里,
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
1等待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
终极命令(尽量不要用单文件,多文件的包效果更好一点)
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批量导入的骚写法。
解决方法:
- pyinstaller命令中增加参数或者编写spec文件来给出额外信息(不友好,弃用)
- 使用hook,实现动态替换,hook指的是在不改变原库的基础上,把动态加载映射为我们正常开发中用到的import的导入方式
pyinstaller文档中给出了hook的开发细节,不过,pyinstaller的社区已经将一些知名库的hook都开发好了,当你安装好pyinstaller时,相应的hook库其实也安装好了,叫pyinstaller-hooks-contrib。
opencv-python和MoviePy都用到了ffmpeg.dll或ffmpeg.so,需要观察这个库是否被自动打进来,如果没有,打包时就需要指定要打包它的位置。
MoviePy打包
官方作者反馈:属于动态导包问题
如何打印MoviePy中所有的隐藏包,不需要修改源码,idea可以直接修改源代码。
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
2
文件:/ElectronPython/venv/lib/python3.9/site-packages/moviepy/video/fx/all/__init__.py
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))
2
3
4
5
6
7
8
9
10
注释掉:
for name in __all__:
exec("from ..%s import %s" % (name, name))
for name in __all__:
exec("from ..%s import %s" % (name, name))
2
替换import方式的正常导入,完整代码如下:
"""
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
INFO: UPX is not available.
这个也不算错误,UPX是一个压缩工具,可以将可执行文件压缩,减小体积,但是这个工具只能在Windows下使用,所以在Linux下是无法使用的,所以会提示这个警告,不影响使用。
pyinstaller打包后运行报错
pycharm运行正常,进入包内执行也正常,但是单独双击就闪退!!!
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MoviePy使用的是imageio_ffmpeg中的ffmpeg,留意MoviePy在上述ffmpeg是否安装的查找过程。
一直无法排查错误:
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))
2
3
4
5
6
或者:
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()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
看到报错:
[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).
2
3
4
5
6
7
无法写入文件(奇奇怪怪-Fuck)
[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).
2
3
4
5
6
7
conda install -c conda-forge ffmpeg
上面的错误是由于FFmpeg版本问题,默认MoviePy使用的FFmpeg是 venvs/lib/python3.9/site-packages/imageio_ffmpeg/binaries/ffmpeg-osx64-v4.2.2
下的ffmpeg,写合成视频的时候,竟然不支持音频导出。
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"))
2
设置audio=False
后,程序运行正常。另外vClip.write_videofile("dist.mp4")
,注意这里是不能使用相对路径的,否则也会出现/dist
之类无权写入的目录。
注意打包环境
在venv中打包,py2app和pyinstaller都这样比较好,在anaconda环境打包,包体积非常大。比如一个20M,一个120M。
注意打包缓存问题
sudo rm -rf build dist
清除旧的目录,防止对新一次的打包产生影响,Android经常出现这种坑爹的坑。
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')],
...
2
3
4
5
6
- pathex: 这个选项用于指定 PyInstaller 在分析代码时应该考虑的额外路径。通常情况下,PyInstaller 会自动搜索 Python 脚本中导入的模块的位置。但在某些情况下,如果模块位于非标准路径,或者由于某种原因 PyInstaller 未能自动找到它们,就需要手动指定这些路径。
- binaries: 这个选项用于明确指定应该包含在可执行文件中的二进制文件。这些通常是 DLL 或者 SO 文件,对于应用程序的运行至关重要,但不是 Python 模块。比如上面的配置指的是将
opencv_videoio_ffmpeg.so
这个 DLL 文件包含在打包后的应用程序中,并且将其放在根目录(由 "." 指定)。这通常用于确保应用程序能够访问必要的依赖,尤其是那些 PyInstaller 可能无法自动检测到的依赖。
总的来说,pathex 用于添加额外的搜索路径以便找到 Python 模块,而 binaries 用于确保必要的非 Python 文件(如动态链接库)被包含在最终的打包文件中。
打包信息出现14799 INFO: EXE target arch: x86_64
先确定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())
2
3
方式二:
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的包,如下:
from moviepy.video.io.VideoFileClip import VideoFileClip
from moviepy.video.io.VideoFileClip import VideoFileClip
上面的导入间接引用了下面的代码,也就是因为MoviePy的导入,导致启动非常慢,所以尽量把导入延迟到点击事件里面。
- from moviepy.audio.io.AudioFileClip import AudioFileClip
- from moviepy.audio.AudioClip import AudioClip
- from moviepy.audio.io.ffmpeg_audiowriter import ffmpeg_audiowrite
- from moviepy.config import get_setting
- ElectronPython/venv/lib/python3.11/site-packages/imageio -> imageio_ffmpeg.get_ffmpeg_exe()
- exe = os.getenv("IMAGEIO_FFMPEG_EXE", None)
- 假如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,也可以单独下载放到它指定的目录。
- 如果还是没找到,继续找 exe = os.path.join(sys.prefix, "bin", "ffmpeg"),其中我用的是venv,所以sys.prefix也是/Users/xx/../ElectronPython/venv。
- 如果连上面的还没找到,exe = "ffmpeg",使用系统的,Try system ffmpeg command。
MoviePy(imageio_ffmpeg)获取平台名称的代码如下:
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
既然知道了上面的查找顺序和原理,那么我们就可以自定义MoviePy使用的FFmpeg路径了,但要注意,os.environ一定要设置在MoviePy的import之前。
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
2
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提供的一个正确方向的指引。
最后,继续缩小调试范围结合调试源码来猜测这个坑的源头吧,注意排查的方向一定不能错,一个方向一直没有结果,极大可能是研究的方向偏了。
把这个坑爹的错误贴出来,让各位看看眼:
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).
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这里写视频的时候多出来一个临时的mp3文件,然后报错源是:ffmpeg_audiowriter.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])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
视频的写出:
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
])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
上面两个文件只关注 self.filename = filename
即可,均是路径信息,不同的是FFMPEG_VideoWriter是我们自己传递的绝对路径,FFMPEG_AudioWriter是MoviePy自动创建的相对路径,按正常来讲,绝对路径和相对路径都是没有问题的,正常运行都可以生成视频输出。But,它确实是这里不正常,最后猜测应该是双击运行的方式不允许在当前目录创建文件吧,那么也许换一个音频临时路径应该就可以了,注意:一定要加文件后缀,最后验证一下,果然可以正常生成视频了,万马奔腾~
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)
2
3
4
5
6
7
8
9