用Parsec逃避行尾

时间:2014-10-14 06:53:46

标签: haskell parsec

这次我正在尝试使用Parsec将文本文件解析为[[String]]。结果是一个列表,其中包含表示文件行的列表。每一行都是一个列表,其中包含可以用任意数量的空格分隔的单词,(可选)逗号和逗号后面的空格。

这是我的代码,它甚至可以运作。

import Text.ParserCombinators.Parsec hiding (spaces)
import Control.Applicative ((<$>))
import System.IO
import System.Environment

myParser :: Parser [[String]]
myParser =
    do x <- sepBy parseColl eol
       eof
       return x

eol :: Parser String
eol = try (string "\n\r")
  <|> try (string "\r\n")
  <|> string "\n"
  <|> string "\r"
  <?> "end of line"

spaces :: Parser ()
spaces = skipMany (char ' ') >> return ()

parseColl :: Parser [String]
parseColl = many parseItem

parseItem :: Parser String
parseItem =
    do optional spaces
       x <- many1 (noneOf " ,\n\r")
       optional spaces
       optional (char ',')
       return x

parseText :: String -> String
parseText str =
    case parse myParser "" str of
      Left e  -> "parser error: " ++ show e
      Right x -> show x

main :: IO ()
main =
    do fileName <- head <$> getArgs
       handle <- openFile fileName ReadMode
       contents <- hGetContents handle
       putStr $ parseText contents
       hClose handle

测试文件:

  

这是我的测试文件
  这个,行,是,分隔,用,逗号
  这是另一条线

结果:

[["this","is","my","test","file"],
 ["this","line","is","separated","by","commas"],
 ["and","this","is","another","line"],
 []] -- well, this is a bit unexpected, but I can filter things

现在,为了让我的生活更加艰难,我希望能够“逃避”eol如果前面有逗号,,即使逗号后跟空格也是如此。所以这应该被认为是一行:

  

这是空格可能在这里
  我的行

实现此语法的最佳策略(最惯用和最优雅)是什么(不会失去忽略行内逗号的能力)。

2 个答案:

答案 0 :(得分:2)

我想到了一些解决方案......一个很容易,另一个是中等难度。


中难度解决方案是将itemSeparator定义为逗号后跟空格,将lineSeparator定义为&#39; \ n&#39;或者&#39; \ r&#39;接下来是空白....确保跳过非&#39; \ n&#39;,&#39; \ r&#39; -whitespace,但在项目解析结束时不再进一步,以便非常在项目必须是&#39; \ n&#39;,&#39; \ r&#39;或&#39;,&#39;之后的下一个字符,它在没有回溯的情况下确定是否有新项目或线路即将来临。

然后使用sepBy1定义parseLine(ie- parseLine = parseItem sepBy1 parseItemSeparator),并使用endBy定义parseFile(ie- parseFile = parseLine endBy parseLineSeparator)。

你确实需要内部的sepBy1,vs sepBy,否则你将有一个零大小的项目列表,这会在解析时产生无限循环。 endBy的作用类似sepBy,但允许额外的&#39; \ n&#39;,&#39; \ r&#39;在文件的末尾....


更简单的方法是通过在解析之前通过简单转换运行输入来规范化输入。您可以编写一个函数来删除逗号后的空格(使用dropWhileisSpace),甚至可以简化&#39; \ n&#39;,&#39; \ r &&的不同情况#39; ....然后通过简化的解析器运行输出。

这样的事情可以解决问题(这是未经测试的......)

canonicalize::String->String
canonicalize [] == []
canonicalize (',':rest) = ',':canonicalize (dropWhile isSpace rest)
canonicalize ('\n':rest) = '\n':canonicalize (dropWhile isSpace rest)
canonicalize ('\r':rest) = '\n':canonicalize (dropWhile isSpace rest) --all '\r' will become '\n'
canonicalize (c:rest) = c:canonicalize rest

因为Haskell是懒惰的,所以当数据进入时,这种转换将对流数据起作用,所以这真的不会让任何事情变慢(取决于你简化解析器的程度,它甚至可以加快速度....虽然很可能它会接近洗涤)

我不知道完整问题有多复杂,但也许规范化函数中添加的一些规则实际上允许您使用lineswords ...

答案 1 :(得分:1)

只需在optional spaces中使用parseColl,就像这样:

parseColl :: Parser [String]
parseColl = optional spaces >> many parseItem

parseItem :: Parser String
parseItem =
    do 
       x <- many1 (noneOf " ,\n\r")
       optional spaces
       optional (char ',')
       return x

其次,从项目中划分分隔符

parseColl :: Parser [String]
parseColl = do
      optional spaces
      items <- parseItem `sepBy` parseSeparator
      optional spaces
      return items

parseItem :: Parser String
parseItem = many1 $ noneOf " ,\n\r"

parseSeparator = try (optional spaces >> char ',' >> optional spaces) <|> spaces

第三,我们重新创建了一点eolspaces

eol :: Parser String
eol = try (string "\n\r")
  <|> string "\r\n"
  <|> string "\n"
  <|> string "\r"
  <|> eof
  <?> "end of line"

spaces :: Parser ()
spaces = skipMany1 $ char ' '

parseColl :: Parser [String]
parseColl = do
      optional spaces
      items <- parseItem `sepBy` parseSeparator
      optional spaces
      eol
      return items

最后,让我们重写myParser

myParser = many parseColl