(一)正则表达式的定义
正则表达式(Regular Expression)是一种用于匹配、查找和替换文本中特定模式的字符串,它独立于具体编程语言,是一种通用的文本处理技术,广泛应用于各类数据处理场景。
它的核心应用场景包括:
- 数据验证:在用户注册、登录等交互场景中,验证输入信息是否符合格式要求。例如,验证手机号码、身份证号码、邮箱地址、密码复杂度(如要求包含大小写字母 + 数字 + 特殊字符)等格式的正确性。
- 文本搜索和替换:在文本编辑器(如 Word)、代码编辑器(如 IDEA、VS Code)等工具中,精准查找特定的单词、短语、代码模式,并进行批量替换操作。比如,在一篇文档中查找所有
yyyy-mm-dd格式的日期并统一替换为yyyy/mm/dd格式,或在代码中批量替换废弃的函数名。 - 数据提取:从网页源码、日志文件、配置文件等大量非结构化 / 半结构化文本数据中,提取指定的关键信息。例如,从 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
正则表达式使用步骤总结
- 定义匹配规则:编写符合需求的正则表达式,通过
.r后缀将普通字符串转为 Scala 正则对象(scala.util.matching.Regex)。 - 准备目标数据:定义需要进行匹配、查找或校验的目标字符串(可来自用户输入、文件读取等场景)。
- 执行匹配操作:根据业务需求调用对应的正则方法(如
findFirstIn找首个匹配、findAllIn找所有匹配、matches做完全校验),并处理匹配结果。
(三)正则表达式的基本组成部分
无论多么复杂的正则表达式,其核心都是由以下 4 个基础部分组合构成,掌握这 4 部分即可搭建任意复杂的匹配规则。
-
字符类:用于匹配单个字符或指定范围的字符,是正则表达式的基础单元。
- 单个字符:直接书写字符本身,如
a匹配字符a,5匹配数字5,@匹配符号@。 - 字符集合:用
[]包裹多个字符,匹配其中任意一个,如[abc]匹配a、b、c中的任意一个字符,[012345]匹配 0-5 中的任意一个数字。 - 字符范围:在
[]中使用-指定连续范围,如[a-z]匹配小写字母 a 到 z 的任意一个,[A-Z]匹配大写字母 A 到 Z 的任意一个,[0-9]匹配 0 到 9 的任意一个数字,[a-zA-Z0-9]匹配大小写字母和数字中的任意一个。 - 排除字符集:在
[]开头添加^,表示匹配除该集合外的任意一个字符,如[^abc]匹配除a、b、c之外的所有字符,[^0-9]匹配非数字字符。
- 单个字符:直接书写字符本身,如
-
量词:用于指定前面的「字符」或「字符组」出现的次数,是实现多字符匹配的核心。
*:匹配前面的字符 / 字符组出现 0 次或多次(贪婪匹配,尽可能多匹配),如a*可匹配空字符串、a、aa、aaa等。+:匹配前面的字符 / 字符组出现 1 次或多次,如a+可匹配a、aa、aaa等,但不能匹配空字符串。?:匹配前面的字符 / 字符组出现 0 次或 1 次(最多 1 次),如a?可匹配空字符串或a。{n}:匹配前面的字符 / 字符组出现 恰好 n 次(n 为非负整数),如a{3}仅匹配aaa。{n,}:匹配前面的字符 / 字符组出现 至少 n 次,如a{2,}可匹配aa、aaa、aaaa等。{n,m}:匹配前面的字符 / 字符组出现 n 次到 m 次(包含 n 和 m,n≤m),如a{1,3}可匹配a、aa、aaa。
-
锚点:用于指定匹配的位置(不匹配具体字符),常用于精准校验文本的开头和结尾。
^:匹配行首(字符串的开头),如^abc表示匹配以abc开头的字符串(仅匹配开头位置,后跟 abc)。$:匹配行尾(字符串的结尾),如abc$表示匹配以abc结尾的字符串(仅匹配结尾位置,前面跟 abc)。\b:匹配单词边界(单词与非单词的分隔处,如空格、标点、开头 / 结尾等),如\bcat\b仅匹配独立的单词cat,不匹配category中的cat部分。\B:匹配非单词边界,与\b相反,如\Bcat\B仅匹配单词内部的cat(如category中的cat)。
-
分组:使用括号
()将多个字符或规则包裹为一个整体(分组),可对分组整体使用量词,也可用于提取匹配的子串。- 基本分组:如
(ab)+表示将ab作为一个整体,匹配其出现 1 次或多次,可匹配ab、abab、ababab等;若不分组,ab+仅表示b出现 1 次或多次(匹配ab、abb、abbb等)。 - 分组提取:匹配成功后,可通过分组索引提取对应子串(索引从 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"(不提取分组) |
(五)实战案例:找出字符串中的手机号
需求分析
在混合文本中提取所有符合国内手机号格式的字符串,国内手机号的格式特征如下:
- 总长度为 11 位数字;
- 首位固定为数字
1(运营商号段起始标识); - 第二位数字取值范围为
3-9(排除 0、1,对应移动、联通、电信等运营商号段); - 后续 9 位为任意数字(0-9)。
正则规则构造
基于上述特征,构建的正则表达式为:1[3-9]\d{9}
规则解读
1:硬匹配手机号首位数字,确保以1开头,契合国内手机号的起始规范;[3-9]:字符类限定,匹配手机号第二位数字,仅允许 3、4、5、6、7、8、9,筛除不符合运营商号段的 0 和 1;\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
}
}