本文將補充說明YouTube影片下載(一)的原始碼當中的subprocess程式庫的call()及Popen()方法;介紹subprocess程式庫之前,首先要知道什麼是process和thread。
Program(程式)、Process(程序)和Thread(執行緒)
Program(程式)是一指揮電腦運作的一連串指令,例如,Chrome瀏覽器是一個程式;下載YouTube影片的pyTube.py檔也是一個程式。
Process(程序)是執行中的程式。每當你開啟瀏覽器,就是令作業系統啟動一個程序,並分配記憶體與處理器等資源給它;每一個Chrome瀏覽器的分頁,都是一個獨立運作的程序。
Thread(執行緒)是程序中的一個執行工作。
以瀏覽「視訊直播」網頁為例,目前開啟的瀏覽器視窗是一個Process(程序),網頁中的文字、影像和視訊,則是由程序裡的不同Thread(執行緒)同時連結到網站伺服器擷取而來的。
使用subprocess程式庫執行外部程式
YouTube影片下載(一)的原始碼在執行過程,分別啟動執行ffprobe.exe和ffmpeg.exe兩個外部應用程式,以便分析及合併影音檔。被我們啟動的pyTube.py程式是「主程序」,由pyTube啟動的外部程式則是「子程序」。
Python內建「啟動新的程序,執行外部程式」的subprocess程式庫(直譯作「子程序」),本文將使用它的這兩個方法:
- call():執行作業系統命令或者啟動外部程式。
- Popen():執行作業系統命令或者啟動外部程式,並可擷取它的輸出;這個方法名稱的P要大寫,代表process。
使用call()方法執行外部命令
call()方法的命令參數,預設要寫成字串列表格式。例如,底下是透過call()方法執行“ping google.com”命令的寫法:
import subprocess subprocess.call(["ping", "google.com"])
在Linux或macOS系統上,上面那一行,請寫成:
subprocess.call(["ping", "-c 4", "google.com"])
call()方法有個shell參數,預設為False,將它設定成True,即可把命令寫成單一字串,像這樣:
subprocess.call("ping google.com", shell=True)
call()方法執行之後,會傳回一個代表命令是否成功執行的數字,如果傳回值不是0,代表執行的命令出了問題。底下的程式片段執行完畢後,將會輸出“命令執行成功!”
import subprocess status = subprocess.call("ping google.com", shell=True) if (status != 0): print("出錯啦~") else: print("命令執行成功!")
YouTube影片下載(二)這篇貼文提到,透過FFmpeg合併視訊檔和聲音檔的命令如下(假設輸入的影音檔名分別是video.mp4和audio.mp3,合併完成的輸出檔名是ok.mp4):
ffmpeg -I video.mp4 -I audio.mp3 -map 0:v -map 1:a -c copy -y ok.mp4
因此,使用call()方法執行上述命令的敘述可以寫成:
cmd = "ffmpeg -I video.mp4 -I audio.mp3 -map 0:v \ -map 1:a -c copy -y ok.mp4" subprocess.call(cmd, shell=True)
YouTube影片下載(一)的原始碼當中,合併影音檔的敘述寫成merge_media()自訂函式(第96行),這部份的原始碼如下:
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('視訊和聲音合併失敗')
從YouTube下載的影片,檔名預設以影片的標題命名。在Windows系統的命令提示字元或其他非採用Unicode編碼的終端機中,英文以外的文字或變成亂碼;執行的命令敘述裡面若有亂碼,可能導致執行錯誤,因此,影音檔下載完畢時,程式先把它們重新命名成“temp_video.mp4”(參閱原始碼第126行)或“temp_audio.mp4”(原始碼第140行)。
影音檔合併完畢後,再將檔名改回影片的標題,然後刪除temp_video.mp4(視訊)及temp_audio.mp4(聲音)檔。關於合併路徑os.path.join()指令的說明,請參閱《超圖解Python程式設計入門》4-4頁,或者查閱書末「索引」中的「檔案操作」單元(D-7頁)。
使用Popen方法執行外部程式並擷取其輸出文字
Popen()的命令參數,預設也要寫成字串列表格式。它也有shell參數,設定成True,即可把命令寫成單一字串。跟call()的不同點在於,Popen()可以接收外部命令輸出在終端機的內容。
使用Popen()執行並接收“ping google”命令的結果的敘述:
import subprocess r = subprocess.Popen(["ping", "google.com"], stdout=subprocess.PIPE)
其中的stdout參數代表“standard output”(標準輸出,這裡指的是終端機螢幕),將它設定成subprocess.PIPE常數,“pipe”的原意是「管線」或「導管」,上面的命令若沒指定stdout參數,外部命令的輸出將同樣顯示在終端機,加上stdout=subprocess.PIPE,代表把外部命令的輸出導入Python程式。
上面的subprocess.Popen()執行後,將傳回stdout=subprocess.PIPE參數產生的Popen類型物件。Popen物件具有communicate(直譯為「通訊」)方法,可傳回元組(tuple)類型的外部命令輸出和錯誤訊息。以底下的敘述為例:
import subprocess r = subprocess.Popen(["ping", "google.com"], stdout=subprocess.PIPE) out, err = r.communicate()
執行之後,out將包含ping google.com命令的輸出,像這樣:
b'\r\nPing google.com [216.58.200.46] (\xa8\xcf\xa5\xce 32 \xa6\xec\xa4\xb8\xb2\xd5\xaa\xba\xb8\xea\xae\xc6):\r\n\xa6^\xc2\xd0\xa6\xdb 216.58.200.46: \xa6\xec\xa4\xb8\xb2\xd5=32 \xae\xc9\xb6\xa1=4ms TTL=56\r\n\xa6^\xc2\xd0\xa6\xdb 216.58.200.46: \xa6\xec\xa4\xb8\xb2\xd5=32 \xae\xc9\xb6\xa1=4ms TTL=56\r\n\xa6^\xc2\xd0\xa6\xdb 216.58.200.46: \xa6\xec\xa4\xb8\xb2\xd5=32 \xae\xc9\xb6\xa1=4ms TTL=56\r\n\xa6^\xc2\xd0\xa6\xdb 216.58.200.46: \xa6\xec\xa4\xb8\xb2\xd5=32 \xae\xc9\xb6\xa1=4ms TTL=56\r\n\r\n216.58.200.46 \xaa\xba Ping \xb2\xce\xadp\xb8\xea\xae\xc6:\r\n \xab\xca\xa5]: \xa4w\xb6\xc7\xb0e = 4\xa1A\xa4w\xa6\xac\xa8\xec = 4, \xa4w\xbf\xf2\xa5\xa2 = 0 (0% \xbf\xf2\xa5\xa2)\xa1A\r\n\xa4j\xac\xf9\xaa\xba\xa8\xd3\xa6^\xae\xc9\xb6\xa1 (\xb2@\xac\xed):\r\n \xb3\xcc\xa4p\xad\xc8 = 4ms\xa1A\xb3\xcc\xa4j\xad\xc8 = 4ms\xa1A\xa5\xad\xa7\xa1 = 4ms\r\n'
在Windows系統上,指定用cp950(或big5)解碼;在Linux或macOS系統,使用utf-8解碼,即可還原中文字串:
out.decode('cp950')
輸出結果如下:
'\r\nPing google.com [216.58.200.46] (使用 32 位元組的資料):\r\n回覆自 216.58.200.46: 位元組=32 時間=4ms TTL=56\r\n回覆自 216.58.200.46: 位元組=32 時間=4ms TTL=56\r\n回覆自 216.58.200.46: 位元組=32 時間=4ms TTL=56\r\n回覆自 216.58.200.46: 位元組=32 時間=4ms TTL=56\r\n\r\n216.58.200.46 的 Ping 統計資料:\r\n 封包: 已傳送 = 4,已收到 = 4, 已遺失 = 0 (0% 遺失),\r\n大約的來回時間 (毫秒):\r\n 最小值 = 4ms, 最大值 = 4ms,平均 = 4ms\r\n'
YouTube影片下載(一)的原始碼第85行的check_media()自訂函式,透過Popen()方法執行ffprobe命令,並嘗試從它的輸出文字中找尋“Audio”字串,藉以得知下載影片是否包含聲音。