Pytube 利用ノート

本稿では Python のサードパーティ製パッケージである pytube について述べる。これを習得すれば YouTube にアップロードされている個々のビデオファイルをダウンロードしてローカルディスクに保存できる。

Note

単純な場合には download-yt を使え。

Note

執筆時点での環境の概略を記す。

  • OS

    • Windows 10 Home x64

      • WSL 2: Ubuntu 20.04.2 LTS

  • Python 環境

目的

目的は YouTube の任意のビデオファイルを MP3 に変換してローカルディスクに保存することだ。

実際には MP4 ファイルさえダウンロードすれば十分だ。 MP4 ファイルを MP3 に変換することは FFmpeg などの既存のツールで対応することにする。ダウンロードまでの処理に pytube の能力を活用する。

導入手順

pip でのインストールになる。アップグレード手順もこれに準じる。

bash$ pip install pytube
bash$ pip install --upgrade pytube
Collecting pytube
  Downloading https://files.pythonhosted.org/packages/.../pytube-9.5.2-py3-none-any.whl
Installing collected packages: pytube
  Found existing installation: pytube 9.5.1
    Uninstalling pytube-9.5.1:
      Successfully uninstalled pytube-9.5.1
Successfully installed pytube-9.5.2

テスト手順

私はまだ試していないのだが、GitHub にあるリポジトリーをローカルに clone すれば単体テスト用のディレクトリー tests にアクセスできる。そこで nosetests するものと思われる。

使用例

いちばん簡単な用例

某ビデオゲームのサントラ視聴用ビデオをダウンロードする例を示す。次のようなコードがビデオファイルを MP4 形式でローカルディスクに保存する処理でもっとも単純なものになるだろう。

>>> from pytube import YouTube
>>> watch_url = 'https://www.youtube.com/watch?v=wDmlxKLqgGo'
>>> tube = YouTube(watch_url)
>>> media = tube.streams.filter(only_audio=True, file_extension='mp4').first()
>>> media.download()
'/path/to/beatmania IIDX 26 Rootage ORIGINAL SOUNDTRACK【Disc3】.mp4'

ここではあとで MP3 形式に変換する前提なので、.filter() に示すようなキーワード引数を与えている。以上のコードを実行すると、作業ディレクトリーにビデオのあるページのタイトル名に拡張子 .mp4 が付いたファイルが生成する。

  • まずは YouTube() に与える URL に

    watch?v=XXXXXXXXXXX
    

    のような文字列で終わるものを扱うといい。単一のビデオファイルだけではなく、 YouTube のページにあるプレイリストのダウンロード機能も備えている。

  • .download() で実際にローカルディスクにファイルを作成する。この例では画のない MP4 ファイルが生成する。

より進んだ用例

pytube の README に記述されている内容をよく読むこと。

コマンドラインツール

インストール後にパッケージと同名の pytube という実行スクリプトが利用可能になる。内容は pytube.cli.main() を実行するものだ。YouTube から単発のビデオ・オーディオをダウンロードする用途ならばコードを自作する必要はなく、これを実行するほうが早い。

次では URLhttps://www.youtube.com/watch?v=XXXXXXXXXXX のタイプのものとする。

  • pytube --help でヘルプを表示する。これだけは覚えておく必要がある。

  • pytube --version でバージョンを表示する。

  • pytube --list URL` でストリーム一覧を出力する。オプション --itag の引数に与えることができる値だ。

  • pytube URL でビデオをダウンロードしてファイルに保存する。利用可能な最高解像度のプログレッシブストリームが選ばれる。

    • pytube --itag ITAG URL で特定のストリームをダウンロードする。

    • pytube --audio FORMAT URL でダウンロードして MP4 に保存する。

      例えば pytube --audio mp4 URL で利用可能な最高ビットレートの AAC/mp4 オーディオデータをダウンロードしてファイルに保存する。

YouTube 側仕様変更に伴う回避策

現在は pytube の開発状況が活発なので、仕様変更が発生しても直ちに対応することが大いに期待できる。したがって、次に記すような回避策をライブラリー利用者が個別に講じることもなくなってきた。

私の過去ツイート より。

改造案

思いつきを記す。

メソッド _extract_videos() および _paginate() の戻り値変更

特定の YouTube チャンネルやプレイリストのビデオ全部の何らかの情報を効率良く取得したい。クラス Playlist やそのサブクラスの Channel にはビデオ一覧を返すプロパティーやメソッド videos, video_generator() があるので、これを用いて得られる YouTube オブジェクトのメソッドやプロパティーからデータを取得することになる。しかし、これはオブジェクトの個数だけサーバーへのリクエストが発生してすこぶる遅い。

これらの機能の実装はプライベートメソッド _extract_videos()_paginate() が核となっている。そこでは YouTube の HTML ページの構造化された情報を URL 以外捨て去っている。これをやめるように改造すれば、少なくともプレイリストページやチャンネルトップページに表示されるビデオのタイトル、閲覧数、アップロード日などの簡単な情報を高速に得られることが期待できる。

次のコード片は Playlist._extract_videos() の終了部の抜粋だ。 Channel._extract_videos() にも同様の処理がある。

# remove duplicates
return (
    uniqueify(
        list(
            # only extract the video ids from the video data
            map(
                lambda x: (
                    f"/watch?v="
                    f"{x['playlistVideoRenderer']['videoId']}"
                ),
                videos
            )
        ),
    ),
    continuation,
)

上記の map 処理を x['playlistVideoRenderer'] を抽出するように変更したいというのが本項の主張だ。

FFmpeg を併用して MP3 に変換する

本節は pytube とは無関係の内容だ。

次のようなシェル関数を作成していつでも利用可能にしておくと楽ができるだろう:

convert_mp3 ()
{
    if ! command -v ffmpeg &> /dev/null; then
        echo 'Error: ffmpeg is not available.' 1>&2
        return
    fi

    local ffmpeg_global_options="-loglevel error -y"
    local ffmpeg_input_options=""
    local ffmpeg_output_options="-qscale:a 0 -map a"

    for i in "$@";
    do
        local input_url="$i"
        local output_url="${input_url%.mp4}.mp3"
        echo "Processing ${input_url}..." 1>&2
        ffmpeg -hide_banner $ffmpeg_global_options $ffmpeg_input_options -i "$input_url" $ffmpeg_output_options "$output_url"
        if [[ $? != 0 ]]; then
            echo "Error: $output_url is not generated" 1>&2
            continue
        fi
        rm -i -f "$input_url"
    done
}