我正在玩玩具HTML解析器,以帮助我熟悉Scala的解析组合库:
import scala.util.parsing.combinator._
sealed abstract class Node
case class TextNode(val contents : String) extends Node
case class Element(
val tag : String,
val attributes : Map[String,Option[String]],
val children : Seq[Node]
) extends Node
object HTML extends RegexParsers {
val node: Parser[Node] = text | element
val text: Parser[TextNode] = """[^<]+""".r ^^ TextNode
val label: Parser[String] = """(\w[:\w]*)""".r
val value : Parser[String] = """("[^"]*"|\w+)""".r
val attribute : Parser[(String,Option[String])] = label ~ (
"=" ~> value ^^ Some[String] | "" ^^ { case _ => None }
) ^^ { case (k ~ v) => k -> v }
val element: Parser[Element] = (
("<" ~> label ~ rep(whiteSpace ~> attribute) <~ ">" )
~ rep(node) ~
("</" ~> label <~ ">")
) ^^ {
case (tag ~ attributes ~ children ~ close) => Element(tag, Map(attributes : _*), children)
}
}
我想要的是确保我的开始和结束标签匹配的一些方法。
我想这样做,我需要某种flatMap
组合子〜Parser[A] => (A => Parser[B]) => Parser[B]
,
所以我可以使用开始标记来构造结束标记的解析器。但我没有看到任何与该签名in the library匹配的内容。
这样做的正确方法是什么?
答案 0 :(得分:5)
您可以编写一个方法,该方法接受标记名称并返回具有该名称的结束标记的解析器:
object HTML extends RegexParsers {
lazy val node: Parser[Node] = text | element
val text: Parser[TextNode] = """[^<]+""".r ^^ TextNode
val label: Parser[String] = """(\w[:\w]*)""".r
val value : Parser[String] = """("[^"]*"|\w+)""".r
val attribute : Parser[(String, Option[String])] = label ~ (
"=" ~> value ^^ Some[String] | "" ^^ { case _ => None }
) ^^ { case (k ~ v) => k -> v }
val openTag: Parser[String ~ Seq[(String, Option[String])]] =
"<" ~> label ~ rep(whiteSpace ~> attribute) <~ ">"
def closeTag(name: String): Parser[String] = "</" ~> name <~ ">"
val element: Parser[Element] = openTag.flatMap {
case (tag ~ attrs) =>
rep(node) <~ closeTag(tag) ^^
(children => Element(tag, attrs.toMap, children))
}
}
请注意,您还需要使node
懒惰。现在,您可以获得不匹配标记的干净错误消息:
scala> HTML.parse(HTML.element, "<a></b>")
res0: HTML.ParseResult[Element] =
[1.6] failure: `a' expected but `b' found
<a></b>
^
为了清楚起见,我比必要的更加冗长。如果您想要简洁,可以跳过openTag
和closeTag
方法,并像这样写element
,例如:
val element = "<" ~> label ~ rep(whiteSpace ~> attribute) <~ ">" >> {
case (tag ~ attrs) =>
rep(node) <~ "</" ~> tag <~ ">" ^^
(children => Element(tag, attrs.toMap, children))
}
我确信更简洁的版本是可能的,但在我看来,这甚至会走向不可读性。
答案 1 :(得分:4)
Parser上有一个flatMap
,还有一个名为into
和运算符>>
的等效方法,这可能是更方便的别名(flatMap
仍需要用于理解)。这确实是一种有效的方式来做你想要的。
或者,您可以检查代码是否与^?
匹配。
答案 2 :(得分:3)
你看错了地方。不过,这是一个正常的错误。您需要一个方法Parser[A] => (A => Parser[B]) => Parser[B]
,但是您查看了Parsers
的文档,而不是Parser
。
看here。
有一个flatMap
,也称为into
或>>
。