本文旨在補充《超圖解Python程式設計入門》第5章的YouTube影片下載單元,採用新版PyTube程式庫,修正部份影片無法下載的問題,並且新增合併視訊和音軌的功能。
由於YouTube網站經常修改伺服器端程式,導致PyTube程式庫無法下載影片(請參閱這一篇留言回應),Harold Martin在原有PyTube的基礎上做了一些修正,推出更新版本,原始檔和相關說明請參閱pytube3專案網頁。
更新PyTube的方式如下,請在CLI視窗輸入底下的命令:
pip install pytube3 --upgrade
我們自行編寫的Python程式碼不需要修改。更新程式庫之後測試下載幾個影片都沒問題,但是許多音樂類型影片仍舊無法下載。
自動合併視訊和聲音
從YouTube下載的高畫質(1080p或更高)影片通常不包含音軌,遇到這種情況,需要再次下載影片的聲音,再將它們合併在一起。筆者修改《超圖解Python程式設計入門》第5章的tube.py檔,新增這些功能:
- 檢測下載影片是否包含音軌(是否有聲音)
- 若影片沒有音軌,則自動下載音軌。
- 自動合併視訊與音軌
原本tube.py程式當中的download_video(下載視訊)函式,改名成比較適切的download_media(下載媒體)。更新後的程式執行流程如下:

更新後的tube.py完整原始碼如下,請直接覆蓋書本的tube.py檔。為了順利執行此程式,請參閱5-26頁「使用FFmpeg轉換多媒體檔案格式」單元說明,先安裝FFmpeg工具程式。
import argparse
import os
import platform
from pytube import YouTube
import subprocess
args = {}
fileobj = {}
download_count = 1
def pyTube_folder():
sys = platform.system()
home = os.path.expanduser('~')
if sys == 'Windows':
folder = os.path.join(home, 'Videos', 'PyTube')
elif sys == 'Darwin':
folder = os.path.join(home, 'Movies', 'PyTube')
if not os.path.isdir(folder): # 若'PyTube'資料夾不存在…
os.mkdir(folder) # 則新增資料夾
return folder
def onProgress(stream, chunk, file_handle, remains):
total = stream.filesize
percent = (total-remains) / total * 100
print('下載中… {:05.2f}%'.format(percent), end='\r')
def video_res(yt):
res_set = set()
video_list = yt.streams.filter(type="video").all()
for v in video_list:
res_set.add(v.resolution)
return sorted(res_set, reverse=True, key=lambda s: int(s[:-1]))
def download_media(args):
try:
yt = YouTube(args.url, on_progress_callback=onProgress,
on_complete_callback=onComplete)
except:
print('下載影片時發生錯誤,請確認網路連線和YouTube網址無誤。')
return
filter = yt.streams.filter
if args.a: # 只下載聲音
target = filter(type="audio").first()
elif args.fhd:
target = filter(type="video", resolution="1080p").first()
elif args.hd:
target = filter(type="video", resolution="720p").first()
elif args.sd:
target = filter(type="video", resolution="480p").first()
else:
target = filter(type="video").first()
if target is None:
print('沒有您指定的解析度,可用的解析度如下:')
res_list = video_res(yt)
for i, res in enumerate(res_list):
print('{}) {}'.format(i+1, res))
val = input('請選擇(預設{}):'.format(res_list[0]))
try:
res = res_list[int(val)-1]
except:
res = res_list[0]
print('您選擇的是 {} 。'.format(res))
target = filter(type="video", resolution=res).first()
# 開始下載
target.download(output_path=pyTube_folder())
# 檢查影片檔是否包含聲音
def check_media(filename):
r = subprocess.Popen(["ffprobe", filename],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, err = r.communicate()
if (out.decode('utf-8').find('Audio') == -1):
return -1 # 沒有聲音
else:
return 1
# 合併影片檔
def merge_media():
temp_video = os.path.join(fileobj['dir'], 'temp_video.mp4')
temp_audio = os.path.join(fileobj['dir'], 'temp_audio.mp4')
temp_output = os.path.join(fileobj['dir'], 'output.mp4')
cmd = f'ffmpeg -i {temp_video} -i {temp_audio} \
-map 0:v -map 1:a -c copy -y {temp_output}'
try:
subprocess.call(cmd, shell=True)
# 視訊檔重新命名
os.rename(temp_output, os.path.join(fileobj['dir'], fileobj['name']))
os.remove(temp_audio)
os.remove(temp_video)
print('視訊和聲音合併完成')
except:
print('視訊和聲音合併失敗')
# 檔案下載的回呼函式
def onComplete(stream, file_handle):
global download_count, fileobj
fileobj['name'] = os.path.basename(file_handle.name)
fileobj['dir'] = os.path.dirname(file_handle.name)
print('\r')
if download_count == 1:
if check_media(file_handle.name) == -1:
print('此影片沒有聲音')
download_count += 1
try:
# 視訊檔重新命名
os.rename(file_handle.name, os.path.join(
fileobj['dir'], 'temp_video.mp4'))
except:
print('視訊檔重新命名失敗')
return
print('準備下載聲音檔')
vars(args)['a'] = True # 設定成a參數
download_media(args) # 下載聲音
else:
print('此影片有聲音,下載完畢!')
else:
try:
# 聲音檔重新命名
os.rename(file_handle.name, os.path.join(
fileobj['dir'], 'temp_audio.mp4'))
except:
print("聲音檔重新命名失敗")
# 合併聲音檔
merge_media()
def main():
global args
parser = argparse.ArgumentParser()
parser.add_argument("url", help="指定YouTube視訊網址")
parser.add_argument("-sd", action="store_true", help="選擇普通(480P)畫質")
parser.add_argument("-hd", action="store_true", help="選擇HD(720P)畫質")
parser.add_argument("-fhd", action="store_true", help="選擇Full HD(1080P)畫質")
parser.add_argument("-a", action="store_true", help="僅下載聲音")
args = parser.parse_args()
download_media(args)
if __name__ == '__main__':
main()
執行此tube.py檔的示範如下,因為此1080p影片沒有音軌,所以程式自動下載並合併音軌成單一MP4檔。

讀取與重新設定命令行參數
修改後的tube.py程式定義了3個全域變數:
args = {} # 儲存命令行參數
fileobj = {} # 儲存下載檔的路徑和檔名
download_count = 1 # 紀錄下載次數
在宣告YouTube物件敘述中,新增一個「下載完成的回呼函式」定義,當影片下載完畢,onComplete()函式將自動被觸發執行。

使用argparse程式庫接收到的命令行參數,將以Namespace物件格式存入args變數。若執行tube.py時輸入-fhd參數,則此物件的fhd參數值為True:

程式可透過Python語言內建的vars()函式,傳回「字典」類型的物件資料:

假設要取出命令行的url(影片網址)參數,底下兩種寫法都行:
![]()
下載媒體檔案的download_media()函式,透過命令行的args變數取得影片的網址(url)和格式(如:fhd高畫質、a僅聲音),假設執行tube.py時指定高畫質格式:
py tube.py https://youtu.be/pG5LTUJskSg -fhd
下載完畢後,程式檢測到該影片檔沒有音軌,需要下載影片的聲音,相當於執行這個命令:
py tube.py https://youtu.be/pG5LTUJskSg -a
由於程式裡的args變數已經紀錄了影片的網址,我們只須要把args裡的a參數設定成True,然後再執行一次download_media()函式,它就會下載同一支影片的聲音檔了。設定args參數的敘述如下:
vars(args)['a'] = True
設定後的args將變成a和fhd參數為True:
Namespace(a=True, fhd=True, hd=False, sd=False, url='https://youtu.be/pG5LTUJskSg')
使用os程式庫取得檔案路徑、檔名並重新命名
重新下載視訊檔案時,相同名稱的影片會覆蓋前一個檔案。同一支影片的「視訊」和「僅聲音」檔的名字都一樣,為了避免彼此覆蓋,下載後的檔案需要重新命名。
os程式庫具備讀取檔案路徑、檔名和重新命名等功能(參閱第4章「使用os程式庫操作檔案」單元),例如,底下的name和dir變數將儲存檔名“下載影片.mp4”和路徑“C:\Users\cubie\Videos\PyTube”:

重新命名的語法和例子:

影片下載完畢執行回呼函式時,會一併傳遞兩個參數,第2個參數是檔案物件,從它的name屬性可取得存檔路徑和檔名,底下的程式將它們存入字典型fileobj變數備用。


老師請問一下,目前我從ffmpege官網下載的壓縮檔裡沒有找的bin資料夾,所以我不知道該怎麼使用ffmpeg,希望老師能回覆我!
請點擊這個網址下載:https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z
thanks,
jeffrey
謝謝老師d(・∀・○)
老師不好意思,今天我按照上面的程式去下載影片時,一直跳出”下載影片時發生錯誤,請確認網路連線和YouTube網址無誤”的訊息,請問要怎麼解決呢?
請參閱:YouTube影片下載(六):改用PyTube程式庫解決執行錯誤
thanks,
jeffrey
TypeError: onProgress() missing 1 required positional argument: ‘remains’
执行tube.py时出现这样的报错,请问老师怎么办?
请参阅底下两篇更新贴文说明:
YouTube影片下載(五):PyTube3程式庫更新說明
YouTube影片下載(六):改用PyTube程式庫解決執行錯誤
thanks,
jeffrey
使用(五)解决了!谢谢老师!
老師不好意思,我一直出現下載影片時發生錯誤,請確認網路連線和YouTube網址無誤的訊息,確認過網路和網址都沒問題,怎麼辦?
我將pytube升級到最新版:
pip install pytube –upgrade
測試執行:
python tube.py https://youtu.be/DCFxibSc_FY -a
沒問題。也請參閱這一篇貼文「YouTube影片下載(六):改用PyTube程式庫解決執行錯誤」。
PvD-dfIiEs-fhd
下載中… 100.00%
Traceback (most recent call last):
File “/home/vito0319tw/tube.py”, line 170, in
main()
File “/home/vito0319tw/tube.py”, line 166, in main
download_media(args)
File “/home/vito0319tw/tube.py”, line 86, in download_media
target.download(output_path=pyTube_folder())
File “/home/vito0319tw/anaconda3/lib/python3.10/site-packages/pytube/streams.py”, line 336, in download
self.on_complete(file_path)
File “/home/vito0319tw/anaconda3/lib/python3.10/site-packages/pytube/streams.py”, line 415, in on_complete
on_complete(self, file_path)
File “/home/vito0319tw/tube.py”, line 129, in onComplete
if check_media(file_path) == -1:
File “/home/vito0319tw/tube.py”, line 96, in check_media
out = subprocess.run([“ffprobe”, filename],
File “/home/vito0319tw/anaconda3/lib/python3.10/subprocess.py”, line 503, in run
with Popen(*popenargs, **kwargs) as process:
File “/home/vito0319tw/anaconda3/lib/python3.10/subprocess.py”, line 971, in __init__
self._execute_child(args, executable, preexec_fn, close_fds,
File “/home/vito0319tw/anaconda3/lib/python3.10/subprocess.py”, line 1847, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: ‘ffprobe’
執行時候一直有這樣的錯誤,怎麼辦老師
請問你有安裝ffmpeg嗎?請參閱這篇貼文:YouTube影片下載(二)