この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。
TL;DR
表示上は全く同じように見える文字列同士が==演算子でfalseを返すことがある。一つの原因として考えられるのはUnicodeが合成文字という仕組みにより濁音/半濁音を別扱いしているというもの。Unicode、また君か? 解決方法はUnicode正規化をすること。例えばrailsのActive Supoortなら以下のように正規化できる。
ActiveSupport::Multibyte::Unicode.normalize(string, :kc)
現象と原因の特定
この現象に遭遇したのはxmlからパースした文字列をrspecで期待値通りかチェックしていた時のこと。このように表示上は同じように表示されるが==(eq)演算子ではfalse扱い。
str1
#=> "電話番号 : 入力データが不正です"
str2
#=> "電話番号 : 入力データが不正です"
str1 == str2
#=> false
そこで、まずはバイトサイズを確認すると一致しない。そこでエンコーディングの差を疑って文字列のエンコーディングを確認するがこれも一致する。
[str1.bytesize, str2.bytesize]
#=> [54, 45]
[str1.encoding, str2.encoding]
#=> [#<Encoding:UTF-8>, #<Encoding:UTF-8>]
では何が違うかというと前述したとおり、str1では濁音/半濁音の記号が分離されている。これはString#unpackで文字列をコードポイントに変換したり、String#charsで文字単位に分割するとわかる。
str1.unpack("U*")
#=> [38651, 35441, 30058, 21495, 32, 58, 32, 20837, 21147, 12486, 12441, 12540, 12479, 12363, 12441, 19981, 27491, 12390, 12441, 12377]
str2.unpack("U*")
#=> [38651, 35441, 30058, 21495, 32, 58, 32, 20837, 21147, 12487, 12540, 12479, 12364, 19981, 27491, 12391, 12377]
str1.chars
#=> ["電", "話", "番", "号", " ", ":", " ", "入", "力", "テ", "゙", "ー", "タ", "か", "゙", "不", "正", "て", "゙", "す"]
str2.chars
#=> ["電", "話", "番", "号", " ", ":", " ", "入", "力", "デ", "ー", "タ", "が", "不", "正", "で", "す"]
これにはUnicodeの合成文字という仕組みが関係している。合成文字/結合文字(combining character)は非結合文字の直後に結合文字を配置することで結合文字列(CCS)を作る仕組みのこと。簡単にいえば「が」という文字列を「か(非結合文字)」「<濁音記号>(結合文字)」という2つのコードポイントで表してもいいということだ。ややこしいのが、Unicodeは「が」という一つのコードポイントで表しても良い点で、つまり同じ文字を表すのに異なるデータの表現方法がある。
当然、バイト列が違うのでそれぞれの表現でバイト数さえ食い違う。このままでは色々な問題が発生するため、これらのバラバラな表現を統一するように正規化することが出来る。
str1.bytesize
#=> 54
ActiveSupport::Multibyte::Unicode.normalize(str1).bytesize
#=> 45
ActiveSupport::Multibyte::Unicode.normalize(str1) == str2
#=> true
ちなみにUnicode正規化にはNFD、NFCなど複数の形式があり、上記のコードでは省略しているが、normalizeメソッドは2番めの引数に正規化形式を指定することが出来る。ex. normalize(str, :nfc)
ところで色々な問題が発生する、と書いたが面白いのはセキュリティに関する問題を引き起こすこと。文字列の比較はセキュリティに深く関わるトピックであり、XSSを防ぐためのエスケープにせよ、パスワードの認証にせよ、文字列が正しく比較出来ることを前提にしている。その前提をいじれてしまうのなら色々なことが出来てしまう。
考えられる他の原因
表示上は同じだがデータは異なる、という文字列比較のややこしい問題は他にも色々ある。やはり文字列データは非常に複雑なデータ形式ということだろうか。こういった問題が不完全な抽象化から抜け出せる日はまだ遠い。主が怒り全地の言葉を乱し人を全地に散らされたために人々は当面の間は苦しむ。
- 非最短形式
- 多対一の変換
- 大文字と小文字
- エンコード情報の不一致
- 7ビットエンコーディングの解釈