YouTube影片下載(三):執行外部命令的subprocess.call()以及subprocess.Popen()方法

本文將補充說明YouTube影片下載(一)的原始碼當中的subprocess程式庫的call()及Popen()方法;介紹subprocess程式庫之前,首先要知道什麼是process和thread。

Program(程式)、Process(程序)和Thread(執行緒)

Program(程式)是一指揮電腦運作的一連串指令,例如,Chrome瀏覽器是一個程式;下載YouTube影片的pyTube.py檔也是一個程式。

Process(程序)是執行中的程式。每當你開啟瀏覽器,就是令作業系統啟動一個程序,並分配記憶體與處理器等資源給它;每一個Chrome瀏覽器的分頁,都是一個獨立運作的程序。

Thread(執行緒)是程序中的一個執行工作。

以瀏覽「視訊直播」網頁為例,目前開啟的瀏覽器視窗是一個Process(程序),網頁中的文字、影像和視訊,則是由程序裡的不同Thread(執行緒)同時連結到網站伺服器擷取而來的。

Program(程式)、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”字串,藉以得知下載影片是否包含聲音。

Posts created 469

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top