Text.Parsecで固定長データをパースする

仕事で地震動波形のファイルを扱うことがある。地震動波形ってのは、地震の加速度を数値化したもので、こんなふうなファイルになっている。

Example wave 1
NUMBER OF DATA=12001, DT=0.010, MAX.=-779.834268(TIME= 22.520), FORMAT=10F8.2
   -0.05   -0.05   -0.05   -0.05   -0.06   -0.06   -0.06   -0.06   -0.06   -0.07
   -0.07   -0.07   -0.07   -0.07   -0.08   -0.08   -0.08   -0.09   -0.09   -0.09
   -0.10   -0.10   -0.10   -0.11   -0.11   -0.12   -0.13   -0.13   -0.14   -0.15
   -0.16   -0.17   -0.18   -0.20   -0.22   -0.24   -0.27   -0.29   -0.33   -0.37
   -0.41   -0.45   -0.50   -0.56   -0.61   -0.68   -0.74   -0.82   -0.89   -0.97
   -1.06   -1.15   -1.25   -1.35   -1.45   -1.56   -1.68   -1.79   -1.92   -2.05
   -2.18   -2.32   -2.46   -2.61   -2.76   -2.91   -3.07   -3.23   -3.39   -3.55
   -3.72   -3.89   -4.06   -4.24   -4.42   -4.60   -4.78   -4.96   -5.14   -5.31
(以下略)

見てのとおりヘッダが2行あって、3行目以降に8桁の固定長データが続いている。
今回はこれを Text.Parsec を使ってパースしてみようってわけ。といっても最初はことを単純にするため、ヘッダを省略してデータの部分だけでやってみる。

   -0.05   -0.05   -0.05   -0.05   -0.06   -0.06   -0.06   -0.06   -0.06   -0.07
   -0.07   -0.07   -0.07   -0.07   -0.08   -0.08   -0.08   -0.09   -0.09   -0.09
   -0.10   -0.10   -0.10   -0.11   -0.11   -0.12   -0.13   -0.13   -0.14   -0.15
   -0.16   -0.17   -0.18   -0.20   -0.22   -0.24   -0.27   -0.29   -0.33   -0.37
   -0.41   -0.45   -0.50   -0.56   -0.61   -0.68   -0.74   -0.82   -0.89   -0.97
   -1.06   -1.15   -1.25   -1.35   -1.45   -1.56   -1.68   -1.79   -1.92   -2.05
   -2.18   -2.32   -2.46   -2.61   -2.76   -2.91   -3.07   -3.23   -3.39   -3.55
   -3.72   -3.89   -4.06   -4.24   -4.42   -4.60   -4.78   -4.96   -5.14   -5.31
(以下略)

Haskell のパーサコンビネータ Parsec には Parsec 2 相当の Text.ParserCombinators.Parsec と Parsec 3 相当の Text.Parsec がある。最近では Parsec 2 ではなく Parsec 3 (つまり Text.Parsec)を使え、ということのようなんだけど、Web で検索しても Text.ParserCombinators.Parsec ばかりで、結構苦労した。特にパーサの型がわからなかった。結局、各パーサの型を書くのをやめて、パースを実行する関数にだけ型を書いたら動いてくれた。書いたコードがこれ。

module Main where

import System.Environment ( getArgs )
import Text.Parsec

--------------------------------------------------------------------------------

main :: IO ()
main = do
  args <- getArgs
  cs <- readFile (head args)
  let wv = parseWave cs
  either (\ e -> print e) (\ w -> putStr $ unlines $ map show w) wv

--------------------------------------------------------------------------------

-- Parsers

wave = do
  ls <- many1
  waveLine
  eof
  return $ concat ls

waveLine = do
  ws <- many1
  waveData
  newline
  return ws

waveData = do
  d <- count 8 dataChar
  return $ read d

dataChar = digit <|> oneOf "-. "

--------------------------------------------------------------------------------

parseWave :: String -> Either ParseError [Double]
parseWave input = parse wave "(unknown)" input

--------------------------------------------------------------------------------

実行例:

takatoh@apostrophe $ runhaskell parsewave.hs data0.dat > result.txt
takatoh@apostrophe $ head result.txt
-5.0e-2
-5.0e-2
-5.0e-2
-5.0e-2
-6.0e-2
-6.0e-2
-6.0e-2
-6.0e-2
-6.0e-2
-7.0e-2

なんとか、うまく動いてくれた。

ちなみに、ヘッダのついた data1.dat を食わせるとこんなエラーになる。

takatoh@apostrophe $ runhaskell parsewave.hs data1.dat
"(unknown)" (line 1, column 1):
unexpected "E"
expecting digit