【Apex】CSVをsplitで分割した際、文字列の比較が上手くいかない場合に疑う点
ApexでCSVファイルを読み込む処理を作る話をもらった際、下記の要件がありました。
読み込むCSVファイルの1行目はヘッダー行であり、 ヘッダーの列名が想定と異なる場合にはエラーとする。 |
ありがちな仕様ですし実装イメージも明確だったので特に気にせずOKしました。が、コーディング後に動作確認してみると上手くいかない!見た目は全く同じ文字列なのに、比較結果がfalseになってしまうという・・
結論から言うと、見た目は同じ文字列でもバイナリで比較すると違かった。とゆうオチでした。
疑うべき点を紹介します。
疑う点① BOM
読み込むCSVファイルの文字コードがUTF-8等のユニコードで、且つBOM付きの場合、ファイルの先頭3バイトはBOM情報になります。このBOMが付いたままだと一見同じ文字列に見えても、Stringクラスの比較はバイナリレベルで行われるので不一致となってしまいます。
簡単な例を書きます。
例えばこんなCSVを読み込むとします(UTF-8のBOM付き)。
No,名前 1,さくら 2,穂波 3,野口 |
このCSVを読み込む処理が下記Apexです(抜粋)。ヘッダーの1列目が"No"だったらデバッグログに"OK"を出力するという処理になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 読み込んだCSVデータを1行毎Listに格納 String body = csv.Body.toString(); List<String> lines = body.split('\n'); // ヘッダー行を1列毎Listに格納 String header = lines.get(0); List<String> headerCols = header.split(','); // 1列目の比較 String col1 = headerCols.get(0); System.debug('読み込んだ値:' + col1); If (col1.equals('No')) { System.debug('OK!'); } else { System.debug('error..'); } |
読み込ませるCSVを見ると1列目が"No"なので、一見上手くいきそうです。しかし結果はこうなってしまいます。
USER_DEBUG [30]|DEBUG|読み込んだ値:No USER_DEBUG [34]|DEBUG|error.. |
これは先で書いたように、CSVから読み込んだ1列目の文字列にBOMが付いているため、バイナリレベルで見ると異なるためです。実際にバイナリサイズを出力してみると3バイト分サイズが違うことが分かります。
1 2 3 4 |
String col1 = headerCols.get(0); System.debug('読み込んだ値:' + col1); System.debug('読み込んだ値のバイナリサイズ:' + Blob.valueOf(col1).size()); System.debug('期待値のバイナリサイズ:' + Blob.valueOf('No').size()); |
USER_DEBUG [30]|DEBUG|読み込んだ値:No USER_DEBUG [31]|DEBUG|読み込んだ値のバイナリサイズ:5 USER_DEBUG [32]|DEBUG|期待値のバイナリサイズ:2 |
疑う点② 改行コード
読み込んだCSVデータを1行毎に分解するには、改行コードでsplitしていくことになると思います。その際、誤った改行コードで分解するとバイナリレベルでゴミが残ってしまい文字列の比較が上手くいかないことがあります。
こちらも例を書きます。
先ほどと同じくこんなCSVを読み込むこととします(UTF-8)。改行コードはCR+LFです。
No,名前 1,さくら 2,穂波 3,野口 |
そして下記がApexで書いたCSV読込み処理の抜粋です。ヘッダーの2列目が"名前"だったら"OK"を出力するという処理になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 読み込んだCSVデータを1行毎Listに格納 String body = csv.Body.toString(); List<String> lines = body.split('\n'); // ヘッダー行を1列毎Listに格納 String header = lines.get(0); List<String> headerCols = header.split(','); // 2列目の比較 String col2 = headerCols.get(1); System.debug('読み込んだ値:' + col2); System.debug('読み込んだ値のバイナリサイズ:' + Blob.valueOf(col2).size()); System.debug('期待値のバイナリサイズ:' + Blob.valueOf('名前').size()); If (col2.equals('名前')) { System.debug('OK!'); } else { System.debug('error..'); } |
こちらも先ほどのBOM付きと同じく、一見OKとなりそうですがerror..となります。バイナリサイズを見ると差があることが分かります。
USER_DEBUG [30]|DEBUG|読み込んだ値:名前 USER_DEBUG [31]|DEBUG|読み込んだ値のバイナリサイズ:7 USER_DEBUG [32]|DEBUG|期待値のバイナリサイズ:6 USER_DEBUG [37]|DEBUG|error.. |
この原因は、改行コードがCR+LFなのに\nでsplitしていることにあります。\r\nでsplitすれば上手くいきます。
1 2 3 |
// 読み込んだCSVデータを1行毎Listに格納 String body = csv.Body.toString(); List<String> lines = body.split('\r\n'); |
終わりに
CSVをsplitで分割した際、文字列の比較が上手くいかない場合に疑う点として、BOMと改行コードを挙げました。これらはExcelファイルを「CSV UTF-8(カンマ区切り)」で保存した際の形式です。運用的にはあり得るので要件を詰める際には確認しておきたいですね。

ちなみにBOMの除去ですが、Apexで不要なバイトの除去方法について情報が無く、今回は解決方法まで言及できませんでした・・。
しかし言語を問わず、様々な文字コードや改行コードに処理を対応させるのは難しいと思うので、どのようなCSVを読み込む必要があるのか予め要件を詰めておくのが良さそうですね。