発端
自社サービスのテスト環境からAmazon S3にファイルをアップロードし、メタ情報を見ようとキーを指定したものの、一致するものが見当たらず、いろいろ調べていると、ファイル名に濁点が入っていたことによるUnicode正規化の罠に引っかかっていたことに気づいた(気づかせてもらった)。
ちょっと気になったので、ほかの状況で、S3がどのような扱いをしているのかを簡単に調査してみることにしました。
…というのが、2016年8月ごろのお話。今どうなっているかはわかりませんが、いったん記事を公開してみます。
現状のものが反映できたら + Windows でどうなるかが判明したら追記・編集します。
おさらい
Mac(HFS+)やWindows(NTFS)でのファイル名の扱いは、下の各ページを参考にしました。
まとめますと
- MacではNFDをベースにした独自の正規化方式
- Windowsではとくになにもしてない(それぞれ作るとそれぞれ置かれる)
- ファイルシステムレベルじゃあ、NFKC・NFKDは適用できんわなぁ。
ということのようです。
まとめと結論
まとめ
- S3ではUnicode正規化方式に関して特に定めていない
- 複数の方式の混在が可能
- 言語別のSDKでは、すきな方式を選ぶことが出来そう(Pythonしか試してない)
- そのほかのアップロードの仕方では、クライアントによって挙動が変わる
- おおむねOS側の方式を引き継ぐものが多いが、
aws s3 cp
のような例外もあるので注意する
- おおむねOS側の方式を引き継ぐものが多いが、
また、下で行った実験の結果を貼っておきます。
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だ…