エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

Unicodeの結合文字列がバグを呼び起こしてしまった

f:id:tansantktk:20210313131938p:plain

エクセルに比べれば外部のCSVを扱う処理はまだ悩むことが少ないだろうと油断していたところ、 先日かなり頭を悩ませたので戒めとして記録したいと思います。

何が起こったのか

  1. ユーザーのCSVをブラウザでロードし、CSV文字列をAPI(.NET Core)へ渡す
  2. API(.NET Core)でCSV文字列とデータベースの文字列と比較し、比較演算子で同値チェック
  3. 仕様上、CSV文字列とデータベースの文字列は原則的に同値になるはずだったが、何故か異なる値に判定されエラーを吐いてしまった

なお、見かけ上のCSV文字列とデータベースの文字列は同値に見えており、おかしなところは見られませんでした。

エラーの原因

CSVファイルにUnicodeの結合文字列が含まれており、常にデータベースの文字列との比較処理が失敗してしまっていました。

Unicodeの結合文字列とは?

Unicodeでは2つの文字列を結合してあたかも1文字に見せる「結合文字列」という記述が可能になっています。

例えば、以下の①、②の文字列ですがどちらも濁点付きの1文字に見えます。ドラッグしても1文字扱いで選択されます。

①が
②が

①と②をC#の比較演算子(==)で比較すると結果はfalseになります。

一方、Unicodeエスケープシーケンスに変換して表記すると以下のようになります。

①\u304c
②\u304b\u3099

①は通常キーボードで入力するような濁点付きの文字列です。

②はUnicodeの結合文字列で、本来別々に存在する2文字を結合し、あたかも1文字のように見せた文字列となっています。 \u304bが「か」、\u3099が濁点です。

Unicodeでこのような記述ができるせいで、比較演算子の処理上では 別の文字列として扱われてしまい、意図せずエラーが発生していたというわけです。


↓のサイトで、結合文字列の作成やエスケープシーケンス変換を試せます。

Unicodeエスケープシーケンス変換|コードをホームページに載せる時に便利 | すぐに使える便利なWEBツール | Tech-Unlimited

ダイアクリティカルマーク(合成可能): ユニコード 0300-036F 文字一覧表(入力Web検索) - 3rdpageSearch Jp

対策

ユーザー側から任意で入力される文字列を比較する場合、比較前に文字列の正規化をし、結合文字列と通常の文字列が同値と判定されるようにします。

有難いことに.NET Coreでは文字列を正規化するための組み込み関数String.Normalize()が用意されていますので、今回はこれを使っていきます。

String.Normalize Method (System) | Microsoft Docs

実装例(.NET Core)

...

// concatenateChar:結合文字列を含む文字列
string concatenateChar = "今夜が山田";
string normalChar = "今夜が山田";

//  concatenateCharを正規化する(=結合文字列が通常の濁点文字に変換される)
concatenateChar = concatenateChar.Normalize();

// 比較結果はtrueになる
if (concatenateChar == normalChar) {
  // true時の任意の処理
}

...

見た目では文字の違いが分からなかったところがかなり辛かった…😭

参考

文字コード地獄秘話 第3話:後戻りの効かないUnicode正規化 | ALBERT Engineer Blog

文字コード再入門 ─ Unicodeでのサロゲートペア、結合文字、正規化、書記素クラスタを理解しよう! - エンジニアHub|若手Webエンジニアのキャリアを考える!