Python
S3

S3をpathlibっぽく操作する実装をするときに役立つモジュール

Python3.4以降、標準ライブラリにpathlibが追加され、「オブジェクト指向っぽくファイルを操作」できます。同じようにS3のパスを操作できないか、調べてみました。

S3をpathlibっぽく操作できると便利な気がしたので、いろいろ調べています。

ちなみにrequestsやpipenvで有名なkennethreitzさんがs3monkeyというライブラリを作っているようなので、調べてます。

globの実装

パターンに一致するファイルを列挙するメソッドです。公式ドキュメントのコードを見るとこんな感じ。

>>> sorted(Path('.').glob('*.py'))
[PosixPath('pathlib.py'), PosixPath('setup.py'), PosixPath('test_pathlib.py')]
>>> sorted(Path('.').glob('*/*.py'))
[PosixPath('docs/conf.py')]

ただ、globモジュールは実際のファイルパスに機能してしまうため、globの記法(UNIXのファイルパスの記法)によるマッチングだけ行うにはfnmatchモジュールを使うようです。

これのfnmatchfnfilterが使えそうですね。

import fnmatch
import os

for file in os.listdir('.'):
    if fnmatch.fnmatch(file, '*.txt'):
        print(file)

s3のバケットとキーを分割する

StackOverflowの「s3 urls - get bucket name and path」を参考に、こんな関数を用意しました。

from urllib.parse import urlparse

def split_s3_path(s3_path):
    parsed = urlparse(s3_path)
    return parsed.netloc, parsed.path.lstrip('/')

bucket, key = split_s3_path('s3://bucket_name/folder1/folder2/file1.json')

print(bucket)
# => 'bucket_name'
print(key)
# => 'folder1/folder2/file1.json'

一つ下の階層を取得する

boto3のlist_objectsメソッドは、ディレクトリ以下のファイルを再帰的にリストアップしてしまいます。本当はディレクトリに見えている"/"は、単なるKeyの一部なのでこういう挙動のほうが自然ではあるのですが。

1階層下だけを取得する場合はちょっと工夫する必要があります。これもStackOverflowの「Retrieving subfolders names in S3 bucket from boto3
」を参考に実装できます。

import boto3

def ls(s3_path):
    # lsの対象がディレクトリのときはうまく機能するが、ファイルのときは空リストが返ってきてしまうので要実装
    client = boto3.client('s3')
    bk, key = split_s3_path(s3_path)
    # rootのときだけ空文字
    if key:
        key = f'{key}/'
    result = client.list_objects(Bucket=bk, Prefix=key, Delimiter='/')
    return [c['Prefix'] for c in result.get('CommonPrefixes', [])]

オンメモリでファイルを開く

get_objectの結果を読み込むと、バイナリで返ってくるので、utf-8の文字列に変換する必要があります。

def s3_read(s3_path):
    client = boto3.client('s3')
    bk, key = split_s3_path(s3_path)
    result = client.get_object(Bucket=bk, Key=key)
    return result['Body'].read().decode('utf-8')

pathlibっぽくwith句を使うのはもうひと工夫必要そうです。