这次我正在尝试使用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
如果前面有逗号,
,即使逗号后跟空格也是如此。所以这应该被认为是一行:
这是空格可能在这里
我的行
实现此语法的最佳策略(最惯用和最优雅)是什么(不会失去忽略行内逗号的能力)。
答案 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;在文件的末尾....
更简单的方法是通过在解析之前通过简单转换运行输入来规范化输入。您可以编写一个函数来删除逗号后的空格(使用dropWhile
和isSpace
),甚至可以简化&#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是懒惰的,所以当数据进入时,这种转换将对流数据起作用,所以这真的不会让任何事情变慢(取决于你简化解析器的程度,它甚至可以加快速度....虽然很可能它会接近洗涤)
我不知道完整问题有多复杂,但也许规范化函数中添加的一些规则实际上允许您使用lines
和words
...
答案 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
第三,我们重新创建了一点eol
和spaces
:
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