使用Parsec对行进行分组

时间:2016-06-18 12:18:31

标签: parsing haskell parsec megaparsec

我想要使用Parsec†解析基于行的文本格式。一行以井号开头并指定由冒号分隔的键值对,或者是由前一个标记描述的URL。

这是一个简短的例子:

#foo:bar
#faz:baz
https://example.com
#foo:beep
https://example.net

为简单起见,我将把所有内容存储为String。标记为type Tag = (String, String),例如("foo", "bar")。最后,我想将这些分组为([Tag], URL)

但是,我很难弄清楚如何解析[一个或多个标签]或[一个网址]。

我目前的做法如下:

import qualified System.Environment   as Env
import qualified Text.Megaparsec      as M
import qualified Text.Megaparsec.Text as M

type Tag = (String, String)

data Segment = Tags [Tag] | URL String
  deriving (Eq, Show)

tagP :: M.Parser Tag
tagP = M.char '#' *> ((,) <$> M.someTill M.printChar (M.char ':') <*> M.someTill M.printChar M.eol) M.<?> "Tag starting with #"

urlP :: M.Parser String
urlP = M.someTill M.printChar M.eol M.<?> "Some URL"

parser :: M.Parser Segment
parser = (Tags <$> M.many tagP) M.<|> (URL <$> urlP)

main :: IO ()
main = do
  fname <- head <$> Env.getArgs
  res <- M.parseFromFile (parser <* M.eof) fname
  print res

如果我尝试在上面的示例中运行它,我得到一个像这样的解析错误:

3:1:
unexpected 'h'
expecting Tag starting with # or end of input

显然,我将many<|>结合使用是不正确的。由于标记解析器不会使用来自URL解析器的任何输入,因此它与回溯无关。我如何更改此项以获得所需的结果?

GitHub上提供了完整的示例。

†我实际上在这里使用MegaParsec来获得更好的错误消息,但我认为这个问题非常通用,而不是解析器组合器的任何特定实现。

2 个答案:

答案 0 :(得分:1)

你正在做的工作非常好,只是,你现在只解析一个片段(即只标记只有一个URL ),但是不消耗整个输入。导致错误的是eof

只需再使用一个manysome,即可允许多个细分受众群:

main :: IO ()
main = do
  fname <- head <$> Env.getArgs
  res <- M.parseFromFile (many parser <* M.eof) fname
  print res

答案 1 :(得分:0)

@cocreature为我解答了on Twitter

正如leftaroundabout所指出的,我的代码中有两个不同的错误:

  1. 解析器本身会误用<|>,而它应该只是顺序解析这些行,如果它不消耗任何输入,则跳到下一个解析器。
  2. 调用(parseFromFile)一次只应用parser函数,一旦到达第二个块就会失败。
  3. 我们可以修复解析器并一次性引入分组:

    parser :: M.Parser ([Tag], String)
    parser = liftA2 (,) (M.many tagP) urlP
    

    之后,我们只需要应用leftaroundabout建议的更改:

    ...
    res <- M.parseFromFile (M.many parser <* M.eof) fname
    

    运行此项会产生预期的结果:

    [([("foo","bar"),("faz","baz")],"https://example.com"),([("foo","beep")],"https://example.net")]