0

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

这是一个简短的示例:

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

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

但是,我很难弄清楚如何解析 [一个或多个标签] 或 [一个 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 来获得更好的错误消息,但我认为这个问题非常普遍,与解析器组合器的任何特定实现无关。

4

2 回答 2

1

您正在做的工作非常好,只是目前您只解析一个片段(即,只有标签只有一个 URL),但这不会消耗整个输入。这eof就是导致错误的原因。

只需使用一个manysome多个,以允许多个段:

main :: IO ()
main = do
  fname <- head <$> Env.getArgs
  res <- M.parseFromFile (many parser <* M.eof) fname
  print res
于 2016-06-18T12:40:23.010 回答
0

@cocreature在 Twitter 上为我回答了这个问题。

正如leftaroundabout在这里指出的那样,我的代码中有两个单独的错误:

  1. 解析器本身会误用<|>,而它应该只是按顺序解析行并在不消耗任何输入的情况下跳到下一个解析器。
  2. 调用 ( parseFromFile) 仅应用该parser函数一次,并且一旦到达第二个块就会失败。

我们可以一次性修复解析器并引入分组:

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")]
于 2016-06-18T12:50:59.340 回答