Amazon S3のUnicode正規化についていろいろと試してみた(2016年8月)


発端

自社サービスのテスト環境からAmazon S3にファイルをアップロードし、メタ情報を見ようとキーを指定したものの、一致するものが見当たらず、いろいろ調べていると、ファイル名に濁点が入っていたことによるUnicode正規化の罠に引っかかっていたことに気づいた(気づかせてもらった)。

ちょっと気になったので、ほかの状況で、S3がどのような扱いをしているのかを簡単に調査してみることにしました。

…というのが、2016年8月ごろのお話。今どうなっているかはわかりませんが、いったん記事を公開してみます。
現状のものが反映できたら + Windows でどうなるかが判明したら追記・編集します。

おさらい

Mac(HFS+)やWindows(NTFS)でのファイル名の扱いは、下の各ページを参考にしました。

まとめますと

  • MacではNFDをベースにした独自の正規化方式
  • Windowsではとくになにもしてない(それぞれ作るとそれぞれ置かれる)
  • ファイルシステムレベルじゃあ、NFKC・NFKDは適用できんわなぁ。

ということのようです。

まとめと結論

まとめ

  • S3ではUnicode正規化方式に関して特に定めていない
    • 複数の方式の混在が可能
  • 言語別のSDKでは、すきな方式を選ぶことが出来そう(Pythonしか試してない)
  • そのほかのアップロードの仕方では、クライアントによって挙動が変わる
    • おおむねOS側の方式を引き継ぐものが多いが、aws s3 cpのような例外もあるので注意する

また、下で行った実験の結果を貼っておきます。

No. アップロードの仕方/ファイルシステム 正規化方式 備考
1. HFS+   NFD(?) touchコマンドで作成
2. NTFS 未調査 Explorerで作成
3. Python の AWS SDK(boto3) NFC / NFD / NFKC / NFKD
選んだものが使われる
旧字体は新字体に置き換わる
4. bash(Mac) + aws s3 sync NFD 1.のファイル
5. bash(Mac) + aws s3 cp NFC 1.のファイル
6. Chrome(Mac、AWSコンソール) NFD 1.のファイル
7. Chrome(Win、AWSコンソール) NFC 2.のファイル

boto3で旧字体が新字体になるのは、Python側のライブラリのせいな気もします。

結論

  • S3にファイルあげるときに日本語ファイル名使うのやめたほうがいい気がする
    • 使わざるをえない場合でも、違いによりキーのマッチが出来ない場合があることに注意を払うべき
  • 特にaws-cliは、状況によって挙動が変わるので扱いには注意したほうがいい

実験

バケットは便宜上、すべて 適当な名前にしています。

Python用AWS SDK(Boto3)によるファイルのアップロード

以下のようなプログラムを用意し、NFC、NFD、NFKC、NFKDで文字列を正規化した場合のS3での扱いを調べました。

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

import unicodedata

import boto3


FORMS = ['NFC', 'NFKC', 'NFD', 'NFKD']
STRINGS = [u"1_ざこば", u"2_ザリガニ", u"3_ピッピ", u"4_㌠", u"5_神", u"6_{0}".format(unichr(0xfa19))]
BUCKET_NAME = '適当な名前'

def make_files():
    session = boto3.session.Session()
    s3 = session.resource("s3")

    for form in FORMS:
        for string in STRINGS:
            key = u"{0:>7}/{1}".format(form, string)
            key = unicodedata.normalize(form, key)
            obj = s3.Object(BUCKET_NAME, u"{0}".format(key))
            obj.put(Body='test')


def list_files():
    session = boto3.session.Session()
    s3 = session.resource("s3")

    bucket = s3.Bucket(BUCKET_NAME)
    for i in bucket.objects.all():
        print i
        print i.key.encode("utf-8")


if __name__ == "__main__":
    make_files()
    list_files()

結果は以下の通りです。

s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/1_\u3056\u3053\u3070')
    NFC/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/2_\u30b6\u30ea\u30ac\u30cb')
    NFC/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
    NFC/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/4_\u3320')
    NFC/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/5_\u795e')
    NFC/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFC/6_\u795e')
    NFC/6_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/1_\u3055\u3099\u3053\u306f\u3099')
    NFD/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb')
    NFD/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
    NFD/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/4_\u3320')
    NFD/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/5_\u795e')
    NFD/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'    NFD/6_\u795e')
    NFD/6_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/1_\u3056\u3053\u3070')
   NFKC/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/2_\u30b6\u30ea\u30ac\u30cb')
   NFKC/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/3_\u30d4\u30c3\u30d4')
   NFKC/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/4_\u30b5\u30f3\u30c1\u30fc\u30e0')
   NFKC/4_サンチーム
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/5_\u795e')
   NFKC/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKC/6_\u795e')
   NFKC/6_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/1_\u3055\u3099\u3053\u306f\u3099')
   NFKD/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb')
   NFKD/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/3_\u30d2\u309a\u30c3\u30d2\u309a')
   NFKD/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/4_\u30b5\u30f3\u30c1\u30fc\u30e0')
   NFKD/4_サンチーム
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/5_\u795e')
   NFKD/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'   NFKD/6_\u795e')
   NFKD/6_神

こうしてみると、S3のファイルシステムとしては何もしていないことがわかります。

もともとファイルシステムと言っても、S3の場合名前(キー)も単なるメタデータの一種ですし、このあたりはアップロード側に任せるというスタンスなのでしょう。

Mac上のBashからaws-cliを用いてアップロードした場合 その1

まずはファイルの状況です。

$ ls -l
-rw-r--r--  1 npoi  staff     0  8  2 21:43 1_ざこば
-rw-r--r--  1 npoi  staff     0  8  2 21:43 2_ザリガニ
-rw-r--r--  1 npoi  staff     0  8  2 23:42 3_ピッピ
-rw-r--r--  1 npoi  staff     0  8  2 21:43 4_㌠
-rw-r--r--  1 npoi  staff     0  8  2 23:42 5_神
-rw-r--r--  1 npoi  staff     0  8  2 23:08 6_神

また、Pythonの対話モードからの確認ではこんな感じです。

$ python
Python 2.7.11 (default, Dec  5 2015, 14:44:53)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> for i in os.listdir("."):
...     print [i.decode('utf-8')],i
...
[u'.DS_Store'] .DS_Store
[u'1_\u3055\u3099\u3053\u306f\u3099'] 1_ざこば
[u'2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb'] 2_ザリガニ
[u'3_\uff8b\uff9f\uff6f\uff8b\uff9f'] 3_ピッピ
[u'4_\u3320'] 4_
[u'5_\u795e'] 5_神
[u'6_\ufa19'] 6_神
>>>

NFDっぽいですが、5と6は、実際に見るとびっくりしますね。

これをaws s3 syncで同期させてみます。

$ aws s3 sync ./sample/ s3://適当な名前/MAC_CLI
upload: sample/4_㌠ to s3://適当な名前/MAC_CLI/4_㌠
upload: sample/2_ザリガニ to s3://適当な名前/MAC_CLI/2_ザリガニ
upload: sample/6_神 to s3://適当な名前/MAC_CLI/6_神
upload: sample/5_神 to s3://適当な名前/MAC_CLI/5_神
upload: sample/1_ざこば to s3://適当な名前/MAC_CLI/1_ざこば
upload: sample/.DS_Store to s3://適当な名前/MAC_CLI/.DS_Store
upload: sample/3_ピッピ to s3://適当な名前/MAC_CLI/3_ピッピ

この状態で、さきほどPythonのSDKからアップロードしたあとの確認で使った関数で確認をすると、こんな風になっていました。

s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/1_\u3055\u3099\u3053\u306f\u3099')
MAC_CLI/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb')
MAC_CLI/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
MAC_CLI/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/4_\u3320')
MAC_CLI/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/5_\u795e')
MAC_CLI/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI/6_\ufa19')
MAC_CLI/6_神

旧字体もきちんとアップロード出来ているようですし、HFS+のNFDのままアップロードされているように見えます。

Mac上のBashからaws-cliを用いてアップロードした場合 その2

先ほどはaws s3 syncを使いましたが、aws s3 cpを使うとどうでしょうか。

$ aws s3 cp 1_ざこば s3://適当な名前/MAC_CLI2/
upload: 1_ざこば to s3://適当な名前/MAC_CLI2/1_ざこば
$ aws s3 cp 2_ザリガニ s3://適当な名前/MAC_CLI2/
upload: ./2_ザリガニ to s3://適当な名前/MAC_CLI2/2_ザリガニ
$ aws s3 cp 3_ピッピ s3://適当な名前/MAC_CLI2/
upload: ./3_ピッピ to s3://適当な名前/MAC_CLI2/3_ピッピ
$ aws s3 cp 4_㌠ s3://適当な名前/MAC_CLI2
upload: ./4_㌠ to s3://適当な名前/MAC_CLI2
$ aws s3 cp 4_㌠ s3://適当な名前/MAC_CLI2/
upload: ./4_㌠ to s3://適当な名前/MAC_CLI2/4_㌠
$ aws s3 cp 5_神 s3://適当な名前/MAC_CLI2/
upload: ./5_神 to s3://適当な名前/MAC_CLI2/5_神
$ aws s3 cp 6_神 s3://適当な名前/MAC_CLI2/
upload: ./6_神 to s3://適当な名前/MAC_CLI2/6_神

同じようにPythonのプログラムで確認します。

s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/1_\u3056\u3053\u3070')
MAC_CLI2/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/2_\u30b6\u30ea\u30ac\u30cb')
MAC_CLI2/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
MAC_CLI2/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/4_\u3320')
MAC_CLI2/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/5_\u795e')
MAC_CLI2/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_CLI2/6_\ufa19')
MAC_CLI2/6_神

なんとNFC。

Mac上のChromeを用いてコンソールからアップロードした場合

s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/1_\u3055\u3099\u3053\u306f\u3099')
MAC_GUI/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb')
MAC_GUI/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
MAC_GUI/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/4_\u3320')
MAC_GUI/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/5_\u795e')
MAC_GUI/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'MAC_GUI/6_\ufa19')
MAC_GUI/6_神

NFDっぽいですね。aws s3 syncを使ったときと同じに見えます。

Windows上のChromeを用いてコンソールからアップロードした場合

s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/1_\u3056\u3053\u3070')
WIN_GUI/1_ざこば
s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/2_\u30b6\u30ea\u30ac\u30cb')
WIN_GUI/2_ザリガニ
s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
WIN_GUI/3_ピッピ
s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/4_\u3320')
WIN_GUI/4_㌠
s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/5_\u795e')
WIN_GUI/5_神
s3.ObjectSummary(bucket_name='適当な名前', key=u'WIN_GUI/6_\ufa19')
WIN_GUI/6_神

NFCだ…