MAUIでCSV入出力を堅牢にする — 文字コード・BOM・改行の実務


MAUIでCSV入出力を堅牢にする — 文字コード・BOM・改行の実務

MAUIでCSV入出力を堅牢にする — 文字コード・BOM・改行の実務

GeoDiveExa(GDE)は現場で計測したデータをCSVで書き出し、また他システムから渡されたCSVを読み込む場面があります。座標変換の中身(測地系・投影法)はGeoCoreJP側の話ですが、その手前の「ファイルをどう読み書きするか」でも実務上の落とし穴が多く、地味に時間を取られてきました。今回はC#(.NET MAUI)でのCSV入出力を堅牢にするために気を付けている、文字コード・BOM・改行まわりの実務をまとめます。


現場のCSVは汚い — 列順バラバラ・Shift-JIS・CRLF

測量・GIS業務でやり取りされるCSVは、必ずしも「きれいなUTF-8・LFカンマ区切り」ではありません。実務でよく遭遇するのは次のようなパターンです。

  • 文字コードがShift-JIS(またはCP932):Excelで作成・保存されたCSVは、いまだにShift-JISのことが珍しくない
  • 列順が現場ごとに違う:同じ「緯度・経度・標高」でも、列の並びや列名の表記(lat/緯度/Y座標 など)がファイルによってバラバラ
  • 改行コードがCRLF:Windows由来のファイルはCRLF、Unix系ツールが吐いたファイルはLFと混在することがある
  • 末尾カンマや空行が混ざる:Excelでの手編集の副作用で、意図しない末尾カンマや空行が入っていることがある

「まず届いたファイルをそのまま信用しない」という前提で入出力コードを書く必要がある、というのが実感です。


文字コードの指定 — CodePagesEncodingProviderが必須

.NET(.NET Core以降)は既定でShift-JISやCP932のような古いコードページに対応していません。そのままだとEncoding.GetEncoding("shift_jis")が例外を投げます。MAUIアプリでCP932を扱うには、起動時にCodePagesEncodingProviderを登録しておく必要があります。

// アプリ起動時(MauiProgram.cs など)に一度だけ登録
System.Text.Encoding.RegisterProvider(
    System.Text.CodePagesEncodingProvider.Instance);

// これでCP932(Shift-JIS)が使えるようになる
var sjis = System.Text.Encoding.GetEncoding("shift_jis");

System.Text.Encoding.CodePages パッケージへの参照が必要な点も忘れがちなポイントです。この登録を忘れると「開発機では動いたのに配布ビルドで例外」という形で見つかることがあり、早い段階でアプリ起動処理に組み込んでおくのが安全です。

読み込み側では、文字コードをファイルごとに自動判定するのは難しいため、GDEでは「UTF-8(BOM有無どちらも)」と「Shift-JIS」を候補にして、明らかな文字化け(置換文字の出現など)を検出したら次の候補にフォールバックする、という簡易的な判定を行っています。完全な自動判定は難しいので、最終的にはユーザーが文字コードを選び直せるUIも用意しています。


BOM有無とExcelの挙動

UTF-8で書き出したCSVをExcelで開いたときに文字化けする、という問題はBOM(Byte Order Mark)の有無が原因であることがほとんどです。

  • BOM無しのUTF-8:Windows版Excelでダブルクリックして開くと、日本語列が文字化けすることがある
  • BOM付きのUTF-8:Excelは正しくUTF-8と認識して開ける
  • 一方でBOM付きファイルを他のツール(一部のCSVパーサやコマンドラインツール)に渡すと、先頭列の列名にBOMのバイト列が混入して一致比較に失敗することがある

このため、GDEの書き出し処理では「Excelでの手軽な確認を優先する出力」はBOM付きUTF-8、「他システムへの連携を優先する出力」はBOM無しUTF-8、と用途別に選べるようにしています。

// BOM付きUTF-8で書き出す
var utf8Bom = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: true);
using var writer = new StreamWriter(path, false, utf8Bom);

// BOM無しUTF-8で書き出す
var utf8NoBom = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
using var writerNoBom = new StreamWriter(path, false, utf8NoBom);

読み込み側でも、BOMがあってもなくても列名の比較が失敗しないよう、先頭列名からBOM文字()を明示的に取り除く処理を入れています。


数値の桁・丸めを壊さない書き出し

座標値をCSVに書き出す際、ToString()の既定の桁数やカルチャ設定に頼ると、意図しない丸めや区切り文字の混入が起きます。

  • 既定のカルチャに依存しない:端末の地域設定によっては、小数点がカンマになる(,)カルチャもあり、CSVの区切り文字と衝突する。数値の書式指定には必ずCultureInfo.InvariantCultureを使う
  • 必要な桁数を明示する:緯度経度は小数点以下9桁前後、平面直角座標のXY・標高はミリメートル単位まで、など座標種別ごとに書式文字列(F9F3など)を固定する
  • 指数表記を避ける:極端に小さい値・大きい値でToString()が指数表記(1.23E-05など)を返すことがあるため、書式指定で固定小数表記にする
double lat = 35.681236;
string s = lat.ToString("F9", System.Globalization.CultureInfo.InvariantCulture);
// => "35.681236000"(カルチャに依存せず、必ずピリオド区切り)

GeoConverterPro(GCVP)のCSV変換機能でも同様に、入力CSVの列名・座標形式を明示指定する設計を採っていますが、GDE側は現場での計測ログという性質上、桁数の固定と指数表記の回避を特に意識しています。


テストデータの作り方

文字コード・BOM・改行の問題は、実機で偶然踏むまで気づきにくいバグです。GDEでは次のようなテストデータをあらかじめ用意し、入出力処理の単体テストに使っています。

  • UTF-8(BOM付き)・UTF-8(BOM無し)・Shift-JISの3種類で内容が同じCSVファイル
  • 改行コードがCRLFのファイルとLFのファイル
  • 列名に日本語(全角)を含むファイルと、英数字のみのファイル
  • 末尾に空行があるファイル・末尾カンマがあるファイル
  • 極端な数値(0、負の座標、非常に小さい/大きい小数)を含む行

これらをテストプロジェクトのリソースとして固定し、読み込み結果が期待どおりの値になるか、書き出したファイルを再読み込みして元の値と一致するか(往復テスト)を確認しています。C#で測地計算の単体テストを書くで紹介した検証値ドリブンの考え方と同じく、「実際に起きるパターン」をテストデータとして固定しておくことが、ファイルI/O周りの回帰を防ぐ一番効果的な方法だと感じています。


まとめ

  • 現場のCSVは文字コード・列順・改行コードがバラバラという前提でコードを書く必要がある
  • .NETでShift-JIS/CP932を扱うにはCodePagesEncodingProviderの登録が必須
  • BOM付き/無しはExcelでの見え方と他ツールとの連携のどちらを優先するかで使い分ける
  • 数値の書式指定は必ずInvariantCultureを使い、桁数と指数表記の有無を固定する
  • 文字コード・改行・BOMのパターンを固定したテストデータで往復テストを行うと、実機でしか踏まないバグを事前に検出しやすい

関連記事


開発者より: アプリ・Kindle本・オープンソースの一覧は GitHub: amru195704 にまとめています。


お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Back to top