使用 Parsec 对线进行分组
Grouping lines with Parsec
我有一个基于行的文本格式,我想用 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 来获得更好的错误消息,但我认为这个问题非常普遍,与解析器组合器的任何特定实现无关。
你所做的工作非常好,只是,目前你只解析一个片段(即,仅标签或仅URL),但这并不会消耗全部输入。是 eof
导致了错误。
只需再使用一个 many
或 some
,以允许多个分段:
main :: IO ()
main = do
fname <- head <$> Env.getArgs
res <- M.parseFromFile (many parser <* M.eof) fname
print res
@cocreature answered this for me on Twitter.
正如 leftaroundabout 在这里指出的,我的代码中有两个独立的错误:
- 解析器本身误用了
<|>
,而它应该只是按顺序解析行并在不消耗任何输入的情况下跳到下一个解析器。
- 调用 (
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")]
我有一个基于行的文本格式,我想用 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 来获得更好的错误消息,但我认为这个问题非常普遍,与解析器组合器的任何特定实现无关。
你所做的工作非常好,只是,目前你只解析一个片段(即,仅标签或仅URL),但这并不会消耗全部输入。是 eof
导致了错误。
只需再使用一个 many
或 some
,以允许多个分段:
main :: IO ()
main = do
fname <- head <$> Env.getArgs
res <- M.parseFromFile (many parser <* M.eof) fname
print res
@cocreature answered this for me on Twitter.
正如 leftaroundabout 在这里指出的,我的代码中有两个独立的错误:
- 解析器本身误用了
<|>
,而它应该只是按顺序解析行并在不消耗任何输入的情况下跳到下一个解析器。 - 调用 (
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")]