ffmpeg+Python實現B站MP4格式音訊與視訊的合併
阿新 • • 發佈:2020-10-21
[TOC]
## 安裝
### 官網下載
[http://ffmpeg.org/](http://ffmpeg.org/)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018044335429-2025314163.png)
選擇需要的版本
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018044522520-1000417137.png)
在這個網址下載ffmpeg,[https://github.com/BtbN/FFmpeg-Builds/releases](https://github.com/BtbN/FFmpeg-Builds/releases)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018044641808-1543360376.png)
將解壓後得到的以下幾個檔案放置在`E:\FFmpeg`下
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018044954452-2100720085.png)
### 環境變數
此電腦--屬性--高階系統設定--環境變數
在系統變數(也就是下面那一半)處找到新建,按如下所示的方法填寫
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018045320020-615174788.png)
再將`%FFMPEG_HOME%`以及`%FFMPEG_HOME%\bin`寫入系統變數的Path中
然後一路確定即可
### 驗證
win+R,cmd
輸入`ffmpeg -version`
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018050417794-2001088960.png)
## ffmpeg的使用
對於我將B站PC端快取的音訊mp4和視訊mp4檔案合併的需求,需要用到的命令為:
`ffmpeg.exe -i audio1.mp4 -i video.mp4 -acodec copy -vcodec copy output.mp4`
可以把mp4的檔案設定成絕對路徑,這樣就可以轉換指定路徑的檔案以及儲存到指定路徑了,比如這樣:
`ffmpeg.exe -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\audio1.mp4" -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\video.mp4" -acodec copy -vcodec copy "E:\B站匯出視訊\Dr.STONE石紀元\第22話寶物.mp4`
通過PC端快取的未合併的視訊和音訊,全都是命名為video.mp4和audio.mp4
PS:有些兄弟是匯出的手機端快取的視訊和音訊,是m4s格式的,方法也一樣
但光有這條命令還不夠,需要自己手動一個個操作,太麻煩了
因此我還需要使用Python來自動幫我完成工作
## Python實現自動處理
雖然Python可以實現自動化,減少時間的浪費,但最快的還是以後記得快取時勾選自動合併
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018151328117-447371319.png)
### 檔案結構
PC端快取的視訊儲存的檔案結構有很多種,我只是根據我碰到的情況寫的,但大同小異,修改起來也不麻煩,只是再加個if和else罷了
#### 番劇快取結構
快取的番劇是這種結構:
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018143049293-1083768195.png)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018142824089-1612987794.png)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018142932266-347656640.png)
上面舉的例子是Dr.stone石紀元,我快取的鬼滅之刃也是如此
特點是在一個以視訊ID號名稱的資料夾(ss27993)後,跟著許多子資料夾(57983089等),然後在這些子資料夾中又有一個或多個子資料夾(比如1),然後快取的視訊儲存在這個資料夾裡,裡面有一個info檔案(就是json格式),還有audio1.mp4和video.mp4。
PS:
還有一個xml檔案,是彈幕資訊,暫時我不知道怎麼處理
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018143927122-920874731.png)
#### 常規快取結構
除了番劇,一般的視訊快取的結構是這樣的
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018144310449-1791799305.png)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018144349252-520065332.png)
不難看出,這比番劇要少一個層級
#### 檔案資訊
檔案資訊主要由info檔案和dvi檔案來記錄
這兩種檔案都可以直接以json檔案來處理,也就是,首先open函式開啟檔案,然後用json.load轉成字典。。。
然後我還發現了一個特點是,視訊和音訊所在的目錄下是info檔案,而它的上一層目錄下是dvi檔案
雖然檔案格式基本是一致的,但是裡面的鍵-值關係卻不一致,單集視訊的名稱在番劇中對應的是鍵`Description`,在其他視訊中對應的是`PartName`
視訊總的名稱儲存在外層的目錄下的info檔案或dvi檔案中,番劇中對應的鍵是`SeasonTitle`,在其他視訊中對應的是`Title`
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018150258872-1344462364.png)
具體問題具體分析,首先由於我實踐得較少,這樣的總結不一定對,然後以後也許也會有新的格式、新的變化
## 程式碼
### 具體程式碼
以下是我的Python程式碼,你可以先試試能不能用,用不了的話,可以在理解的基礎上修改。理解不了的話可以看我的後面的解釋,以及程式碼中的註釋,對於執行過程中一些變數的值,我都把它放在註釋中了,方便你理解。
```
# -*- coding = utf-8 -*-
# @time:2020/10/17/017 23:09
# Author:cyx
# @File:main.py.py
# @Software:PyCharm
# 從.info檔案中獲得了Title資訊,但是如果其中有某些特殊字元,儲存時可能出現問題
def get_correct_title(title):
error_set = ['/', '\\', ':', '*', '?', '"', '|', '<', '>', '\b', ' ', '.']
correct_title = title
# print(title)
for c in correct_title:
if c in error_set:
correct_title = correct_title.replace(c, '')
return correct_title
def popen(cmd):
# https://blog.csdn.net/qq_41451161/article/details/82901235
try:
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE)
popen.wait()
lines = popen.stdout.readlines()
return [line.decode('gbk') for line in lines]
except BaseException as e:
return -1
if __name__ == '__main__':
import os
import json
import subprocess
# ffmpeg -i video.m4s -i audio.m4s -c:v copy -c:a aac -strict experimental output.mp4
# ffmpeg.exe -i audio1.mp4 -i video.mp4 -acodec copy -vcodec copy output.mp4
AVhao = input("請輸入視訊AV號:")
superPath = "E:\\嗶哩嗶哩視訊" + "\\" + AVhao
partDirs = [] # 儲存每P視訊所在的資料夾路徑
paths = os.listdir(superPath) # 獲取當前路徑下所有的檔案(包括資料夾)名稱
# paths = ['8','9']
# 有時候,會莫名其妙的少了幾個視訊,可以通過過載來重新載入缺失的視訊
# print(paths)
# paths
# ['27993.info', '57983089', '58612211', '59811008', '60862133', '61898240', '62925012', '64005445', '65020725', '66013155', '66808912', '67587875', '68398229', '69175748', '70021307', '70873680', '71617211', '73379440', '74051851', '74974157', '75746600', '76619409', '77413703', '78266594', '79070874', 'cover.jpg', 'desktop.ini']
# 獲取每P視訊所在的資料夾路徑
savePos = ''
seq = []
# 鑑於有些up主命名時毫無規律,匯出後無法正常排序,只能手動排序了
# 根據AV號名資料夾下的子資料夾的名稱進行排序,但是番劇的話不是這樣排序,不過番劇單集的名稱很規範,不需要這樣
for p in paths:
if '.' not in p:
seq.append(p)
if 'info' in p:
# print(p)
# p:
# 27993.info
info = superPath + "\\" + p
with open(info, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
projectTitle = load_dict['SeasonTitle']
projectTitle = get_correct_title(projectTitle)
savePos = 'E:\\B站匯出視訊\\' + projectTitle
print('savePos: ', savePos)
if 'dvi' in p:
# print(p)
# P:
# 328738595.dvi
dvi = superPath + "\\" + p
with open(dvi, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
projectTitle = load_dict['Title']
projectTitle = get_correct_title(projectTitle)
savePos = 'E:\\B站匯出視訊\\' + projectTitle
print('savePos: ', savePos)
# 防止檔案存在時再次生成該資料夾出現錯誤
try:
os.mkdir(savePos)
break
except:
pass
subDir = superPath + "\\" + p
if os.path.isdir(subDir):
# print(subDir)
# 所有子資料夾的路徑儲存在partDirs中
partDirs.append(subDir)
# print("partDirs: ",partDirs)
# partDirs: ['E:\\嗶哩嗶哩視訊\\ss27993\\57983089', 'E:\\嗶哩嗶哩視訊\\ss27993\\58612211', 'E:\\嗶哩嗶哩視訊\\ss27993\\59811008', 'E:\\嗶哩嗶哩視訊\\ss27993\\60862133', 'E:\\嗶哩嗶哩視訊\\ss27993\\61898240', 'E:\\嗶哩嗶哩視訊\\ss27993\\62925012', 'E:\\嗶哩嗶哩視訊\\ss27993\\64005445', 'E:\\嗶哩嗶哩視訊\\ss27993\\65020725', 'E:\\嗶哩嗶哩視訊\\ss27993\\66013155', 'E:\\嗶哩嗶哩視訊\\ss27993\\66808912', 'E:\\嗶哩嗶哩視訊\\ss27993\\67587875', 'E:\\嗶哩嗶哩視訊\\ss27993\\68398229', 'E:\\嗶哩嗶哩視訊\\ss27993\\69175748', 'E:\\嗶哩嗶哩視訊\\ss27993\\70021307', 'E:\\嗶哩嗶哩視訊\\ss27993\\70873680', 'E:\\嗶哩嗶哩視訊\\ss27993\\71617211', 'E:\\嗶哩嗶哩視訊\\ss27993\\73379440', 'E:\\嗶哩嗶哩視訊\\ss27993\\74051851', 'E:\\嗶哩嗶哩視訊\\ss27993\\74974157', 'E:\\嗶哩嗶哩視訊\\ss27993\\75746600', 'E:\\嗶哩嗶哩視訊\\ss27993\\76619409', 'E:\\嗶哩嗶哩視訊\\ss27993\\77413703', 'E:\\嗶哩嗶哩視訊\\ss27993\\78266594', 'E:\\嗶哩嗶哩視訊\\ss27993\\79070874']
videoPos = ''
i = 0
for p in partDirs:
# print(p)
# 列出子資料夾中的所有檔案
sublist = os.listdir(p)
# 檢查info檔案是否在當前子資料夾中
for file in sublist:
# print(file)
# file:
# 1
# 57983089.
# dvi
# cover.jpg
# desktop.ini
if 'info' in file:
infoPos = p + "\\" + file
videoPos = p
else:
subsubDir = p + "\\" + file
if os.path.isdir(subsubDir):
# print(subsubDir)
# subsubDir: E:\嗶哩嗶哩視訊\ss27993\57983089\1
subsubList = os.listdir(subsubDir)
for subsubFile in subsubList:
if 'info' in subsubFile:
infoPos = subsubDir + "\\" + subsubFile
videoPos = subsubDir
break
with open(infoPos, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
if 'ss' in AVhao:
videoTitle = load_dict['Description']
else:
videoTitle = load_dict['PartName']
videoTitle = get_correct_title(videoTitle)
print('videoTitle: ', videoTitle)
videoDir = videoPos + "\\" + 'video.mp4'
audioDir = videoPos + "\\" + 'audio1.mp4'
# print('videoDir: ', videoDir)
# print('audioDir: ', audioDir)
# videoDir: E:\嗶哩嗶哩視訊\ss27993\74051851\1\video.mp4
# audioDir: E:\嗶哩嗶哩視訊\ss27993\74051851\1\audio1.mp4
if 'ss' in AVhao:
outDir = savePos + "\\" + videoTitle + '.mp4'
else:
outDir = savePos + "\\" + seq[i] + '_' + videoTitle + '.mp4'
i += 1
# 對於那些命名很規範的視訊,可以不用自己再排序,進行一下過載,不規範的視訊再把這句註釋掉就好
outDir = savePos + "\\" + videoTitle + '.mp4'
# command = 'cd ' + superPath + '\\64 && ' # && 多名命令
# command = 'cd ' + 'E:\\ProgramFiles\\ffmpeg' + ' && '
command = 'E:\\FFmpeg\\bin\\ffmpeg.exe -i ' + '"' + audioDir + '"' ' -i ' + '"' + videoDir + '"'+ ' -acodec copy -vcodec copy ' + '"' + outDir + '"'
# print("儲存地址",outDir)
# 儲存地址 E:\B站匯出視訊\[Lynda視訊]音訊錄製錄音技巧教程(中英雙語字幕)全集130課時AudioRecordingTechniques混音錄音棚音樂工作室歌曲調音\98錄製獨奏薩克斯演奏技巧二.mp4
# print(command)
# command = 'E:\\FFmpeg\\bin\\ffmpeg.exe -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\audio1.mp4" -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\video.mp4" -acodec copy -vcodec copy "E:\B站匯出視訊\Dr.STONE石紀元\第22話寶物.mp4'
# os.system(command)
popen(command)
# ffmpeg.exe -i audio1.mp4 -i video.mp4 -acodec copy -vcodec copy output.mp4
break
```
### 程式碼說明
如你所見我的程式設計水平不高,模組化做的很差,不便於理解,所以有必要進行說明。
直接從main開始看起吧。
```
AVhao = input("請輸入視訊AV號:")
superPath = "E:\\嗶哩嗶哩視訊" + "\\" + AVhao
partDirs = [] # 儲存每P視訊所在的資料夾路徑
paths = os.listdir(superPath) # 獲取當前路徑下所有的檔案(包括資料夾)名稱
```
首先是用input接收AV號或BV號的輸入,放入AVhao變數中。
然後用superPath變數存放你需要合併的視訊的根目錄。比如我在B站快取的所有視訊存放在`E:\嗶哩嗶哩視訊`下,注意程式中要有兩條\,然後superPath就是`E:\嗶哩嗶哩視訊\AVhao`。
os.listdir(),括號中的引數必須是一個真實的路徑,這個函式可以得到這個路徑下所有的檔案和資料夾的名稱。
我用paths來存放`E:\嗶哩嗶哩視訊\AVhao`路徑下所有的檔名稱和資料夾名稱。
為了防止我的表達能力有限帶來的理解上的不便,你可以看圖,paths對應的是下圖中的內容:
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201018143049293-1083768195.png)
這個變數的型別是列表,所以可以用for迴圈來遍歷。
```
savePos = ''
seq = []
for p in paths:
if '.' not in p:
seq.append(p)
if 'info' in p:
# print(p)
# p:
# 27993.info
info = superPath + "\\" + p
with open(info, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
projectTitle = load_dict['SeasonTitle']
projectTitle = get_correct_title(projectTitle)
savePos = 'E:\\B站匯出視訊\\' + projectTitle
print('savePos: ', savePos)
if 'dvi' in p:
# print(p)
# P:
# 328738595.dvi
dvi = superPath + "\\" + p
with open(dvi, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
projectTitle = load_dict['Title']
projectTitle = get_correct_title(projectTitle)
savePos = 'E:\\B站匯出視訊\\' + projectTitle
print('savePos: ', savePos)
# 防止檔案存在時再次生成該資料夾出現錯誤
try:
os.mkdir(savePos)
break
except:
pass
subDir = superPath + "\\" + p
if os.path.isdir(subDir):
# print(subDir)
# 所有子資料夾的路徑儲存在partDirs中
partDirs.append(subDir)
```
我設定了一個savePos變數,用來表示合併後的視訊儲存的位置,因為我希望將視訊儲存在一個我自己指定的資料夾下,同時這個資料夾的名稱是這個視訊的名稱,比如Dr.stone石紀元。
因為想自動化操作,所以我通過快取資料夾中的info檔案和dvi檔案來找到視訊的名稱。
使用open函式開啟這兩個檔案中的一個,因為不確定檔案結構是什麼樣的,所以我用了兩個if語句。
然後再用json.load函式將其載入為字典,並根據對應的鍵讀取對應的值,從而可以拼接處對應的儲存地址savePos。
由於建立已經存在的同名資料夾會發生錯誤,為避免這種可能,我將建立目錄的操作放在了try語句下。
建立目錄用的是os.mkdir()函式,括號中是一個絕對路徑。
然後我用subDir來表示子資料夾的名稱,注意!是子資料夾,而不是檔案。
我用os.path.isdir(subDir)來進行判斷,如果是資料夾而不是檔案的話,就加到partDirs列表中,partDirs.append(subDir)
這個列表中的每個元素都是一個子資料夾的絕對路徑,比如:E:\\嗶哩嗶哩視訊\\ss27993\\57983089
而這個名為seq的顯得很突兀,這個其實我也是後來加的,這個列表的作用在於記錄當前子資料夾的名稱,也就是在AVhao資料夾的下一層,如果不是番劇的話,應當有許多個資料夾分別是1、2、3等等,這些其實對應的是播放列表中的順序。
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201020233114685-1872505221.png)
![](https://img2020.cnblogs.com/blog/1776217/202010/1776217-20201021001921251-489830021.png)
而之所以使用`if '.' not in p`,因為這一層的資料夾全都是用來表示播放順序的,因此不存在後綴名,從而也就沒有“.”。
```
videoPos = ''
i = 0
for p in partDirs:
# print(p)
# 列出子資料夾中的所有檔案
sublist = os.listdir(p)
# 檢查info檔案是否在當前子資料夾中
for file in sublist:
# print(file)
# file:
# 1
# 57983089.
# dvi
# cover.jpg
# desktop.ini
if 'info' in file:
infoPos = p + "\\" + file
videoPos = p
else:
subsubDir = p + "\\" + file
if os.path.isdir(subsubDir):
# print(subsubDir)
# subsubDir: E:\嗶哩嗶哩視訊\ss27993\57983089\1
subsubList = os.listdir(subsubDir)
for subsubFile in subsubList:
if 'info' in subsubFile:
infoPos = subsubDir + "\\" + subsubFile
videoPos = subsubDir
break
with open(infoPos, 'r', encoding='utf-8') as load_f:
load_dict = json.load(load_f)
if 'ss' in AVhao:
videoTitle = load_dict['Description']
else:
videoTitle = load_dict['PartName']
videoTitle = get_correct_title(videoTitle)
print('videoTitle: ', videoTitle)
videoDir = videoPos + "\\" + 'video.mp4'
audioDir = videoPos + "\\" + 'audio1.mp4'
# print('videoDir: ', videoDir)
# print('audioDir: ', audioDir)
# videoDir: E:\嗶哩嗶哩視訊\ss27993\74051851\1\video.mp4
# audioDir: E:\嗶哩嗶哩視訊\ss27993\74051851\1\audio1.mp4
if 'ss' in AVhao:
outDir = savePos + "\\" + videoTitle + '.mp4'
else:
outDir = savePos + "\\" + seq[i] + '_' + videoTitle + '.mp4'
i += 1
# 對於那些命名很規範的視訊,可以不用自己再排序,進行一下過載,不規範的視訊再把這句註釋掉就好
outDir = savePos + "\\" + videoTitle + '.mp4'
# command = 'cd ' + superPath + '\\64 && ' # && 多名命令
# command = 'cd ' + 'E:\\ProgramFiles\\ffmpeg' + ' && '
command = 'E:\\FFmpeg\\bin\\ffmpeg.exe -i ' + '"' + audioDir + '"' ' -i ' + '"' + videoDir + '"'+ ' -acodec copy -vcodec copy ' + '"' + outDir + '"'
# print("儲存地址",outDir)
# 儲存地址 E:\B站匯出視訊\[Lynda視訊]音訊錄製錄音技巧教程(中英雙語字幕)全集130課時AudioRecordingTechniques混音錄音棚音樂工作室歌曲調音\98錄製獨奏薩克斯演奏技巧二.mp4
# print(command)
# command = 'E:\\FFmpeg\\bin\\ffmpeg.exe -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\audio1.mp4" -i "E:\嗶哩嗶哩視訊\ss27993\77413703\1\video.mp4" -acodec copy -vcodec copy "E:\B站匯出視訊\Dr.STONE石紀元\第22話寶物.mp4'
# os.system(command)
popen(command)
# ffmpeg.exe -i audio1.mp4 -i video.mp4 -acodec copy -vcodec copy output.mp4
break
```
這一部分是我用來實現合併特定路徑的視訊和音訊,並最終匯出到指定目錄的。
videoPos是待合併的視訊的路徑,音訊檔案也在這一目錄下。
`for p in partDirs:`,在這個for迴圈中遍歷的是partDirs列表,這個列表由前面的步驟得到,其中的每個元素都是一個路徑。
接下來的這個if-else是用來區別番劇和普通視訊合集的,因為它們有不同的目錄結構。
infoPos用來記錄包含單集視訊名稱的info檔案的路徑,然後用open函式開啟這個info檔案,根據AVhao中是否有ss,判斷是否是番劇,如果有ss,則表明是番劇,對應的視訊名稱為Description鍵對應的值。否則的話,對應的視訊名稱為PartName鍵對應的值,但是如果是要儲存為檔案的話,當然不能直接以這個名稱命名,否則極有可能發生錯誤,因此我用了一個get_correct_title函式來對標題進行過載,以確保格式正確。
videoDir和audioDir分別是待合併的視訊和音訊的絕對路徑。
然後我根據AVhao中是否有ss,來判斷是否是番劇。因為番劇的單集視訊名稱通常會有序號,所以我可以將輸出視訊的儲存路徑設定為儲存目錄加視訊名.mp4。
許多up主的視訊名稱沒有體現視訊的先後順序,這樣帶來的問題是匯出後順序播放時產生跳集現象。因此我用seq加下劃線的方式來為視訊排序。比如“1_簡介.mp4”。
而對於視訊名稱本身有排序的情況1_1簡介.mp4,這樣有些奇怪,所以我們碰到這種情況時,可以直接在下面新增一個`outDir = savePos + "\\" + videoTitle + '.mp4'`,其他情況不用時註釋掉就好。
最後是用Python程式執行Dos命令,我將命令設為command變數,通過for迴圈,會自動生成不同的命令,然後執行命令列有兩種方法,一種是匯入os庫,使用os.system(command),另一種是我在`https://blog.csdn.net/qq_41451161/article/details/82901235`借用的popen函式,這個也可以用,但需要匯入subprocess庫。
使用命令列上,有兩個比較坑的地方,一是前面必須給出ffmpeg.exe的絕對路徑,也就是E:\\FFmpeg\\bin\\ffmpeg.exe,在Python中用是這樣的,但直接在命令列中,只要輸入ffmpeg.exe即可(前提是你設定好了環境變數)。第二個坑是,command賦值時,一定給路徑加上引號,否則的話識別命令時會發生錯誤。