正規表現を超えるの補足として、CSVファイルを例に挙げて考えてみる。
file = record *(CRLF record) [CRLF] record = field *(COMMA field) field = (escaped / non-escaped) escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE non-escaped = *TEXTDATA TEXTDATA = %x20-21 / %x23-2B / %x2D-7E COMMA = %x2C CRLF = CR LF CR = %x0D LF = %x0A DQUOTE = %x22
これを Haskell の Parsec で書くと、ほとんどそのままの形で実装できる。
import Text.ParserCombinators.Parsec file :: Parser [[String]] file = record `sepEndBy1` crlf record :: Parser [String] record = field `sepBy1` comma field :: Parser String field = escaped <|> nonEscaped escaped :: Parser String escaped = do dquote txt <- many $ textdata <|> comma <|> cr <|> lf <|> try (dquote >> dquote) dquote return txt nonEscaped :: Parser String nonEscaped = many textdata textdata :: Parser Char textdata = oneOf $ " !" ++ ['#'..'+'] ++ ['-'..'~'] comma :: Parser Char comma = char ',' crlf :: Parser Char crlf = cr >> lf lf :: Parser Char lf = char '\x0a' cr :: Parser Char cr = char '\x0d' dquote :: Parser Char dquote = char '"'
さて、上記の Parsec のコードを書いて気付いたことがある。以下の実行例を見て頂きたい。
parseTest file "foo,bar,baz\r\n" [["foo","bar","baz"],[""]]
1行を解析したのに、2行分の結果が返ってきた。この現象は、入力ファイルが CRLF で終わっているときに起きる。理由は、field の定義が空文字を許すので、最後の CRLF で終わりなのか、次の行があるのか判断できないためだ。
とういう訳で、RFC4180 の BNF の定義は、間違っているとは言えないものの、曖昧であることが分かる。こんな考察ができるのも Parsec の素晴らしさの一つである。