CSV,TSVなどの文字区切りテキストをパースする
連載記事C#でCSVファイル操作ライブラリ の最初の記事です。
動作的には、 TextFieldParser クラス (Microsoft.VisualBasic.FileIO) のReadFields()メソッドに準じますが、TextFieldPaser にはC# で CSV を扱うのに CsvHelper を使う - dunno logs に記載されているような問題があるようですので、このあたりの改良も含め、以下の仕様で実装してみたいと思います。
仕様
- 1レコード(行)は改行文字(※1)で区切られる
- フィールドの区切り文字には1文字以上の文字列を複数指定できる
{ "," }
(カンマ区切り){ "\t", " " }
(タブとスペースが混在){ "::" }
(コロン2つで区切る) など
- フィールドに以下の文字を含む場合はダブルクオーテーションで囲む
- 区切り文字に指定した文字列
- 改行文字(※1)
- ダブルクオーテーション
- フィールド前後の空白(※2)
- 上記以外のフィールドの場合でもダブルクオーテーションで囲んでいる場合がある
- フィールド内のダブルクオーテーションは2つ重ねてエスケープする
- フィールド前後の空白(※2)は削除する
(※1) 今回はTextReader.ReadLine()等で読み取った1行を解析するため、改行文字は使用するTextReaderの実装によります。
System.IO.StreamReader
では'\r', '\n', '\r\n'
を行区切りと認識します。
(※2) ここで認識される空白文字はchar.IsWhitespace(char)
でtrueを返す文字です。半角、全角スペース、タブなどを含みます。 )
Parseメソッド
1行分の文字列を受け取ってフィールドに分割し、文字列のシーケンスとして返すメソッドを定義します。
class XsvReader { public static IEnumerable<string> Parse(string line, ICollection<string> delimiters, Func<string> followingLineSelector); }
line
パラメータには、処理したい1行分の文字列を渡します。delimiters
パラメータで、区切り文字を指定します。followingLineSelector
パラメータには、次の1行を提供する処理を記述します。(フィールド内に改行を含む場合に、後続の行が必要となります。)
これを使用する側の実装は次のようになります。
var csvData = new List<string[]>(); using(var reader = new StreamReader("filename.csv")) { while(!reader.EndOfStream) { var row = XsvReader.Parse(reader.ReadLine(), new []{","}, ()=> reader.ReadLine()); csvData.Add(row.ToArray()); } }
この例ではSystem.IO.StreamReader
を使用し、
ReadLine()
メソッドで読み込んだ1行分の文字列を第1引数に渡しています。
さらに、第3引数に渡すデリゲート内で追加の1行をReadLine()
する処理を記述しています。
ソースコード
以下、Parseメソッドの全容です。
処理の概要は、ソースコード内のコメントを参照ください。割とオーソドックスな?実装になっていると思います。
※ 実行速度についてはあまり考慮していません。簡単に計測した結果では、TextFieldPaser の1.5倍~2倍の実行時間でした。
実用に耐えるレベルではあると思いますが、これは今後の課題にしておきます。
実行してみる
次のテキストを使って動作確認をします。
No., Name, Price, Comment<n> 001, りんご, 98円, 青森産<n> 002, バナナ, 120円, " とっても! お,い,し,い,よ!"<n> <n> 004, "うまい棒""めんたい""", 10円,<n> 005, バナメイ海老, 800円, "300g エビチリに" (※ `<n>` は、レコード終わりの改行を分かりやすくするために付加したマークです。 実際に使用するテキストには含まれません。)
比較のために、TextFieldParser クラス (Microsoft.VisualBasic.FileIO)
のReadFields()
メソッドでの実行結果も見てみます。
実行結果
まずは、TextFieldParser の出力結果
フィールド前後の空白を削除する/しないを設定するTextFieldParser.TrimWhiteSpac
プロパティの値によって、出力結果が異なりますので両方で実行してみます。
TextFieldParser で問題とされている、以下の2点に注意して出力結果を見てみましょう。
- 空の行が削除されてしまう
- ダブルクオーテーションで囲んだフィールド内の空白や改行が削除されてしまう。
前後の空白を削除するTrimWhiteSpace=true
の場合
<< TextFieldParser (TrimWhiteSpace=true) >> No.;Name;Price;Comment<n> 001;りんご;98円;青森産<n> 002;バナナ;120円;とっても! お,い,し,い,よ!<n> 004;うまい棒"めんたい";10円;<n> 005;バナメイ海老;800円;300g エビチリに<n>
- 区切り文字後の空白が削除されている
"
で囲まれたフィールド内の空白(とっても!
の前)まで削除されている"
で囲まれたフィールド内の空白行(エビチリに
の上の行)が削除されている- No.003に相当する空の行が削除されている
続いて、前後の空白を削除しない TrimWhiteSpace=false
の場合
<< TextFieldParser (TrimWhiteSpace=false) >> No.; Name; Price; Comment<n> 001; りんご; 98円; 青森産<n> 002; バナナ; 120円; とっても! お,い,し,い,よ!<n> 004;うまい棒"めんたい"; 10円;<n> 005; バナメイ海老; 800円;300g エビチリに<n>
- 区切り文字後の空白が削除されていない
"
で囲まれたフィールド内の空白も削除されていない"
で囲まれたフィールド内の空白行(エビチリに
の上の行)が削除されている- No.003に相当する空の行が削除されている
作成したParseメソッドの結果
では、今回作成したParseメソッドの結果を確認してみましょう。
<< XsvReader.Parse() >> No.;Name;Price;Comment<n> 001;りんご;98円;青森産<n> 002;バナナ;120円; とっても! お,い,し,い,よ!<n> <n> 004;うまい棒"めんたい";10円;<n> 005;バナメイ海老;800円;300g エビチリに<n>
- 区切り文字の後ろの空白は削除されている。
"
で囲まれたフィールド内の空白(とっても!の前)は削除されていない"
で囲まれたフィールド内の区切り文字(,
)がそのまま残されている"
で囲まれたフィールド内のダブルクオーテーション(""
)が正しく処理されている"
で囲まれたフィールド内の改行が削除されていない。- 空白行(No.003に相当する行)では要素0の
IEnumerable<string>
を返している
ちゃんと、意図した通りの動作になっているようです。
テスト用コード
最後に、上記の確認用に使用したテストコードを掲載しておきます。