Boto3 で S3 のオブジェクトを操作する（高レベルAPIと低レベルAPI）

AWS SDK for Python である Boto3 について、改めて [ドキュメント](https://boto3.readthedocs.org/en/latest/index.html "Boto 3 Documentation") を見ながら使い方を調べてみた。

## 動作環境

[PyPIのページ](https://pypi.python.org/pypi/boto3/) によると、2系であれば2.6以上、3系では3.3以上で動作するとのこと。

以下は

- Python 3.4.3
- Boto3 1.1.3

の環境で動作確認している。

## Boto3 の構成

[What's New](https://boto3.readthedocs.org/en/latest/guide/new.html) のページにある "Major Features" の項には、次の5つの機能について概要が記載されている。

- **Resources** : 高レベルなオブジェクト指向インターフェース
- **Collections** : 複数のリソースを操作するイテレータ
- **Clients** : 低レベルなサービス接続
- **Paginators** : 自動的なページング
- **Waiters** : 一定の状態に達するまで待機

自分はこの構成を理解できておらず、いままで **Resources** と **Clients** を混同してしまっていた。

なお、高レベルなAPIはすべての AWS サービスで使えるわけではなく、今のところ EC2 や S3 など一部のサービスでしか対応していないようだ。

## インストールと事前準備

pip で簡単にインストールできる。

```console
$ pip install boto3
```

AWS の操作には IAM のアクセスキーが必要なので、あらかじめマネージメントコンソールから作成して、ユーザーに適切なアクセス許可を設定しておく。

端末側ではアクセスキーの情報を `~/.aws/credentials` に設定する。
awscli を使っているのであれば `aws configure` をした際にすでにこのファイルが生成されているが、そうでない場合は awscli のセットアップをするか、 credentials ファイルに直接アクセスキーを設定する。

```ini:~/.aws/credentials
[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY
```

## 高レベルAPIでS3バケットへアクセスする

[S3.Bucket](https://boto3.readthedocs.org/en/latest/reference/services/s3.html#bucket) オブジェクトを通して、バケットへアクセスすることができる。

```python3
import boto3

# バケット名
AWS_S3_BUCKET_NAME = 'hogehoge'

s3 = boto3.resource('s3')
bucket = s3.Bucket(AWS_S3_BUCKET_NAME)

print(bucket.name)
# => hogehoge
```

属性 `objects` を通して、バケットに保存されているS3オブジェクトの情報にアクセスできる。

この属性は `Bucket.objectsCollectionManager` クラスのインスタンスで、 `all()`, `delete()`, `filter()`, `limit()`, `page_size()` のメソッドが利用できる。これらのメソッドは `s3.Bucket.objectsCollection` クラスのインスタンスを返し、このオブジェクトをイテレートすることで `ObjectSummary` クラスのインスタンスを得ることができる。

```python3
print(bucket.objects.all())
# => s3.Bucket.objectsCollection(s3.Bucket(name='hogehoge'), s3.ObjectSummary)

print([obj_summary.key for obj_summary in bucket.objects.all()])
# => ['hayabusa.txt']
```

`objects` を使った操作は、バケットに保存されているオブジェクトを探す場合など対象のオブジェクトが特定されていない場合に有効である。

## 高レベルAPIでS3バケットからオブジェクトを取得する

キーがわかっているS3オブジェクトを取得する場合は、 [S3.Object](https://boto3.readthedocs.org/en/latest/reference/services/s3.html#object) クラスを使う。

```python3
GET_OBJECT_KEY_NAME = 'hayabusa.txt'

obj = bucket.Object(GET_OBJECT_KEY_NAME)

print(obj.key)
# => hayabusa.txt
```

`Object` オブジェクトは Bucket オブジェクトを介さずにバケット名とキー名を指定することで生成することもできる。

```python3
obj = s3.Object(AWS_S3_BUCKET_NAME, GET_OBJECT_KEY_NAME)

print(obj.key)
# => hayabusa.txt
```

S3オブジェクトの中身を取得するには、オブジェクトの `get()` メソッドを使用する。

`get()` メソッドの戻り値は辞書で、その辞書の中にある `Body` を通してオブジェクトの中身を参照することができる。

この `Body` は `botocore.response.StreamingBody` クラスのインスタンスで、バイト型データを扱うストリームとなっている。そのため、文字列として扱うためにはストリームから読み込んで文字列型に変換する必要がある。

```python3
response = obj.get()
body = response['Body'].read()

print(type(body))
# => <class 'bytes'>

print(body.decode('utf-8'))
# => 東京〜新函館北斗 10往復
# 仙台〜新函館北斗 1往復
```

なお、ストリームはいったん `read()` してしまうとストリームの末尾にシークされてしまうため、2回目以降の呼び出しでは結果を取得できないので注意が必要である。

## 高レベルAPIでS3バケットにオブジェクトを追加する

取得と同様、 `Object` オブジェクトを使うことで、新規にバケットへS3オブジェクトを追加したり、中身を更新したりすることができる。

S3オブジェクトの中身を設定するには、 `put()` メソッドの引数 `Body` に保存したい内容をバイト列として渡せばよい。 `ACL` や `ContentType` など、細かなオプションを引数で指定することもできる。

```python3
PUT_OBJECT_KEY_NAME = 'hayate.txt'

obj = bucket.Object(PUT_OBJECT_KEY_NAME)

body = """盛岡〜新函館北斗 1往復
新青森〜新函館北斗 1往復
"""

response = obj.put(
    Body=body.encode('utf-8'),
    ContentEncoding='utf-8',
    ContentType='text/plane'
)
```

## 低レベルAPIを使った操作

[S3.Client](https://boto3.readthedocs.org/en/latest/reference/services/s3.html#client) オブジェクトを使うことで、低レベルなAPIを使用した操作も可能である。

例えば、S3オブジェクトの取得は低レベルAPIを使って次のように書くこともできる。

```python3
s3 = boto3.resource('s3')
client = s3.meta.client

response = client.get_object(Bucket=AWS_S3_BUCKET_NAME, Key=GET_OBJECT_KEY_NAME)
body = response['Body'].read()

print(body.decode('utf-8'))
# => 東京〜新函館北斗 10往復
# 仙台〜新函館北斗 1往復
```

## まとめ

いままでは低レベルなAPI（ Clients ）と高レベルなAPI（ Resources ）を混同して使用していた。

低レベルAPIでしか提供されていない機能も一部あるが、オブジェクト指向なプログラムを書くことができるので高レベルなAPIがあるのであればそちらを使用した方がいいだろう。