scala中正则表达式的运用(一)

26 阅读8分钟

(一)正则表达式的定义

正则表达式(Regular Expression)是一种用于匹配、查找和替换文本中特定模式的字符串,它独立于具体编程语言,是一种通用的文本处理技术,广泛应用于各类数据处理场景。

它的核心应用场景包括:

  1. 数据验证:在用户注册、登录等交互场景中,验证输入信息是否符合格式要求。例如,验证手机号码、身份证号码、邮箱地址、密码复杂度(如要求包含大小写字母 + 数字 + 特殊字符)等格式的正确性。
  2. 文本搜索和替换:在文本编辑器(如 Word)、代码编辑器(如 IDEA、VS Code)等工具中,精准查找特定的单词、短语、代码模式,并进行批量替换操作。比如,在一篇文档中查找所有yyyy-mm-dd格式的日期并统一替换为yyyy/mm/dd格式,或在代码中批量替换废弃的函数名。
  3. 数据提取:从网页源码、日志文件、配置文件等大量非结构化 / 半结构化文本数据中,提取指定的关键信息。例如,从 HTML 代码中提取所有的<a>标签链接地址、从服务器日志中提取所有异常请求的 IP 地址、从配置文件中提取所有键值对配置项。

(二)第一个正则表达式的案例

核心使用示例

// 1. 定义正则表达式:使用 .r 后缀将普通字符串转为 Scala 正则对象
val reg = "x".r  // 匹配字符 "x" 的正则规则

// 场景1:查找第一个匹配的子串(返回 Option 类型,避免空指针)
val re1 = reg.findFirstIn("a x b x c") // 在目标字符串中查找第一个 "x"
if (!re1.isEmpty) { // 判断是否找到匹配结果
   println("找到的第一个匹配项:" + re1.get) // 若存在,通过 .get 获取匹配值
}

// 场景2:查找所有匹配的子串(返回可迭代对象,可转为集合方便操作)
val re2 = reg.findAllIn("a x b x c").toList // 查找所有 "x" 并转为 List 集合
println("找到的所有匹配项:" + re2) // 输出 List(x, x)

// 场景3:判断字符串是否完全匹配正则规则(数据校验常用)
val isMatch = reg.pattern.matcher("x").matches() // 判断字符串 "x" 是否完全匹配
println("是否完全匹配:" + isMatch) // 输出 true

正则表达式使用步骤总结

  1. 定义匹配规则:编写符合需求的正则表达式,通过 .r 后缀将普通字符串转为 Scala 正则对象(scala.util.matching.Regex)。
  2. 准备目标数据:定义需要进行匹配、查找或校验的目标字符串(可来自用户输入、文件读取等场景)。
  3. 执行匹配操作:根据业务需求调用对应的正则方法(如findFirstIn找首个匹配、findAllIn找所有匹配、matches做完全校验),并处理匹配结果。

(三)正则表达式的基本组成部分

无论多么复杂的正则表达式,其核心都是由以下 4 个基础部分组合构成,掌握这 4 部分即可搭建任意复杂的匹配规则。

  1. 字符类:用于匹配单个字符或指定范围的字符,是正则表达式的基础单元。

    • 单个字符:直接书写字符本身,如 a 匹配字符a5 匹配数字5@ 匹配符号@
    • 字符集合:用 [] 包裹多个字符,匹配其中任意一个,如 [abc] 匹配abc中的任意一个字符,[012345] 匹配 0-5 中的任意一个数字。
    • 字符范围:在 [] 中使用 - 指定连续范围,如 [a-z] 匹配小写字母 a 到 z 的任意一个,[A-Z] 匹配大写字母 A 到 Z 的任意一个,[0-9] 匹配 0 到 9 的任意一个数字,[a-zA-Z0-9] 匹配大小写字母和数字中的任意一个。
    • 排除字符集:在 [] 开头添加 ^,表示匹配除该集合外的任意一个字符,如 [^abc] 匹配除abc之外的所有字符,[^0-9] 匹配非数字字符。
  2. 量词:用于指定前面的「字符」或「字符组」出现的次数,是实现多字符匹配的核心。

    • *:匹配前面的字符 / 字符组出现 0 次或多次(贪婪匹配,尽可能多匹配),如 a* 可匹配空字符串、aaaaaa 等。
    • +:匹配前面的字符 / 字符组出现 1 次或多次,如 a+ 可匹配aaaaaa 等,但不能匹配空字符串。
    • ?:匹配前面的字符 / 字符组出现 0 次或 1 次(最多 1 次),如 a? 可匹配空字符串或a
    • {n}:匹配前面的字符 / 字符组出现 恰好 n 次(n 为非负整数),如 a{3} 仅匹配aaa
    • {n,}:匹配前面的字符 / 字符组出现 至少 n 次,如 a{2,} 可匹配aaaaaaaaa 等。
    • {n,m}:匹配前面的字符 / 字符组出现 n 次到 m 次(包含 n 和 m,n≤m),如 a{1,3} 可匹配aaaaaa
  3. 锚点:用于指定匹配的位置(不匹配具体字符),常用于精准校验文本的开头和结尾。

    • ^:匹配行首(字符串的开头),如 ^abc 表示匹配以abc开头的字符串(仅匹配开头位置,后跟 abc)。
    • $:匹配行尾(字符串的结尾),如 abc$ 表示匹配以abc结尾的字符串(仅匹配结尾位置,前面跟 abc)。
    • \b:匹配单词边界(单词与非单词的分隔处,如空格、标点、开头 / 结尾等),如 \bcat\b 仅匹配独立的单词cat,不匹配category中的cat部分。
    • \B:匹配非单词边界,与\b相反,如 \Bcat\B 仅匹配单词内部的cat(如category中的cat)。
  4. 分组:使用括号 () 将多个字符或规则包裹为一个整体(分组),可对分组整体使用量词,也可用于提取匹配的子串。

    • 基本分组:如 (ab)+ 表示将ab作为一个整体,匹配其出现 1 次或多次,可匹配abababababab 等;若不分组,ab+ 仅表示b出现 1 次或多次(匹配ababbabbb 等)。
    • 分组提取:匹配成功后,可通过分组索引提取对应子串(索引从 1 开始,0 表示整个匹配结果),如正则 (\d{4})-(\d{2})-(\d{2}) 匹配日期时,分组 1 提取年、分组 2 提取月、分组 3 提取日。
    • 非捕获分组:若仅需将多个规则作为整体,无需提取子串,可使用 (?:) 定义非捕获分组,如 (?:ab)+,性能优于普通分组。

(四)常见正则规则详解

核心使用模板

// 1. 定义正则规则(可替换为下方任意规则)
val reg = "需要测试的正则规则".r  

// 2. 定义目标文本
val targetText = "需要匹配的目标字符串"

// 3. 查找所有匹配项并输出
val allMatches = reg.findAllIn(targetText).toList
println(s"正则规则:${reg.pattern.pattern()}")
println(s"目标文本:$targetText")
println(s"匹配结果:$allMatches")
println("-" * 50) // 分隔线,方便查看结果

. 匹配单个字符的核心规则

规则类型具体规则规则说明示例正则目标文本匹配结果
普通单字符任意普通字符(a、5、@等)大多数字符直接匹配自身abc"abc"、"aabbcc"、"abd"匹配 "abc"(精准匹配自身)
字符集合[]匹配括号内任意一个字符[abc]"a"、"b"、"c"、"d"匹配 "a"、"b"、"c"
排除字符集合[^abc]匹配除括号内字符外的任意一个字符[^abc]"a"、"d"、"e"、"b"匹配 "d"、"e"
任意非换行字符.匹配除换行符(\n)外的任意单个字符a.c"abc"、"a&c"、"a\nc"、"acc"匹配 "abc"、"a&c"、"acc"

2. 匹配特殊字符集的快捷规则

快捷规则等价规则规则说明示例正则目标文本匹配结果
\d[0-9]匹配任意单个数字字符\d{3}"123"、"456"、"abc123"匹配 "123"、"456"
\D[^0-9]匹配任意单个非数字字符\D+"abc"、"123abc"、"@#$"匹配 "abc"、"abc"、"@#$"
\w[a-zA-Z0-9_]匹配字母、数字、下划线(单词字符)\w+"abc"、"abc123"、"abc_"匹配 "abc"、"abc123"、"abc_"
\W[^a-zA-Z0-9_]匹配非单词字符(标点、符号等)\W"a"、"&"、"_"、"#"匹配 "&"、"#"
\s[ \t\n\r\f]匹配空白字符(空格、制表符等)a\sb"a b"、"a\tb"、"ab"匹配 "a b"、"a\tb"
\S[^ \t\n\r\f]匹配任意非空白字符\S+"abc"、"a b"、"123"匹配 "abc"、"a"、"b"、"123"

注意:在 Scala 字符串中,反斜杠``是转义字符,因此正则中的\d\w等快捷规则,在 Scala 代码中需要写作\d\w(双反斜杠)。

3. 匹配多次字符的量词规则

量词规则规则说明示例正则目标文本匹配结果
*匹配前面的字符 / 分组 0 次或多次a*""、"a"、"aa"、"aaa"匹配所有目标文本
+匹配前面的字符 / 分组 1 次或多次a+""、"a"、"aa"、"aaa"匹配 "a"、"aa"、"aaa"
?匹配前面的字符 / 分组 0 次或 1 次a?""、"a"、"aa"、"aaa"匹配 ""、"a"
{n}匹配前面的字符 / 分组 恰好 n 次a{3}"a"、"aa"、"aaa"、"aaaa"匹配 "aaa"
{n,}匹配前面的字符 / 分组 至少 n 次a{2,}"a"、"aa"、"aaa"、"aaaa"匹配 "aa"、"aaa"、"aaaa"
{n,m}匹配前面的字符 / 分组 n 次到 m 次a{1,3}"a"、"aa"、"aaa"、"aaaa"匹配 "a"、"aa"、"aaa"

4. 特殊进阶规则

规则类型具体规则规则说明示例正则目标文本匹配结果
非贪婪匹配量词后加?(如*?+?优先匹配最少字符(默认是贪婪匹配,优先最多)a*?"aaaa"先匹配空字符串,按需匹配单个a
行首锚点^匹配字符串 / 行的开头位置^abc"abc123"、"xabc123"匹配 "abc123"(仅开头是 abc 的字符串)
行尾锚点$匹配字符串 / 行的结尾位置abc$"123abc"、"123abcx"匹配 "123abc"(仅结尾是 abc 的字符串)
单词边界锚点\b匹配单词与非单词的分隔位置\bcat\b"cat"、"category"、"cat123"仅匹配独立单词 "cat"
分组匹配()将多个规则视为整体,支持量词修饰(ab)+"ab"、"abab"、"abb"匹配 "ab"、"abab"
非捕获分组(?:)分组不参与子串提取,性能更优(?:ab)+"ab"、"abab"、"abb"匹配 "ab"、"abab"(不提取分组)

(五)实战案例:找出字符串中的手机号

需求分析

在混合文本中提取所有符合国内手机号格式的字符串,国内手机号的格式特征如下:

  1. 总长度为 11 位数字;
  2. 首位固定为数字1(运营商号段起始标识);
  3. 第二位数字取值范围为3-9(排除 0、1,对应移动、联通、电信等运营商号段);
  4. 后续 9 位为任意数字(0-9)。

正则规则构造

基于上述特征,构建的正则表达式为:1[3-9]\d{9}

规则解读

  1. 1:硬匹配手机号首位数字,确保以1开头,契合国内手机号的起始规范;
  2. [3-9]:字符类限定,匹配手机号第二位数字,仅允许 3、4、5、6、7、8、9,筛除不符合运营商号段的 0 和 1;
  3. \d{9}\d匹配单个数字(等价于[0-9]),{9}量词表示匹配连续 9 个数字,对应手机号前两位之后的剩余 9 位,确保总长度为 11 位。

完整可运行代码

import scala.util.matching.Regex

object PhoneNumberExtractDemo {
    def main(args: Array[String]): Unit = {
        // 1. 定义包含手机号的目标文本
        val targetText = 
            """
              |我的手机号是13812345678,同事的手机号是13987654321,
              |无效号码:12345678901(第二位是2,不符合)、1381234567(长度不足11位),
              |还有一个手机号:18612345678。
              |""".stripMargin

        // 2. 定义匹配手机号的正则表达式
        val phoneRegex: Regex = "1[3-9]\d{9}".r  // 核心正则规则

        // 3. 提取所有匹配的手机号
        val allPhoneNumbers = phoneRegex.findAllIn(targetText).toList

        // 4. 输出结果
        println("目标文本中的有效手机号:")
        allPhoneNumbers.foreach(phone => println(phone))
        
        // 输出结果:13812345678、13987654321、18612345678
    }
}