Parsecを使ってCSVを読み込むライブラリを作ってみました
Parsecを使ってCSVを読み込むライブラリを作ってみました。ParsecはGHCに標準添付なんで気軽に使えてよいです。なおCSVの仕様はRFC4180を参考にしていますが、厳密にはRFC4180には準拠していません。差分は以下の通りです。
- 改行文字は\nのみ許可しています。が、Windowsではファイルはテキストモードで開かれるのでCRLFはLFに変換されます。あまり問題にはならないかもしれません。
- 8ビット目が立っている文字も許容しています。
ファイル: CSV.hs
module CSV (parseCSV, parseCSVFromFile) where import Text.ParserCombinators.Parsec parseCSV :: String -> [[String]] parseCSV s = case parse parseCSV' "" s of Right v -> removeLastEmptyCVSRecord v Left e -> fail $ show e parseCSVFromFile :: FilePath -> IO [[String]] parseCSVFromFile s = do ret <- parseFromFile parseCSV' s case ret of Right v -> return $ removeLastEmptyCVSRecord v Left e -> fail $ show e parseCSV' :: Parser [[String]] parseCSV' = file where file :: Parser [[String]] file = record `sepBy` char '\n' record :: Parser [String] record = field `sepBy` char ',' field :: Parser String field = escaped <|> nonEscaped escaped :: Parser String escaped = do char '"' v <- many escapedChar char '"' return v escapedChar :: Parser Char escapedChar = try (char '"' >> char '"' >> return '"') <|> noneOf "\"" nonEscaped :: Parser String nonEscaped = many $ noneOf ",\"\n" removeLastEmptyCVSRecord :: [[String]] -> [[String]] removeLastEmptyCVSRecord a | isEmptyCVS = [] | isEndNewLineCVS = reverse $ tail $ reverse a | otherwise = a where isEmptyCVS :: Bool isEmptyCVS = (length a == 1) && (null $ head a) isEndNewLineCVS :: Bool isEndNewLineCVS = last a == [""]
ファイル: test.hs
module Main (main) where import System.Environment (getArgs, getProgName) import CSV main :: IO () main = do prog <- getProgName args <- getArgs if length args == 0 then putStrLn $ usage prog else mapM_ (\path -> mapM_ print =<< parseCSVFromFile path) args where usage p = p ++ " files..."
ファイル: Makefile
TARGET = test SOURCES = test.hs CSV.hs HC = ghc HCFLAGS = -W -fno-warn-unused-matches --make all: $(TARGET) $(TARGET): $(SOURCES) $(HC) $(HCFLAGS) $< -o $@ clean: rm -f $(TARGET) *.o *.hi *.exe
コンパイル手順は以下の通りです。
$ make ghc -W -fno-warn-unused-matches --make test.hs -o test Chasing modules from: test.hs Compiling CSV ( ./CSV.hs, ./CSV.o ) Compiling Main ( test.hs, test.o ) Linking ... $
実行例1。以下のファイルの場合。
$ cat data.txt aaa, bbb ,ccc,"ddd""ddd ddd", foo,bar,baz,quux $ ./test.exe data.txt ["aaa"," bbb ","ccc","ddd\"ddd\nddd",""] ["foo","bar","baz","quux"] $
実行例2。空ファイルの場合。空のリストが返るので何も表示されません。
$ ./test.exe data2.txt $
上の実装では、関数removeLastEmptyCVSRecordを使ってCSVファイルが改行文字で終わる場合の処理を行っています。具体的には、空のCSVを読み込ませた時には[]を返して、CSVが改行で終わる場合には最後の[""]を削除しています。reverseが続くので非効率ぽいです。うーむ、何かいい解決策はありませんかね? (関数fileのrecord `sepBy` char '\n'をrecord `sepEndBy` char '\n'にしても関数nonEscapedが'\n'を消費できなくて、次の空のnonEscapedで処理されてしまいます…)
参考: