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

発端
-----

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

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

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

おさらい
-------

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

- [HFS+のエンコーディングとUnicode正規化 第3版 – ものかの](http://tama-san.com/hfsplus_normalize/)
- [Mac OS XでのUTF-8とWindowsでのNFC/NFD : numa's diary](http://blog.livedoor.jp/numa2666/archives/52345191.html)

まとめますと

- 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<br />選んだものが使われる | 旧字体は新字体に置き換わる |
| 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での扱いを調べました。

```py
#!/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

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

```bash
$ 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の対話モードからの確認ではこんな感じです。

```py
$ 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`で同期させてみます。

```bash
$ 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`を使うとどうでしょうか。

```bash
$ 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だ…