0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

boto3 x S3をほりほり(Lambda)Part 1

0
Posted at

S3 基礎(put/get/delete/list)

Lambda+S3はAWSサーバレスで頻繁に利用される。
文字列・CSV・JSON・バイナリ・設定ファイル...S3に配置して処理する。

ただし、boto3のS3APIは”つまずきポイント”多数あり、理解していないとデータ破損、ファイル取得ミス、prefix違いの事故が起きます。

1.ファイルアップロード(put_object)

➤サンプル

s3 = boto3.client("s3")

def put_text(bucket: str, key: str, text: str):
    s3.put_object(
        Bucket=bucket,
        Key=key,
        Body=text.encode("utf-8"),
        ContentType="text/plain",
    )

:bulb:ポイント

  • Bodyはstrではなくbytes
  • 文字コードは必ずUTF-8指定(事故防止)
  • ContentTypeを指定しないとブラウザがご解釈することも
    → PythonはstrをそのままS3に送れない。暗黙的に変換して送るため、想定外の挙動のならないため明示的に送ってあげる必要がある。またS3はバイナリストレージのため人が見てわかるようContentTypeを設定してあげる。

アンチパターン:文字列をそのまま入れる

s3.put_object(Bucket=b, Key=k, Body="hello") 
  • python内部で勝手にencodeing → AWS側の仕様変更で壊れる可能性が潜む

2.テキストファイル読み込み(get_object)

➤サンプル

def get_text(bucket, key) -> str:
    resp = s3.get_object(Bucket=bucket, Key=key)
    return resp["Body"].read().decode("utf-8")

==============================================
import gzip

def get_text(bucket, key) -> str:
    resp = s3.get_object(Bucket=bucket, Key=key)
    raw = resp["Body"].read()

    if resp.get("ContentEncoding") == "gzip":
        raw = gzip.decompress(raw)

    return raw.decode("utf-8")

:bulb:ポイント

  • BodyはStreamingBodyという型でファイルの中身を持っているわけではなく、データを少しずつ読みだせ、一気にメモリに載せない。取り出すにはread()で取り出す
  • decodeしないとStreamingBody.read() の結果は 必ず bytes
  • レスポンスがgzip圧縮されてる場合にはdecodeの前に解凍が必要

アンチパターン:Bodyをそのまま返す

content = resp["Body"]

この場合、この処理時点ではエラーも出ず、テキスト、Jsonなど期待した結果は入っておらず
StreamingBodyのオブジェクトが入っているだけで取り出したファイルを処理する際エラーとなる。

3.JSON読み書き

JSON 保存

import json

def put_json(bucket, key, obj):
    s3.put_object(
        Bucket=bucket,
        Key=key,
        Body=json.dumps(obj).encode("utf-8"),
        ContentType="application/json",
    )

JSON 読み込み

def get_json(bucket, key):
    resp = s3.get_object(Bucket=bucket, Key=key)
    text = resp["Body"].read().decode("utf-8")
    return json.loads(text)

アンチパターン:json.loads()にbytesを直接渡す

data = json.loads(resp["Body"].read())

pythonはbytes → JSONパースできない

:pencil2: json.loadsって
JSON形式の文字列(str)を、Pythonの辞書・リストに変換する関数。
例:'{"a": 1}'{"a": 1}


S3 の「なんちゃってフォルダ構造」

S3はフォルダがないオブジェクトストレージ
見た目は階層的だが、中身はただのキー名

その結果実装で様々な事故に...

  • prefixが曖昧で関係ないファイルまでヒット
  • listが最大1000件までしか返さない
  • フォルダのつもりで末尾 / の扱いをミスる

1.基本 list_objets_v2(最大1000件まで)

resp = s3.list_objects_v2(Bucket=bucket, Prefix=prefix)
contents = resp.get("Contents", [])

:bulb:ポイント

  • Contentsが0件の時S3はキー自体を返さないためKeyErrorになる
    → 対策: .get("Contents", []) は実務必須の安全策。
  • S3 の LIST API は 最大 1000 件までしか返さない
    → 対策: 1000 件超は NextContinuationToken を使って繰り返し取得

2. 1000件を超える場合(必須:ContinuationToken)

def list_all(bucket, prefix):
    token = None
    keys = []

    while True:
        params = {"Bucket": bucket, "Prefix": prefix}
        if token:
            params["ContinuationToken"] = token

        resp = s3.list_objects_v2(**params)

        for obj in resp.get("Contents", []):
            keys.append(obj["Key"])

        if resp.get("IsTruncated"):  # まだ続きがある
            token = resp.get("NextContinuationToken")
        else:
            break

    return keys

やってることざっくり

  • paramsの初期化
    S3に渡すパラメータを作る。
  • s3.list_objects_v2(**params)
    prefixに一致する最大1000件のリストが返る。
  • .get("Contents", [])
    prefixに一致するオブジェクトが0件の時Contensキーを返さない
  • if resp.get("IsTruncated")
    IsTruncatedはbool型(True / False)で返ってくる
    Trueで1000件以上あり、レスポンスが途中まで、False=これで全部

:warning: prefixはフォルダっぽい検索だけど、S3はあくまで文字列の一致

アンチパターン:# Prefixのファイル全部取りたい!!

resp = s3.list_objects_v2(Bucket=b, Prefix=p)
keys = [c["Key"] for c in resp["Contents"]]  

:warning: 1000件以上あると永遠に残りが取れない


Prefixの罠

何度も書いているが、S3のフォルダは実態ではなく「ただのキー名の一部

dir1/file1.txt
dir1/file2.txt
dir10/file3.txt
こんな感じの時prefixを"dir1"で検索するとdir10までヒットしてしまう。

1.末尾スラッシュ("/")これ絶対:bangbang:

正しい例

prefix = "dir1/"

まずい例:

prefix = "dir1"

こうすることでdir1に絞りdir10,dir100がヒットしない。

2.「/」位置ずれ

S3内のキー:data/2025/...というのがあるとして

date = "2025/"
prefix = f"data/{date} # NG
prefix = f"data/{date}/ # OK

この場合NGパターンは一切ヒットしない。
OKパターンのようにすることで必ずフォルダ風の prefix になる

S3はホントに「キー文字列一致」の世界で
1文字ズレただけで見つからない or 取りすぎの事故発生

3.S3でフォルダを作る方法

AWSのコンソールでフォルダ作成から「my-dir」を作成してみる
image.png

ん?フォルダの概念はないのでは:interrobang:
実態は、ただのキー(文字列)であったフォルダではない。
フォルダを表現するための0バイトオブジェクト

なぜ知っておく必要があるか
狙った操作が正確に行えず事故の原因となってしまう可能性があるので知識として

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?