あどけない話

インターネットに関する技術的な話など

正規表現を超える--CSVファイル編

正規表現を超えるの補足として、CSVファイルを例に挙げて考えてみる。

CSVファイルの定義は、RFC4180にある。

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 の素晴らしさの一つである。