Python, PyDriveでGoogle Driveのフォルダ作成、ファイル移動、一括処理

Posted: | Tags: Python, PyDrive

PyDriveを使うとPythonからGoogleドライブを簡単に操作できる。

ここでは、Googleドライブ上のフォルダ関連の操作について説明する。

  • Googleドライブにフォルダを作成
  • ローカルのファイルを指定のフォルダにアップロード
  • Googleドライブのファイルを指定のフォルダに移動
  • Googleドライブのフォルダを削除
  • フォルダ内のファイルのリストを作成
    • 一階層のみリスト化
    • サブフォルダの中身まで再帰的にリスト化
  • フォルダ内のファイルを一括処理(ダウンロードなど)

PyDriveの基本的な使い方(利用登録や認証、ファイルのダウンロード、アップロード、削除など)は以下の記事を参照。

なお、以下はPyDriveバージョン1.3.1の情報。

Googleドライブにフォルダを作成

フォルダの作成もファイルの作成と同様、CreateFile()GoogleDriveFileオブジェクトを作成しUpload()する、という流れ。

import pprint

from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive

gauth = GoogleAuth()
gauth.LocalWebserverAuth()

drive = GoogleDrive(gauth)

f_folder = drive.CreateFile({'title': 'new_folder',
                             'mimeType': 'application/vnd.google-apps.folder'})
print(f_folder)
# GoogleDriveFile({'title': 'new_folder', 'mimeType': 'application/vnd.google-apps.folder'})

f_folder.Upload()

pprint.pprint(f_folder)
# {'alternateLink': 'https://drive.google.com/drive/folders/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#  'appDataContents': False,
#  'capabilities': {'canCopy': False, 'canEdit': True},
#  'copyRequiresWriterPermission': False,
#  'copyable': False,
#  'createdDate': '2019-07-16T14:04:39.276Z',
#  'editable': True,
#  'embedLink': 'https://drive.google.com/embeddedfolderview?id=1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#  'etag': '"_sqIxUq0fTLFIA17mBQDotbHWsg/MTU2MzI4NTg3OTI3Ng"',
#  'explicitlyTrashed': False,
#  'iconLink': 'https://drive-thirdparty.googleusercontent.com/16/type/application/vnd.google-apps.folder+48',
#  'id': '1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#  'kind': 'drive#file',
#  'labels': {'hidden': False,
#             'restricted': False,
#             'starred': False,
#             'trashed': False,
#             'viewed': True},
#  'lastModifyingUser': {'displayName': 'nkmk on',
#                        'emailAddress': 'nkmk.on@gmail.com',
#                        'isAuthenticatedUser': True,
#                        'kind': 'drive#user',
#                        'permissionId': '15529215658844670952'},
#  'lastModifyingUserName': 'nkmk on',
#  'lastViewedByMeDate': '2019-07-16T14:04:39.276Z',
#  'markedViewedByMeDate': '1970-01-01T00:00:00.000Z',
#  'mimeType': 'application/vnd.google-apps.folder',
#  'modifiedByMeDate': '2019-07-16T14:04:39.276Z',
#  'modifiedDate': '2019-07-16T14:04:39.276Z',
#  'ownerNames': ['nkmk on'],
#  'owners': [{'displayName': 'nkmk on',
#              'emailAddress': 'nkmk.on@gmail.com',
#              'isAuthenticatedUser': True,
#              'kind': 'drive#user',
#              'permissionId': '15529215658844670952'}],
#  'parents': [{'id': '0AAeKIFCqYN07Uk9PVA',
#               'isRoot': True,
#               'kind': 'drive#parentReference',
#               'parentLink': 'https://www.googleapis.com/drive/v2/files/0AAeKIFCqYN07Uk9PVA',
#               'selfLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz/parents/0AAeKIFCqYN07Uk9PVA'}],
#  'quotaBytesUsed': '0',
#  'selfLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#  'shared': False,
#  'spaces': ['drive'],
#  'title': 'new_folder',
#  'userPermission': {'etag': '"_sqIxUq0fTLFIA17mBQDotbHWsg/38oVvgHkBS5xDYba_gRDJx2lqsQ"',
#                     'id': 'me',
#                     'kind': 'drive#permission',
#                     'role': 'owner',
#                     'selfLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz/permissions/me',
#                     'type': 'user'},
#  'version': '1',
#  'writersCanShare': True}

ポイントは'mimeType''mimeType''application/vnd.google-apps.folder'に設定するとフォルダとして扱われる。

なお、ファイルの場合はSetContentFile()でローカルのファイルをセットしてアップロードできるが、SetContentFile()にフォルダ(ディレクトリ)のパスを指定するとエラーになる。

f_folder = drive.CreateFile()

# f_folder.SetContentFile('src')
# IsADirectoryError: [Errno 21] Is a directory: 'src'

ローカルのフォルダの中身をアップロードするには、glob()などでローカルのファイルのパスの一覧を取得し、SetContentFile()でひとつずつアップロードすればよい。

ローカルのファイルをGoogleドライブの指定のフォルダにアップロードする方法については次に説明する。

ローカルのファイルを指定のフォルダにアップロード

Googleドライブにおけるファイルやフォルダは親フォルダ(直上のフォルダ)の情報を保持している。

親フォルダの情報はGoogleDriveFileオブジェクトでは'parents'に格納されている。

したがって、ローカルのファイルをGoogleドライブ上の指定のフォルダにアップロードしたい場合は、'parents'に所望のフォルダのIDを指定すればよい。

ここではフォルダ名から検索してフォルダのIDを取得している。詳細は以下の記事を参照。

folder_id = drive.ListFile({'q': 'title = "new_folder"'}).GetList()[0]['id']

f = drive.CreateFile({"parents": [{"id": folder_id}]})
f.SetContentFile('src/lena.jpg')
f['title'] = 'lena.jpg'
f.Upload()

print(type(f['parents']))
# <class 'list'>

pprint.pprint(f['parents'])
# [{'id': '1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'isRoot': False,
#   'kind': 'drive#parentReference',
#   'parentLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'selfLink': 'https://www.googleapis.com/drive/v2/files/1nUWOO8QWyqE13PMW4fzemKF8gcsOCXkJ/parents/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz'}]

print(f['parents'][0]['id'] == folder_id)
# True

サンプルコードからも分かるように、'parents'はIDなどの情報を含む辞書のリスト。リストなので設定するときも[]が必要。

'parents'がリストなのは、Googleドライブにおける親フォルダは実際はタグのような扱いであるため。フォルダとして考えると違和感があるが、'parents'に複数のフォルダを設定できる。

Googleドライブのファイルを指定のフォルダに移動

Googleドライブ上のファイルを指定のフォルダに移動する場合も考え方はアップロードと同じ。'parents''id'を更新すればよい。

メタデータの更新についての詳細は以下の記事を参照。

ルートフォルダ(マイフォルダ)に移動する例。ルートフォルダのIDは'root'で代用可。

f = drive.CreateFile({'id': file_id})
f.FetchMetadata()
pprint.pprint(f['parents'])
# [{'id': '1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'isRoot': False,
#   'kind': 'drive#parentReference',
#   'parentLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'selfLink': 'https://www.googleapis.com/drive/v2/files/1nUWOO8QWyqE13PMW4fzemKF8gcsOCXkJ/parents/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz'}]

f['parents'] = [{'id': 'root'}]
f.Upload()
pprint.pprint(f['parents'])
# [{'id': '0AAeKIFCqYN07Uk9PVA',
#   'isRoot': True,
#   'kind': 'drive#parentReference',
#   'parentLink': 'https://www.googleapis.com/drive/v2/files/0AAeKIFCqYN07Uk9PVA',
#   'selfLink': 'https://www.googleapis.com/drive/v2/files/1nUWOO8QWyqE13PMW4fzemKF8gcsOCXkJ/parents/0AAeKIFCqYN07Uk9PVA'}]

任意のフォルダに移動する例。該当のフォルダのIDを指定する。

folder_id = drive.ListFile({'q': 'title = "new_folder"'}).GetList()[0]['id']

f['parents'] = [{'id': folder_id}]
f.Upload()
pprint.pprint(f['parents'])
# [{'id': '1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'isRoot': False,
#   'kind': 'drive#parentReference',
#   'parentLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'selfLink': 'https://www.googleapis.com/drive/v2/files/1nUWOO8QWyqE13PMW4fzemKF8gcsOCXkJ/parents/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz'}]

フォルダのIDはブラウザでGoogleドライブを開いてURLから確認することもできる。末尾がIDになっている。

  • https://drive.google.com/drive/u/0/folders/<ID>

なお、フォルダを別のフォルダに移動させたい場合も、同様に'parents''id'を更新すればよい。

Googleドライブのフォルダを削除

ファイルと同様、フォルダの削除にはTrash(), UnTrash(), Delete()を使う。

注意点はフォルダ内のファイルやフォルダ('parents'のIDがそのフォルダであるファイルやフォルダ)の扱い。フォルダを削除するとフォルダ内のファイルやフォルダも削除される。

ゴミ箱に入れるTrash()の例。ファイルとその親フォルダのIDからGoogleDriveFileオブジェクトを生成してフォルダをゴミ箱に入れる。

メタデータの'labels''trashed'はそのファイルがゴミ箱に入っているかどうかを示し、'explicitlyTrashed'はそのオブジェクト自体が直接ゴミ箱に入れられたか、親フォルダとともに再帰的にゴミ箱に入れられたかを示す。

file_id = drive.ListFile({'q': 'title = "lena.jpg"'}).GetList()[0]['id']

f_file = drive.CreateFile({'id': file_id})
pprint.pprint(f_file['parents'])
# [{'id': '1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'isRoot': False,
#   'kind': 'drive#parentReference',
#   'parentLink': 'https://www.googleapis.com/drive/v2/files/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz',
#   'selfLink': 'https://www.googleapis.com/drive/v2/files/1nUWOO8QWyqE13PMW4fzemKF8gcsOCXkJ/parents/1dQT4GYkl2zHDP_7yXYcKuomX6XggF9Cz'}]

f_folder = drive.CreateFile({'id': f_file['parents'][0]['id']})

f_folder.Trash()

f_folder.FetchMetadata()
print(f_folder['labels']['trashed'])
# True

print(f_folder['explicitlyTrashed'])
# True

f_file.FetchMetadata()
print(f_file['labels']['trashed'])
# False

print(f_file['explicitlyTrashed'])
# False

ゴミ箱から出すUnTrash()

f_folder.UnTrash()

f_folder.FetchMetadata()
print(f_folder['labels']['trashed'])
# False

print(f_folder['explicitlyTrashed'])
# False

f_file.FetchMetadata()
print(f_file['labels']['trashed'])
# False

print(f_file['explicitlyTrashed'])
# False

完全に削除するDelete()。フォルダ内のファイルも完全に削除されるので注意。

f_folder.Delete()

# f_folder.FetchMetadata()
# ApiRequestError: <HttpError 404 when requesting https://www.googleapis.com/drive/v2/files/144fD0jujJEyCdaM5jv8Fgp-czXLZNfBe?alt=json returned "File not found: 144fD0jujJEyCdaM5jv8Fgp-czXLZNfBe">

# f_file.FetchMetadata()
# ApiRequestError: <HttpError 404 when requesting https://www.googleapis.com/drive/v2/files/1-j36c901moQ3gsTXwnvwLK3lRRKhS1CT?alt=json returned "File not found: 1-j36c901moQ3gsTXwnvwLK3lRRKhS1CT">

フォルダ内のファイルのリストを作成

PyDriveを利用したGoogleドライブのファイル・フォルダのリスト生成についての詳細は以下の記事を参照。

ここでは特にフォルダ内のファイル・フォルダのリスト生成について説明する。以降のサンプルコードでは、以下の構造のファイル・フォルダを例とする。

root
├── dir1
│   ├── file11
│   ├── file12
│   └── subdir1
│       └── file111
├── dir2
│   └── file21
├── file1
└── file2

一階層のみリスト化

ListFile()の引数の'q''"<folder_id>" in parentsという条件を指定する。GoogleDriveFileオブジェクトのリストが得られる。

folder_id = drive.ListFile({'q': "title = 'dir1'"}).GetList()[0]['id']

file_list = drive.ListFile({'q': '"{}" in parents and trashed = false'.format(folder_id)}).GetList()

print(type(file_list))
# <class 'list'>

print(type(file_list[0]))
# <class 'pydrive.files.GoogleDriveFile'>

for f in file_list:
    print(f['title'], ' \t', f['id'])
# subdir1    1CpPdnFTdN6DWPz7VBM1pV-RMZfcbvzck
# file12     1iv9UB1pi65MqfFmqYwBoz3AfZbKr2jLx
# file11     15sy12U3d845pXjLaEWww4VLO9Vs7C4J9

上の例ではtrashed = falseという条件もつけている。上述のように、Googleドライブにおいてはゴミ箱に入っているかどうかはメタデータの'labels''trashed'で判定され、親フォルダを示すparentsは元のまま。trashed = falseがないと、ゴミ箱に入っているファイルやフォルダもリストアップされてしまう。要注意。

サブフォルダの中身まで再帰的にリスト化

ListFile()parentsの条件を指定するだけだと、さらに下の階層、サブフォルダの中のファイルやフォルダはリスト化できない。

サブフォルダの中まで対象とするには、以下のように再帰的な関数が考えられる。

def get_list_recursively(parent_id, l=None):
    if l is None:
        l = []

    file_list = drive.ListFile({'q': '"{}" in parents and trashed = false'.format(parent_id)}).GetList()
    l += file_list

    for f in file_list:
        if f['mimeType'] == 'application/vnd.google-apps.folder':
            get_list_recursively(f['id'], l)

    return l

for f in get_list_recursively(folder_id):
    print(f['title'], ' \t', f['id'])
# subdir1    1CpPdnFTdN6DWPz7VBM1pV-RMZfcbvzck
# file12     1iv9UB1pi65MqfFmqYwBoz3AfZbKr2jLx
# file11     15sy12U3d845pXjLaEWww4VLO9Vs7C4J9
# file111    1bfc7jlDobxuoyZeF8CDyXlN66zl84Ech

フォルダを除外してファイルのみを対象としたい場合は以下の通り。

def get_list_file_recursively(parent_id, l=None):
    if l is None:
        l = []

    file_list = drive.ListFile({'q': '"{}" in parents and trashed = false'.format(parent_id)}).GetList()
    l += [f for f in file_list if f['mimeType'] != 'application/vnd.google-apps.folder']

    for f in file_list:
        if f['mimeType'] == 'application/vnd.google-apps.folder':
            get_list_file_recursively(f['id'], l)

    return l

for f in get_list_file_recursively(folder_id):
    print(f['title'], '   \t', f['id'])
# file12         1iv9UB1pi65MqfFmqYwBoz3AfZbKr2jLx
# file11         15sy12U3d845pXjLaEWww4VLO9Vs7C4J9
# file111        1bfc7jlDobxuoyZeF8CDyXlN66zl84Ech

フォルダ内のファイルを一括処理(ダウンロードなど)

ファイル(GoogleDriveFileオブジェクト)のリストが取得できればそれを元に一括処理を行うのは簡単。

これまでの例ではprint(f['title'], ...)などのようにファイル名などを出力していたが、そこでファイルのダウンロードやリネームなどの処理を行えばよい。

GetContentFile()でダウンロードする例。

for f in get_list_file_recursively(folder_id):
    f.GetContentFile(os.path.join('dst', f['title']))

リスト自体が必要ない場合、特に再帰的に大量のファイルを処理するのであれば、一旦リスト化してから処理するよりも直接処理するほうが効率的。

ダウンロードの場合はフォルダ構造を保持したままダウンロードすることもできる。

def download_file_recursively(parent_id, dst_dir):
    os.makedirs(dst_dir, exist_ok=True)

    file_list = drive.ListFile({'q': '"{}" in parents and trashed = false'.format(parent_id)}).GetList()

    for f in file_list:
        if f['mimeType'] == 'application/vnd.google-apps.folder':
            download_file_recursively(f['id'], os.path.join(dst_dir, f['title']))
        else:
            dst_path = os.path.join(dst_dir, f['title'])
            f.GetContentFile(dst_path)
            print('Download {} to {}'.format(f['title'], dst_path))

download_file_recursively('root', 'dst/root')
# Download file21 to dst/root/dir2/file21
# Download file111 to dst/root/dir1/subdir1/file111
# Download file12 to dst/root/dir1/file12
# Download file11 to dst/root/dir1/file11
# Download file2 to dst/root/file2
# Download file1 to dst/root/file1

なお、ブラウザのGoogleドライブではフォルダをZIPでまとめてダウンロードできる。Python / PyDriveで自動化する必要がなければブラウザでアクセスして手動でダウンロードするほうがはるかに簡単。

関連カテゴリー

関連記事