Pythonを使ってS3にあるファイルをダウンロードする

最近社内のpythonツールの修正を担当して、AWS S3で作ったbucketからファイルを取得する処理を修正しました。
この文章ではpythonを使ってS3にあるファイルをダウンロードする方法を記載します。

# 動作環境
- windows 10
- python 3.9

# 事前準備
## pythonのインストール

まず、pythonを書いてlocalで実行するためにはpython環境の構築が必要です。
以下はwindows環境でpython環境を構築する手順になります。どうぞご参照ください。

https://prog-8.com/docs/python-env-win

## AWS CLIのインストール

localでAWSのリソースにアクセスするためにAWS CLIを使います。
以下はAWS CLIをインストールする手順になります。

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

## AWS S3 bucketの作成

そもそもS3は何かというのはAWSのホームページを参照しましょう。

https://aws.amazon.com/jp/s3/?nc1=h_ls

以下はAWS S3をインストールする手順になります。
作成できたら、適宜ファイルをアップロードしてみましょう。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/create-bucket-overview.html

## AWS IAM roleの作成

S3バケットを操作するためにIAM roleを使います。
以下の手順を参考にIAM roleを作成しましょう。

https://www.jpcyber.com/support/create-iam-role

こちらの手順は`AmazonS3FullAccess`のポリシーを使っていますが、s3へのすべての操作が可能になるため、
意図しないことが発生することがあります。
権限管理をちゃんとしたい場合は用途に応じてポリシーを付与しましょう。

## アクセスキーの生成

アクセスキーを作る手順はこちらになります。

https://style.potepan.com/articles/35403.html

アクセスキーが漏洩すると、他人の悪用によって意図せずお金がかかってしまうことがありますので、
細心の注意を払って保管しましょう。

## 認証情報のセットアップ

AWS CLIを使って、S3にアクセスするための認証情報を端末に設定します。
コマンドプロンプトを起動して、aws configureを使ったら設定が可能です。
profileのオプションを使うと、いろんなユーザーを作ることができます。
こちらのプロファイル名は後のpythonのソースで使うので、どこかでメモするか覚えておきましょう。

```
$ aws configure --profile {プロファイル名}
AWS Access Key ID [None]: {アクセスキーID}
AWS Secret Access Key [None]: {シークレットアクセスキー}
Default region name [None]: {空白可、例えばap-northeast-1}
Default output format [None]: {空白可、例えばjson}
```
これによって、自端末から作成したS3のバケットにアクセスできるようになります。
以下のコマンドを実行してみて、アクセスできるか確認しましょう。
```
aws s3 ls s3://<バケット名>
```

# pythonコードを実際に書く

## boto3ライブラリ
pythonでAWSのリソースにアクセスするにはAWS SDK for Python (Boto3)を使います。
Boto3に関して詳細を知りたい方は以下のリンクをご参照ください。

https://aws.amazon.com/jp/sdk-for-python/

外部的なライブラリですので、localでインストールする必要があります。
コマンドは以下です。
```
pip install boto3
```

## S3のバケットにアクセスする

サンプルコードは以下のようです。
コマンドプロントで作られたプロファイル名でs3との通信を確定して、
その後、バケット名と取得したいファイルのパスを指定すれば、そのパス以下のファイルを取得できます。
```Python
import boto3

def downloadS3(local_dir_path, marker) :
    session = boto3.session.Session(profile_name={プロファイル名})
    s3_client = session.client('s3')
    bucket_name = '{バケット名}'
    s3_objects_responses = s3_client.list_objects_v2(Bucket=bucket_name, Prefix='{取得したいフォルダのパス}')['Contents']
```

## S3からファイルをダウンロードする

サンプルコードは以下になります。

```Python
    for s3_object_response in s3_objects_responses:
        # S3オブジェクト側のファイルパスを取得する
        s3_file_path = s3_object_response['Key']

        if s3_file_path.endswith("/"):
            continue

        # S3オブジェクト側のディレクトリ＆ファイルパスとローカル側のディレクトリ＆ファイルパスと取得する
        s3_dir_path = os.path.dirname(s3_file_path)
        s3_file_name = os.path.basename(s3_file_path)

        # S3のファイルパスをローカルの所定のフォルダに変換する
        converted_dir_path_for_local = s3_dir_path.replace('{変換前のパス}', '{変換後のパス}')
        local_object_dir_path =os.path.join(local_dir_path, converted_dir_path_for_local)
        local_object_file_path = os.path.join(local_object_dir_path, s3_file_name)

        # ローカル側にディレクトリパスが存在するか確認する
        if not os.path.exists(local_object_dir_path):
            # ローカル側にディレクトリを作成する
            print("makedirs to local: " + local_object_dir_path)
            os.makedirs(local_object_dir_path)

        # ファイルをダウンロードする
        print("downloadfile[from, to]: [" + s3_file_path + ", " + local_object_file_path + "]")
        s3_client.download_file(bucket_name, s3_file_path, local_object_file_path)

```
bucket以下にさらにフォルダやファイルがいっぱいあるとかがありますので、少し複雑なコードになっていますが、中心的な部分は以下の一行です。
```Python
s3_client.download_file(bucket_name, s3_file_path, local_object_file_path)
```
S3のバケット名、S3にあるファイルのパス、ダウンロード先のlocalのパスを指定すれば、S3からファイルをダウンロードすることが可能です。

## S3にあるファイルが1000件以上である場合

AWSのsdkの仕様上、一回につきダウンロードできるファイルの上限は1000件となります。
1000件を超えた場合、S3 objectにあるisTruncatedフラグが1になります。
以下のコードで、s3_objectsをプリントして、フラグ値を確認すればわかると思います。
```Python
s3_objects = s3_client.list_objects_v2(Bucket=bucket_name, Prefix='{取得したいフォルダのパス}'}
print(s3_objects)
```

そのため、すべてのファイルを一括で取得したい場合は、処理を再帰的に呼び出す必要があります。
それも考慮したコードは以下になります。
markerをつけて、どこまでダウンロードしたかを記録します。
もし1000件以上ある場合は、再帰的に処理を呼び出して、記録したところからダウンロードを再開するというような処理になります。

```Python
def downloadS3(local_dir_path, marker) :
    session = boto3.session.Session(profile_name={プロファイル名})
    s3_client = session.client('s3')
    bucket_name = '{バケット名}'

    # S3バケットからPrefix内のオブジェクト一覧を取得する
    s3_objects = s3_client.list_objects_v2(Bucket=bucket_name, Prefix='{取得したいフォルダのパス}', StartAfter=marker)
    s3_objects_responses = s3_client.list_objects_v2(Bucket=bucket_name, Prefix='{取得したいフォルダのパス}', StartAfter=marker)['Contents']

    for s3_object_response in s3_objects_responses:
        # S3オブジェクト側のファイルパスを取得する
        s3_file_path = s3_object_response['Key']
        if s3_file_path.endswith("/"):
            continue

        # S3オブジェクト側のディレクトリ＆ファイルパスとローカル側のディレクトリ＆ファイルパスと取得する
        s3_dir_path = os.path.dirname(s3_file_path)
        s3_file_name = os.path.basename(s3_file_path)

        # S3のファイルパスをローカルの所定のフォルダに変換する
        converted_dir_path_for_local = s3_dir_path.replace('{変換前のパス}', '{変換後のパス}')
        local_object_dir_path =os.path.join(local_dir_path, converted_dir_path_for_local)
        local_object_file_path = os.path.join(local_object_dir_path, s3_file_name)

        # ローカル側にディレクトリパスが存在するか確認する
        if not os.path.exists(local_object_dir_path):
            # ローカル側にディレクトリを作成する
            print("makedirs to local: " + local_object_dir_path)
            os.makedirs(local_object_dir_path)

        # ファイルをダウンロードする
        print("downloadfile[from, to]: [" + s3_file_path + ", " + local_object_file_path + "]")
        s3_client.download_file(bucket_name, s3_file_path, local_object_file_path)

    #再取得が必要かどうかを判定して、必要がある場合は処理を再度呼び出す
    is_truncated = s3_objects.get(IS_TRUNCATED) 
    print("IsTruncated(1000件以上DLするオブジェクトがあるか) = " + str(is_truncated))
    if is_truncated:
        print(s3_file_path)
        downloadS3(local_dir_path, s3_file_path)
```

# 最後に
以上はAWS S3からファイルをダウンロードするサンプルの紹介になります。
AWSのリソースを操作したい方の参考になれれば幸いです!