読者です 読者をやめる 読者になる 読者になる

AccessViolation Exception

仕事でもはんだづけ、家でもはんだづけ

c#でSpracheを使った構文解析(4)

c# パーサジェネレータ 技術ネタ

c#でSpracheを使った構文解析(3) - AccessViolation Exception

前回記事です。

目標

var csv = "hoge,foo,bar,\nhoge1,foo1,bar1\nhoge2,foo2,bar2,bazz";

今回はcsvのような区切り文字や改行で仕切られているものを無事に展開することを目標とします。

セル単体を定義

static readonly Parser<string> Cell = 
    Parse.CharExcept(',').AtLeastOnce().Text()
         .Or(Parse.Return(""));

前回のParse.LetterParse.LetterOrDigitでは記号等を扱うことが出来ず、またスペースがあったらそこで途切れてしまいます。

Parse.AnyCharでは区切り文字ひっくるめて持ってきてしまうのでNG。

そこで指定文字以外を扱えるCharExcept()を使います。

.Or(Parse.Return(""))してるのは、セルには文字がない場合があります。その時に例外を起こさないようにする対策です。

これで "hoge,foo,bar" からhogeを取り出せるようになりました。

カンマで区切られた文字をパース

from begin in Parse.Char('\'')
from content in Cell
from end in Parse.Char('\'')

これでも最初と最後のカンマがない場合などを処理すれば出来ないこともないですが、DelimitedByという便利なものがあるので

static readonly Parser<IEnumerable<string>> Line =
    Cell.DelimitedBy(Parse.Char(','));

こう書くことが出来ます。最初と最後の区切り文字がない場合も考慮してくれます。

これで",hoge,foo,bar,,"["", "hoge", "foo", "bar", "", ""]にパースすることが出来ます。

CSV全体をパース

今度は改行で区切られたLineを定義すればいいので

static readonly Parser<IEnumerable<IEnumerable<string>>> CSV =
    Line.DelimitedBy(Parse.Char('\n'));

いいはずですがこれだけではダメ。先ほどカンマを例外文字に追加したように、改行も例外に追加しないとCellとして認識されてしまいます。

static readonly Parser<string> Cell =
    Parse.AnyChar.Except(Parse.Chars(',', '\n'))
         .AtLeastOnce().Text()
         .Or(Parse.Return(""));

'Parse.AnyChar.Expect()'で適当な文字から例外を指定、例外に追加するのは'Parse.Chars'で複数指定しておくように修正すればよし。

これで最初の目標に掲げていた

var csv = "hoge,foo,bar,\nhoge1,foo1,bar1\nhoge2,foo2,bar2,bazz";

[
    ["hoge", "foo", "bar", ""],
    ["hoge1", "foo1", "bar1"],
    ["hoge2", "foo2", "bar2", "baz"]
]

にパースできるようになりました。めでたしめでたし。

番外


カンマで区切られてかつダブルクォートでくくられている場合

csvってたまに

var csv = "\"hoge\",\"foo\",\"bar\",\nhoge1,\"foo1\",bar1\nhoge2,foo2,bar2,bazz";

みたいにダブルクォートでくくられている場合があります。現状ではダブルクォートごと文字列として識別してしまいます。これにも対応させてみましょう。

まずはクォートされたセルを定義

static readonly Parser<string> QuotedCell =
    from begin in Parse.Char('"')
    from cell in Cell
    from end in Parse.Char('"')
    select cell;

セルの例外文字にダブルクォートを追加

static readonly Parser<string> Cell =
    Parse.AnyChar.Except(Parse.Chars(',', '\n', '\"'))
         .AtLeastOnce().Text()
         .Or(Parse.Return(""));

あとはLineで識別するセルにQuotedCellを追加

static readonly Parser<IEnumerable<string>> Line =
    QuotedCell.Or(Cell).DelimitedBy(Parse.Char(','));

これで先ほどのダブルクォート付きのセルが混ざっていてもパース可能になりました。

今回はほぼDelimitedByとExceptの紹介のようなものでしたが以上になります。