【问题标题】:attoparsec: "nested" parsers -- parse a subset of the input with a different parserattoparsec:“嵌套”解析器——使用不同的解析器解析输入的子集
【发布时间】:2015-06-26 21:15:49
【问题描述】:

其实我很确定我使用了错误的术语。这是我要解决的问题:markdown 格式的解析器,以及它的一个子集。

我的问题在于块引用功能。块引用中的每一行都以> 开头;否则一切都是markdown文件中的正常结构。

您不能单独查看单独的行,因为您需要将段落与正常行分开,例如

> a
> b

不一样

> a
>
> b

和类似的东西(如果列表被块引用,你不想要 x 列表而是一个带有 x 元素的列表)。一种自然而简单的方法是“取消”> 标志,自行解析块引用,忽略它周围的任何内容,用BlockQuote 类型构造函数包装它,将其放入外部 AST 并继续解析原始输入。如果我没记错的话,这就是pango 所做的:

https://hackage.haskell.org/package/pandoc-1.14.0.4/docs/src/Text-Pandoc-Readers-Markdown.html#blockQuote

blockQuote :: MarkdownParser (F Blocks)
blockQuote = do
  raw <- emailBlockQuote
  -- parse the extracted block, which may contain various block elements:
  contents <- parseFromString parseBlocks $ (intercalate "\n" raw) ++ "\n\n"
  return $ B.blockQuote <$> contents

然后:

http://hackage.haskell.org/package/pandoc-1.5.1/docs/src/Text-Pandoc-Shared.html#parseFromString

-- | Parse contents of 'str' using 'parser' and return result.
parseFromString :: GenParser tok st a -> [tok] -> GenParser tok st a
parseFromString parser str = do
  oldPos <- getPosition
  oldInput <- getInput
  setInput str
  result <- parser
  setInput oldInput
  setPosition oldPos
  return result

现在 parseFromString 对我来说看起来很 hacky,此外它也是 Parsec 而不是 attoparsec 所以我不能在我的项目中使用它。我不确定如何从块引用中获取 Text 并对其进行解析并返回解析结果,以便它“适合”当前解析。似乎不可能?

我一直在谷歌上搜索这个问题,我认为 pipes-parseconduit 可以在该领域提供帮助,尽管我很难找到示例并且我看到的看起来不太好看at 比“纯”解析器/attoparsec 解析器。

解析块引用的其他选项是重写通常的解析器,但使用&gt; 捕获...复杂和重复很多。解析块引用分别计算每一行并编写一些凌乱的“合并”函数。或者解析到第一个 AST,它将在第一个 BlockquoteText 类型构造函数中包含块引用作为 Text 等待转换,在那里它们将被单独解析,不是很优雅,但它具有简单的好处,这确实很重要.

我可能会选择后者,但肯定有更好的方法吗?

【问题讨论】:

  • 我不知道有一个解析器组合器库可以顺利支持这种组合(我过去一直在寻找它)。
  • 前几天我遇到了类似的问题,试图解析一个“面向行”的文件。 Parsec 的问题是:它给您的印象是您不需要词法分析器,而且理论上您可能不需要。在您的情况下,似乎首先“标记”您的文件将解决问题。您只需要为 BlockQuote 创建令牌。我可能错了。
  • @mb14 有趣...您知道这种方法的一些 OSS 示例吗,lexer 然后 parsec?它可能确实有效,但这次我可能会跳过它并采用一种不太优雅的方法。
  • 我知道 parsec 也支持词法分析器,但我没有发现任何真正有用的东西。但是,我确定有。 Lexer + Parser 是标准方法,所以至少 GHC 应该做类似的事情。
  • this,我真的没有时间好好看看。

标签: haskell attoparsec


【解决方案1】:

我也问过自己同样的问题。为什么没有像您描述的那样用于嵌套解析器的标准组合器?我的默认模式是信任包作者,尤其是当该作者还与人合写了“Real World Haskell”时。如果缺少如此明显的功能,也许是设计使然,我应该寻找更好的方法。然而,我已经设法说服自己,这样一个方便的组合器几乎是无害的。当全有或全无类型解析器适用于内部解析时很有用。

实施

import Data.Attoparsec.Text
import qualified Data.Text as T
import Data.Text(Text)
import Control.Applicative

我已将所需的功能分为两个解析器。第一个,constP,对某些给定文本执行“就地”解析。它将常量解析器的失败替换为 empty(来自 Alternative),但没有其他副作用。

constP :: Parser a -> Text -> Parser a
constP p t = case parseOnly p t of
  Left _ -> empty
  Right a -> return a

第二部分来自parseOf,它根据外部解析的结果执行恒定的内部解析。这里的empty 替代方案允许失败的解析返回而不消耗任何输入。

parseOf :: Parser Text -> Parser a -> Parser a
parseOf ptxt pa = bothParse <|> empty
  where
    bothParse = ptxt >>= constP pa

块引用降价可以以所需的方式编写。此实现需要完全解析生成的块。

blockQuoteMarkdown :: Parser [[Double]]
blockQuoteMarkdown = parseOf blockQuote ( markdownSurrogate <* 
                                          endOfInput
                                        )

我只是实现了一个空格分隔双精度的快速解析器,而不是实际的降价解析器。解析器的复杂性在于允许最后一个非空行以新行结束。

markdownSurrogate :: Parser [[Double]]
markdownSurrogate = do
  lns <- many (mdLine <* endOfLine)
  option lns ((lns ++) . pure <$> mdLine1)
  where
    mdLine = sepBy double (satisfy (==' '))
    mdLine1 = sepBy1 double (satisfy (==' '))

这两个解析器负责将内部文本返回到块引号。

blockQuote :: Parser Text
blockQuote = T.unlines <$> many blockLine

blockLine :: Parser Text
blockLine = char '>' *> takeTill isEndOfLine <* endOfLine

最后是解析器的测试。

parseMain :: IO ()
parseMain = do

  putStrLn ""
  doParse "a" markdownSurrogate a
  doParse "_" markdownSurrogate ""
  doParse "b" markdownSurrogate b
  doParse "ab" markdownSurrogate ab
  doParse "a_b" markdownSurrogate a_b
  doParse "badMarkdown x" markdownSurrogate x
  doParse "badMarkdown axb" markdownSurrogate axb

  putStrLn ""
  doParse "BlockQuote ab" blockQuoteMarkdown $ toBlockQuote ab
  doParse "BlockQuote a_b" blockQuoteMarkdown $ toBlockQuote a_b
  doParse "BlockQuote axb" blockQuoteMarkdown $ toBlockQuote axb
  where
    a = "7 3 1"
    b = "4 4 4"
    x = "a b c"

    ab = T.unlines [a,b]
    a_b = T.unlines [a,"",b]
    axb = T.unlines [a,x,b]

    doParse desc p str = do
      print $ T.concat ["Parsing ",desc,": \"",str,"\""]
      let i = parse (p <* endOfInput ) str
      print $ feed i ""

    toBlockQuote = T.unlines
                 . map (T.cons '>')
                 . T.lines

*Main> parseMain

"Parsing a: \"7 3 1\""
Done "" [[7.0,3.0,1.0]]
"Parsing _: \"\""
Done "" []
"Parsing b: \"4 4 4\""
Done "" [[4.0,4.0,4.0]]
"Parsing ab: \"7 3 1\n4 4 4\n\""
Done "" [[7.0,3.0,1.0],[4.0,4.0,4.0]]
"Parsing a_b: \"7 3 1\n\n4 4 4\n\""
Done "" [[7.0,3.0,1.0],[],[4.0,4.0,4.0]]
"Parsing badMarkdown x: \"a b c\""
Fail "a b c" [] "endOfInput"
"Parsing badMarkdown axb: \"7 3 1\na b c\n4 4 4\n\""
Fail "a b c\n4 4 4\n" [] "endOfInput"

"Parsing BlockQuote ab: \">7 3 1\n>4 4 4\n\""
Done "" [[7.0,3.0,1.0],[4.0,4.0,4.0]]
"Parsing BlockQuote a_b: \">7 3 1\n>\n>4 4 4\n\""
Done "" [[7.0,3.0,1.0],[],[4.0,4.0,4.0]]
"Parsing BlockQuote axb: \">7 3 1\n>a b c\n>4 4 4\n\""
Fail ">7 3 1\n>a b c\n>4 4 4\n" [] "Failed reading: empty"

讨论

显着的区别在于失败的语义。比如解析axb和blockquotedaxb时,分别是下面两个字符串

7 3 1
a b c
4 4 4

> 7 3 1
> a b c
> 4 4 4

markdown 解析结果

Fail "a b c\n4 4 4\n" [] "endOfInput"

而引用的结果是

Fail ">7 3 1\n>a b c\n>4 4 4\n" [] "Failed reading: empty"

降价消耗“7 3 1\n”,但这在引用的失败中没有报告。相反,失败会变成全有或全无。

同样,在部分成功的情况下也不允许处理未解析的文本。但考虑到用例,我认为没有必要这样做。例如,如果解析如下所示

"{ <tok> unhandled }more to parse"

其中{} 表示已识别的块引用上下文,&lt;tok&gt; 在该内部上下文中进行解析。部分成功则必须将“未处理”从该块引用上下文中提取出来,并以某种方式将其与“更多解析”结合起来。

我没有看到 通用 方法可以做到这一点,但可以通过选择内部解析器返回类型来实现。例如,通过某些解析器parseOf blockP innP :: Parser (&lt;tok&gt;,Maybe Text)。但是,如果出现这种需要,我希望有一种比嵌套解析器更好的方法来处理这种情况。

也可能会担心丢失 attoparsec 部分解析。也就是说,constP 的实现使用了parseOnly,它将解析返回FailPartial 折叠成单个Left 失败状态。换句话说,我们失去了向内部解析器提供更多可用文本的能力。但是,请注意,要解析的文本本身就是外部解析的结果;只有在向外部解析提供了足够的文本后,它才可用。所以这也不应该是个问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-01-04
    • 2012-02-21
    • 1970-01-01
    • 2018-08-25
    • 1970-01-01
    • 2015-12-11
    • 1970-01-01
    相关资源
    最近更新 更多