エクセルに比べれば外部のCSVを扱う処理はまだ悩むことが少ないだろうと油断していたところ、 先日かなり頭を悩ませたので戒めとして記録したいと思います。
何が起こったのか
- ユーザーのCSVをブラウザでロードし、CSV文字列をAPI(.NET Core)へ渡す
- API(.NET Core)でCSV文字列とデータベースの文字列と比較し、比較演算子で同値チェック
- 仕様上、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エンジニアのキャリアを考える!