【Python】GoogleDriveのファイルをgdownを使わずにダウンロードする

Python
スポンサーリンク

 

GoogleDriveにアップロードしたファイルをダウンロードするのに便利なgdownだけど、大量のファイルをダウンロードしたり、大きなファイルをダウンロードしようとしたりすると、何かエラーを吐く。

Cannot retrieve the public link of the file. You may need to change
the permission to 'Anyone with the link', or have had many accesses.

gdownでのアクセス過多による制限っぽいんだけど、これが結構シビアっぽく、条件もよくわからなかったので、

「じゃあgdownを使わずにやろう」

というのが今回の趣旨。

 

スポンサーリンク

環境

python -V
Python 3.11.9

 

gdownを使う場合

gdownを使う場合はめちゃ簡単。

import gdown
import requests

def get_gdrive_filename(url):
    # HTMLを取得
    response = requests.get(url)
    if response.status_code == 200:
        html_content = response.text
    else:
        raise Exception(
            f"対象を取得できません. Status code: {response.status_code}")

    # PyQueryでHTMLを解析
    doc = pq(html_content)

    # ファイル名を含む要素を取得
    # Google Driveのページ構造により、ファイル名は 'meta' タグや他のタグに含まれる場合がある
    file_name = doc('meta[property="og:title"]').attr('content')  # ファイル名の候補

    # 結果を表示
    if file_name:
        print(f"ファイル名: {file_name}")
    else:
        print("ファイル名が見つかりませんでした。")

    return file_name


def download_gdrive_content(url, dl_dir):
    # ファイルIDの抽出
    file_id = url.split('/d/')[1].split('/')[0]
    # ダウンロード用の直接リンクを生成
    download_url = f"https://drive.google.com/uc?id={file_id}"

    # gdriveのDL先ファイル名を取得
    filename = get_gdrive_filename(url)

    # ダウンロード先のファイルパス
    output_filepath = dl_dir + filename
    # ファイルをダウンロード
    gdown.download(download_url, output_filepath, quiet=False)


gdrive_url = 'https://drive.google.com/file/d/XXXXXXXXXXXXXXXXXXXXXXXX/view?usp=sharing'

download_gdrive_content(gdrive_url,'/tmp/download/')

gdown自体、GoogleDriveからダウンロードするために用意されたOSSなので、かなり簡単にダウンロードできる。

また、ダウンロード中もプログレスバー等で視覚的に表示してくれるので、今回の目的のように、大量にダウンロードしたいみたいな要件がなければgdownを使うのをおすすめする。

が、今回の趣旨とは違うので紹介程度に。

 

gdownを使わないでダウンロードする

gdownを使わないとなると、いつも通りrequestsを使うことになる

import requests

def download_gdrive_content_no_gdown(url, dl_dir):
    # ファイルIDの抽出
    file_id = url.split('/d/')[1].split('/')[0]
    # ダウンロード用の直接リンクを生成
    download_url = f"https://drive.google.com/uc?id={file_id}"
    # gdriveのDL先ファイル名を取得
    filename = get_gdrive_filename(url)
    print('DL_LINK: ' + download_url)
    # 保存先のパスを作成
    save_path = dl_dir + filename
    # リクエストを送信してファイルをダウンロード
    response = requests.get(download_url, stream=True)
    if response.status_code == 200:
        output_filepath = dl_dir + filename

        # ファイルを保存
        with open(output_filepath, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        print(f"ファイルが正常にダウンロードされました: {save_path}")

ダウンロード用の直接リンクを作ってあげれば、それをリクエストするだけでダウンロードできる。

じゃあgdownよりも簡単かというとそう単純なものでもなかった。

直接リンクに移動したときにたまにこんな画面が出ることがある。

これが出ると、直接リンク先のこのHTMLをダウンロードしてしまい、本当にほしいコンテンツを取得できない。

これを防ぐためには、以下のようなリンクに作り変える必要がある。

'https://drive.usercontent.google.com/download?id=[id]&authuser=0&confirm=t&uuid=[uuid]

authuserは0、confirm=tで固定っぽいが、問題はuuid。

レスポンスヘッダにもないし、どこを参照するパラメータなのかあくせく探し回った結果、html内に埋め込まれていることを発見。

というわけで、webスクレイピングを使ってhtml内のuuidを取得することになる。

from pyquery import PyQuery as pq

# 直接リンクを引数にいれるとUUIDとかつけたウィルスチェックをパスするリンクを生成する
def create_gdrive_pass_viruscheck_link(url):
    # ページのHTMLを取得
    response = requests.get(url)
    if response.status_code == 200:
        html_content = response.text
    else:
        # HTTPで取れなかったら例外を投げる
        raise Exception(
            f"Failed to fetch the page. Status code: {response.status_code}")

    # PyQueryでHTMLを解析
    doc = pq(html_content)

    # name="uuid" を持つ input 要素を検索し、value を取得
    id = doc('input[name="id"]').attr('value')
    authuser = doc('input[name="authuser"]').attr('value')
    if authuser == None:
        authuser = '0'
    confirm = doc('input[name="confirm"]').attr('value')
    uuid = doc('input[name="uuid"]').attr('value')
    at = doc('input[name="at"]').attr('value')

    print(id, authuser, confirm, uuid, at)

    # リンクを生成
    dl_link = 'https://drive.usercontent.google.com/download?' + 'id=' + id + \
        '&authuser=' + authuser + '&confirm=' + \
        confirm + '&uuid=' + uuid  # + '&at' + at

    return dl_link

WEBスクレイピングにはpyqueryを利用。

htmlからidとuuid等を取得して、それを使ってリンクを作成する。

これによって真の直接ダウンロードリンクを生成できる。

というわけで全体。

import requests
from pyquery import PyQuery as pq

def get_gdrive_filename(url):
    # HTMLを取得
    response = requests.get(url)
    if response.status_code == 200:
        html_content = response.text
    else:
        raise Exception(
            f"Failed to fetch the page. Status code: {response.status_code}")

    # PyQueryでHTMLを解析
    doc = pq(html_content)

    # ファイル名を含む要素を取得
    # Google Driveのページ構造により、ファイル名は 'meta' タグや他のタグに含まれる場合があります
    file_name = doc('meta[property="og:title"]').attr('content')  # ファイル名の候補
    # 上でファイル名が取れなかった場合、以下を試す
    if file_name == None:
        file_name = doc('span.uc-name-size a').text()
    if file_name == None or file_name == '':
        file_name = get_content_disposition(url).split('"')[1]

    # 結果を表示
    if file_name:
        print(f"ファイル名: {file_name}")
    else:
        print("ファイル名が見つかりませんでした。")

    return file_name

def create_gdrive_pass_viruscheck_link(url):
    # ページのHTMLを取得
    response = requests.get(url)
    if response.status_code == 200:
        html_content = response.text
    else:
        # HTTPで取れなかったら例外を投げる
        raise Exception(
            f"Failed to fetch the page. Status code: {response.status_code}")

    # PyQueryでHTMLを解析
    doc = pq(html_content)

    # print(html_content)

    # name="uuid" を持つ input 要素を検索し、value を取得
    id = doc('input[name="id"]').attr('value')
    authuser = doc('input[name="authuser"]').attr('value')
    if authuser == None:
        authuser = '0'
    confirm = doc('input[name="confirm"]').attr('value')
    uuid = doc('input[name="uuid"]').attr('value')
    at = doc('input[name="at"]').attr('value')

    print(id, authuser, confirm, uuid, at)

    dl_link = 'https://drive.usercontent.google.com/download?' + 'id=' + id + \
        '&authuser=' + authuser + '&confirm=' + \
        confirm + '&uuid=' + uuid  # + '&at' + at

    return dl_link

def get_content_disposition(url):
    # HEADリクエストでヘッダーだけを取得
    response = requests.head(url, allow_redirects=True)
    response.raise_for_status()  # エラーがあれば例外をスロー

    # ヘッダーからContent-Dispositionを取得
    content_disposition = response.headers.get("Content-Disposition")
    if content_disposition:
        print(f"Content-Disposition: {content_disposition}")
        return content_disposition
    else:
        print("Content-Dispositionヘッダーが見つかりませんでした")
        return None


def download_gdrive_content_no_gdown(url, dl_dir):
    # ファイルIDの抽出
    file_id = url.split('/d/')[1].split('/')[0]
    # ダウンロード用の直接リンクを生成
    download_url = f"https://drive.google.com/uc?id={file_id}"
    # gdriveのDL先ファイル名を取得
    filename = get_gdrive_filename(url)
    print('DL_LINK: ' + download_url)
    # 保存先のパスを作成
    save_path = dl_dir + filename
    # リクエストを送信してファイルをダウンロード
    response = requests.get(download_url, stream=True)
    if response.status_code == 200:
        # ファイル名を取得(Content-Disposition ヘッダーから)
        if 'Virus scan warning' in response.text:
            viruscheck_url = create_gdrive_pass_viruscheck_link(download_url)
            response = requests.get(viruscheck_url, stream=True)

        output_filepath = dl_dir + filename

        # ファイルを保存
        with open(output_filepath, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

        print(f"ファイルが正常にダウンロードされました: {save_path}")

gdrive_url = 'https://drive.google.com/file/d/XXXXXXXXXXXXXXXXXXXXXXXX/view?usp=sharing'

download_gdrive_content_no_gdown(gdrive_url,'/tmp/download/')

これでサイズの大きなファイルを取得する際にもちゃんと認証パスしてダウンロードできる。

ここまで書いておいて何だけど、やっぱりgdown使ったほうが楽だわ。

コメント

タイトルとURLをコピーしました