本文旨在補充《超圖解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影片下載(二)