如何在Scala中使用正则表达式进行模式匹配?

139

我希望能够找到一个单词的首字母和"ABC"组中的一个字母匹配的方法。在伪代码中,这可能看起来像:

case Process(word) =>
   word.firstLetter match {
      case([a-c][A-C]) =>
      case _ =>
   }
}

在Scala中如何获取首字母而不是Java?如何正确表达正则表达式?能否在case class内实现这一点?


9
警告:在Scala(和*ML语言)中,模式匹配有另一种与正则表达式非常不同的含义。 - user395760
1
你可能需要使用[a-cA-C]来表示那个正则表达式。 - user166390
2
在 Scala 2.8 中,字符串被转换为 Traversable(例如 ListArray)。如果你想要前三个字符,可以尝试 "my string".take(3)。对于第一个字符,可以使用 "foo".head - shellholic
7个回答

259

你可以这样做,因为正则表达式定义了提取器,但你需要先定义好正则表达式模式。我无法访问Scala REPL以测试它,但是类似下面的内容应该可以工作。

val Pattern = "([a-cA-C])".r
word.firstLetter match {
   case Pattern(c) => c bound to capture group here
   case _ =>
}

7
请注意,您不能声明一个捕获组并且不使用它(例如,case Pattern() 在此处将不匹配) - Jeremy Leipzig
40
请注意,在正则表达式中,您必须使用分组:val Pattern = "[a-cA-C]".r将无法工作。这是因为match-case使用unapplySeq(target: Any): Option[List[String]],它返回匹配的分组。 - rakensi
2
这是StringLike上的一个方法,它返回一个Regex - asm
12
@rakensi No. val r = "[A-Ca-c]".r ; 'a' match { case r() => } . 这段代码使用 Scala 的正则表达式库进行匹配,意思是创建一个名为“r”的变量,将正则表达式字符串"[A-Ca-c]"转换为正则表达式对象,并将其赋值给变量。然后通过match语句,尝试对字符 'a' 进行匹配,如果它符合正则表达式的规则,则会执行该case语句块中的代码。更多详细信息请查阅 Scala 文档中有关正则表达式的部分:http://www.scala-lang.org/api/current/#scala.util.matching.Regex - som-snytt
4
@JeremyLeipzig 忽略匹配分组: val r = "([A-Ca-c])".r ; "C" match { case r(_*) => } - som-snytt
显示剩余2条评论

134
自2.10版本以来,可以使用Scala的字符串插值功能:
implicit class RegexOps(sc: StringContext) {
  def r = new util.matching.Regex(sc.parts.mkString, sc.parts.tail.map(_ => "x"): _*)
}

scala> "123" match { case r"\d+" => true case _ => false }
res34: Boolean = true

更好的是,可以绑定正则表达式组:

scala> "123" match { case r"(\d+)$d" => d.toInt case _ => 0 }
res36: Int = 123

scala> "10+15" match { case r"(\d\d)${first}\+(\d\d)${second}" => first.toInt+second.toInt case _ => 0 }
res38: Int = 25

您可以设置更详细的绑定机制:

scala> object Doubler { def unapply(s: String) = Some(s.toInt*2) }
defined module Doubler

scala> "10" match { case r"(\d\d)${Doubler(d)}" => d case _ => 0 }
res40: Int = 20

scala> object isPositive { def unapply(s: String) = s.toInt >= 0 }
defined module isPositive

scala> "10" match { case r"(\d\d)${d @ isPositive()}" => d.toInt case _ => 0 }
res56: Int = 10

一个展示了Dynamic可能性的令人印象深刻的例子在博客文章Introduction to Type Dynamic中展示:
object T {

  class RegexpExtractor(params: List[String]) {
    def unapplySeq(str: String) =
      params.headOption flatMap (_.r unapplySeq str)
  }

  class StartsWithExtractor(params: List[String]) {
    def unapply(str: String) =
      params.headOption filter (str startsWith _) map (_ => str)
  }

  class MapExtractor(keys: List[String]) {
    def unapplySeq[T](map: Map[String, T]) =
      Some(keys.map(map get _))
  }

  import scala.language.dynamics

  class ExtractorParams(params: List[String]) extends Dynamic {
    val Map = new MapExtractor(params)
    val StartsWith = new StartsWithExtractor(params)
    val Regexp = new RegexpExtractor(params)

    def selectDynamic(name: String) =
      new ExtractorParams(params :+ name)
  }

  object p extends ExtractorParams(Nil)

  Map("firstName" -> "John", "lastName" -> "Doe") match {
    case p.firstName.lastName.Map(
          Some(p.Jo.StartsWith(fn)),
          Some(p.`.*(\\w)$`.Regexp(lastChar))) =>
      println(s"Match! $fn ...$lastChar")
    case _ => println("nope")
  }
}

@sschaef:那个 case p.firstName.lastName.Map(... 的模式——我该如何阅读它? - Erik Kaplun
1
@ErikAllik 可以将其理解为“当'firstName'以'Jo'开头且'secondName'与给定的正则表达式匹配时,匹配成功”。这更多地展示了Scala的强大之处,我不会在生产代码中以这种方式编写此用例。顺便说一下,应该使用List替换Map,因为Map是无序的,对于更多的值,不能保证正确的变量与正确的匹配器相匹配。 - kiritsuku
@sschaef:我认为这种魔法已经太过分了……除非你在应用程序中进行了一些复杂的正则表达式模式匹配(即使是这样,你也需要更加人性化的东西)。 - Erik Kaplun
此外,请注意使用内部捕获组会破坏组绑定。代码可能可以修改以修复该错误,但是我对Scala的了解还不足够。 - Alex Abdugafarov
1
这对于快速原型设计非常方便,但请注意,每次检查匹配时都会创建一个新的 Regex 实例。这是一项相当昂贵的操作,涉及正则表达式模式的编译。 - HRJ
显示剩余8条评论

50

正如delnan所指出的,Scala中的match关键字与正则表达式无关。要找出一个字符串是否匹配一个正则表达式,可以使用String.matches方法。要查找一个字符串是否以小写或大写的a、b或c开头,正则表达式应该是这样的:

word.matches("[a-cA-C].*")

你可以将这个正则表达式理解为“字符a,b,c,A,B或C中的一个,后面跟着任何内容”(.代表“任意字符”,*代表“零次或多次”,因此“.*”表示任何字符串)。


29

对于 Andrew的回答,我稍作解释:正则表达式可以定义提取器,通过Scala的模式匹配操作可以很好地将由正则表达式匹配到的子字符串进行分解,例如:

val Process = """([a-cA-C])([^\s]+)""".r // define first, rest is non-space
for (p <- Process findAllIn "aha bah Cah dah") p match {
  case Process("b", _) => println("first: 'a', some rest")
  case Process(_, rest) => println("some first, rest: " + rest)
  // etc.
}

我真的很困惑这个高帽符号“^”。我原以为“^”表示“匹配行首”,但它似乎并没有匹配行首。 - Michael Lafayette
@MichaelLafayette:在字符类([])内,插入符号表示否定,因此 [^\s] 表示“非空格”。 - Fabian Steeg

10
请注意,@AndrewMyers的答案中的方法将整个字符串与正则表达式进行匹配,使用^$在字符串的两端锚定了正则表达式。例如:
scala> val MY_RE = "(foo|bar).*".r
MY_RE: scala.util.matching.Regex = (foo|bar).*

scala> val result = "foo123" match { case MY_RE(m) => m; case _ => "No match" }
result: String = foo

scala> val result = "baz123" match { case MY_RE(m) => m; case _ => "No match" }
result: String = No match

scala> val result = "abcfoo123" match { case MY_RE(m) => m; case _ => "No match" }
result: String = No match

没有以.*结尾:

scala> val MY_RE2 = "(foo|bar)".r
MY_RE2: scala.util.matching.Regex = (foo|bar)

scala> val result = "foo123" match { case MY_RE2(m) => m; case _ => "No match" }
result: String = No match

2
惯用的写法是 val MY_RE2 = "(foo|bar)".r.unanchored ; "foo123" match { case MY_RE2(_*) => }。更惯用的写法是使用小写字母的变量名,例如 val re - som-snytt

9

首先我们需要知道正则表达式可单独使用。这里有一个例子:

import scala.util.matching.Regex
val pattern = "Scala".r // <=> val pattern = new Regex("Scala")
val str = "Scala is very cool"
val result = pattern findFirstIn str
result match {
  case Some(v) => println(v)
  case _ =>
} // output: Scala

其次,我们应该注意到将正则表达式与模式匹配相结合将是非常强大的。这里是一个简单的例子。

val date = """(\d\d\d\d)-(\d\d)-(\d\d)""".r
"2014-11-20" match {
  case date(year, month, day) => "hello"
} // output: hello

事实上,正则表达式本身已经非常强大。我们需要做的唯一一件事就是通过 Scala 使其更加强大。以下是 Scala 文档中的更多示例:http://www.scala-lang.org/files/archive/api/current/index.html#scala.util.matching.Regex


9

使用String.matches可以进行正则表达式匹配。

但是在实际的Scala代码中,word.firstLetter变得更加方便,看起来像:

word(0)

Scala将字符串视为Char序列,因此如果您出于某种原因想要明确获取字符串的第一个字符并进行匹配,可以使用以下代码:

"Cat"(0).toString.matches("[a-cA-C]")
res10: Boolean = true

我不认为这是一般情况下使用正则表达式模式匹配的方法,但它与您提出的先查找字符串的第一个字符,然后将其与正则表达式匹配的方法一致。

编辑: 要明确一点,我会像其他人一样这样做:

"Cat".matches("^[a-cA-C].*")
res14: Boolean = true

只是想展示一个尽可能接近你最初伪代码的示例。干杯!


3
“Cat"(0).toString” 可以更清晰地写为“Cat”取第1个字符,依我之见。 - David Winslow
此外(尽管这是一个老话题 - 我可能在挖坟):您可以从末尾删除“.*”,因为它对正则表达式没有任何价值。只需使用“Cat”。.matches(“ ^ [a-cA-C]”) - akauppi
今天是2月11日,val r = "[A-Ca-c]".r; "cat"(0) match { case r() => } - som-snytt
帽子符号 (^) 代表什么意思? - Michael Lafayette
这是一个锚点,意思是“行首”(https://www.cs.duke.edu/csl/docs/unix_course/intro-73.html)。因此,如果紧随其后的内容是该行的第一项,那么跟在hi hat后面的所有内容都将匹配该模式。 - Janx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接