1. NPoi

    Posted

    NPoi
Changes in title
+Amazon S3のUnicode正規化についていろいろと試してみた(2016年8月)
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,314 @@
+発端
+-----
+
+自社サービスのテスト環境から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='npoi-s3test', key=u'MAC_GUI/1_\u3055\u3099\u3053\u306f\u3099')
+MAC_GUI/1_ざこば
+s3.ObjectSummary(bucket_name='npoi-s3test', key=u'MAC_GUI/2_\u30b5\u3099\u30ea\u30ab\u3099\u30cb')
+MAC_GUI/2_ザリガニ
+s3.ObjectSummary(bucket_name='npoi-s3test', key=u'MAC_GUI/3_\uff8b\uff9f\uff6f\uff8b\uff9f')
+MAC_GUI/3_ピッピ
+s3.ObjectSummary(bucket_name='npoi-s3test', key=u'MAC_GUI/4_\u3320')
+MAC_GUI/4_㌠
+s3.ObjectSummary(bucket_name='npoi-s3test', key=u'MAC_GUI/5_\u795e')
+MAC_GUI/5_神
+s3.ObjectSummary(bucket_name='npoi-s3test', 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だ…