Pythonで扱うS3のパスをs3pathlibでイケてる記述にする

こんにちは。R&D室のsiidaです。R&D室ではストレージとしてS3を使用し、分析環境としてSageMakerを使用しています。そのためPythonからS3を使用することが多いのですが、今回はその際に便利な s3pathlib についてご紹介します。

TL;DR

PythonでS3のファイルパスを扱う場合には s3pathlib が便利です。組み込みモジュールの pathlib では s3:// から始まるS3のパスをパースすることができませんが s3pathlib なら pathlib と同じように扱うことができます。

from pathlib import Path
from s3pathlib import S3Path

local_dir = Path("./my_dir")
s3_dir = S3Path("s3://my_bucket/my_dir")

if local_dir.exists():
    print(f"ローカルのディレクトリ {local_dir} は既に存在します。")

if s3_dir.exists():
    print(f"S3上のディレクトリ {s3_dir} は既に存在します。")

私は s3pathlib を知る前は str 型でS3上のパスを処理していたのですが、 str 型でパスを記述することは型の安全性の観点から推奨されませんし、上記の exists()joinpath() , parent といった pathlib で実装されている機能を代替するのに手間がかかることからフラストレーションを感じていました。

そんな悩みも s3pathlib で解決です。

s3pathlib.readthedocs.io

S3は厳密にはディレクトリ構造を持たずKey/Valueによるフラットな構造を採用していますが、疑似的にディレクトリ構造のように扱うことができます。また s3pathlib のドキュメント上でもディレクトリという用語が使用されています。そのため、この記事でも便宜上ディレクトリという用語を使用します。

Amazon S3 has a flat structure instead of a hierarchy like you would see in a file system. However, for the sake of organizational simplicity, the Amazon S3 console supports the folder concept as a means of grouping objects.

docs.aws.amazon.com

環境

AWS CLIが使用できる環境をご利用ください。

インストール方法

pipで配布されています。

pip install s3pathlib

s3pathlib チートシート

よく使う機能を一通りまとめます。

S3Path オブジェクトを定義する

ファイルの場合

  • パスをそのまま貼り付ける
s3_file = S3Path("s3://my_bucket/my_dir/my_file.txt")
  • パスをバケット・ディレクトリ・ファイルに分割して指定する
s3_file = S3Path("my_bucket", "my_dir", "my_file.txt")
  • パスをARN表記で指定する
s3_file = S3Path("arn:aws:s3:::my_bucket/my_dir/my_file.txt")

ディレクトリの場合

ファイルの場合とほとんど同じ。

  • パスをそのまま貼り付ける
s3_dir = S3Path("s3://my_bucket/my_dir/")
  • パスをバケット・ディレクトリ・ファイルに分割して指定する
s3_dir = S3Path("my_bucket", "my_dir")
  • パスをARN表記で指定する
s3_dir = S3Path("arn:aws:s3:::my_bucket/my_dir/)
  • 末尾が / でない場合はディレクトリとして認識されないので注意する
>>> s3_dir = S3Path("s3://my_bucket/my_dir/")
>>> print(s3_dir.is_dir())
True
>>> s3_dir = S3Path("s3://my_bucket/my_dir")
>>> print(s3_dir.is_dir())
False

S3Path オブジェクトの情報を取得する

便宜上 S3Path のメンバとして表記していますが、ソースはMixInクラス上にあります。記事中の他のメンバについても同様です。

S3Path.uri: URIを取得する

>>> print(s3_file.uri)
s3://my_bucket/my_dir/my_file.txt

S3Path.bucket: バケットを取得する

>>> print(s3_file.bucket)
my_bucket/my_dir/my_file.txt

S3Path.key: パスを取得する

>>> print(s3_file.key)
my_bucket/my_dir/my_file.txt

アップロード

S3path.upload_file(): ファイルをアップロード

  • ローカル上に存在する my_file.txt をS3のパス s3://my_bucket/my_dir/my_file.txt へアップロード
local_file = Path("my_file.txt")
s3_file = S3Path("s3://my_bucket/my_dir/my_file.txt")
s3_file.upload_file(local_file)
  • 上書きを許可する場合
local_file = Path("my_file.txt")
s3_file = S3Path("s3://my_bucket/my_dir/my_file.txt")
s3_file.upload_file(local_file, overwrite=True)

S3path.upload_dir(): ディレクトリをアップロード

  • ローカル上に存在する my_dir/ をS3のパス s3://my_bucket/my_dir/ へアップロード
local_dir = Path("my_dir/")
s3_dir = S3Path("s3://my_bucket/my_dir/")
s3_dir.upload_dir(local_dir)
  • 上書きを許可する場合
local_dir = Path("my_dir/")
s3_dir = S3Path("s3://my_bucket/my_dir/")
s3_dir.upload_dir(local_dir, overwrite=True)

ダウンロード

boto3 モジュールに S3Path を組み合わせることで、ダウンロード用のスクリプトを記述できます。

ファイルをダウンロード

単一のファイルをダウンロードする機能は、次のように実装できます。

from pathlib import Path

import boto3
from s3pathlib import S3Path


def download_file_from_s3(src_path: S3Path, *, dst_dir: Path = Path("./")):`
    """Download a file from input S3 path.

    Args:
        src_path (S3Path): Directory of file path on S3.
        dst_dir (Path, optional): Directory path on local.
    """
    s3_resource = boto3.resource("s3")
    bucket = s3_resource.Bucket(src_path.bucket)
    dst_path = dst_dir.joinpath(Path(src_path).name)
    bucket.download_file(src_path, dst_path)
    print(f"Downloaded to {dst_path}")

ディレクトリをダウンロード

また、ディレクトリ毎すべてのファイルとサブディレクトリをダウンロードする機能は、次のように実装できます。

from pathlib import Path

import boto3
from s3pathlib import S3Path
from tqdm import tqdm


def download_from_s3(src_path: S3Path, *, dst_dir: Path = Path("./")):
    """Download all files from input S3 path.

    Args:
        src_path (S3Path): Directory of file path on S3.
        dst_dir (Path, optional): Directory path on local.
    """
    s3_resource = boto3.resource("s3")
    bucket = s3_resource.Bucket(src_path.bucket)

    dst_path = Path("./")
    for filepath in tqdm(get_s3_files(src_path)):
        dst_path = dst_dir.joinpath(Path(filepath).name)
        dst_path.parent.mkdir(exist_ok=True, parents=True)
        if dst_path.exists():
            print(f"{dst_path} already exists.")
            continue
        bucket.download_file(filepath, dst_path)
        print(f"Downloaded to {dst_path}")

その他S3の操作

S3Path.mkdir(): S3上にディレクトリを作成する

>>> s3_dir = S3Path("s3://my_bucket/my_dir/").to_dir()
>>> s3_dir.mkdir(exist_ok=True)

S3Path.write_text(), S3Path.read_text(): S3上のファイルに読み書きする

>>> s3_file = S3Path("s3://my_bucket/my_dir/my_file.txt")
>>> s3_file.write_text("hoge")
>>> s3_file.read_text()
'hoge'

その他pathlibでよく使う機能に相当するもの

S3Path.joinpath(): 下の階層のパスをつなげる

>>> s3_dir = S3Path("s3://my_bucket/my_dir/")
>>> s3_file = s3_dir.joinpath("my_file.txt")
>>> print(s3_file.uri)
s3://my_bucket/my_dir/my_file.txt

S3Path.exists(): S3上に存在するか確かめる

  • ディレクトリでもファイルでも同様
>>> s3_dir = S3Path("s3://my_bucket/my_dir/")
>>> print(s3_dir.exists()
True
>>> s3_dir = S3Path("s3://my_bucket/pseudo_my_dir/")
>>> print(s3_dir.exists())
False

S3Path.is_dir(): パスがディレクトリかファイルか判定する

>>> s3_dir = S3Path("s3://my_bucket/my_dir/")
>>> print(s3_dir.is_dir())
True

まとめ

一通り s3pathlib の主要な機能を挙げてみましたが、この中でも特に joinpath() とアップロード・ダウンロードの組み合わせは大変便利です。これだけでS3のパスを str で記述して split() でバラして...というような非効率なコードから解放されるようになります。

S3を頻繁に使うPythonエンジニアのみなさんはぜひ使ってみてください。


本記事は Qiitaで投稿した記事と同じ内容です。 アクセス解析を目的としてマルチポストしています。

qiita.com

また弥生では一緒に働く仲間を募集しています。 ぜひエントリーお待ちしております。

herp.careers