正则表达式入门指南
前言
在开发过程中,处理文本等信息时,常会遇到一些需要复杂筛选或匹配的需求,例如筛选指定类型的日志、筛选符合某种规范的文本、匹配字符串中的某些子串。很多场景下,通过简单的 if-else 或更通用的接口或许能处理这些问题,但筛选的复杂度上升之后,简单的条件判断或是单个接口已疲于应对,此时或许可以考虑正则表达式,通过一串规格化的字符串来匹配所需的文本。
一、简要介绍
正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于描述字符串匹配模式的语言,诞生于 20 世纪 50 年代的数学领域,后被深深嵌入到编程语言、文本编辑器和命令行工具中。
简单来说,正则表达式就是用一串字符来定义一个搜索模式,你可以用它来完成:
- 验证用户输入是否合法
- 从海量日志中提取关键信息
- 批量替换代码中的特定字符
虽然初时看起来像一串无厘头的字符,但掌握基本规则之后,经过合理地应用就能极大地提高开发过程中处理文本的效率。
二、基本规则
学习正则,本质上是在学习如何描述一个字符串集合。以下是构建模式的核心组件。
1)锚点
不消耗字符,只限定位置。
| 符号 | 含义 |
|---|---|
^ |
匹配字符串开头 |
$ |
匹配字符串结尾 |
\b |
匹配单词边界 |
例如 \bcat\b 匹配独立的单词 cat,而不匹配 category 中的 cat。
2)字面字符与元字符
- 字面字符:直接匹配自身,例如正则
cat匹配字符串中的cat。 - 元字符:有特殊含义的符号,需要转义才能匹配字面意义。
常见元字符:
. * + ? ^ $ { } [ ] ( ) | \
若想匹配 $ 符号本身,需用 \$ 表示。
3)字符类
用方括号 [] 定义一个字符集合,匹配其中任意一个字符。
| 写法 | 含义 |
|---|---|
[aeiou] |
匹配任意一个元音字母 |
[a-z] |
匹配小写字母 |
[0-9] |
匹配数字 |
[^a-z] |
取反,匹配非小写字母的字符 |
预定义字符类:
| 写法 | 含义 | 等价于 |
|---|---|---|
\d |
匹配一个数字 | [0-9] |
\w |
匹配一个单词字符(字母、数字、下划线) | [a-zA-Z0-9_] |
\s |
匹配一个空白字符(空格、制表符、换行等) | — |
\D、\W、\S 分别对应上述类的反义,即非数字、非单词字符、非空白字符。
4)量词
控制前面元素的出现次数。
| 写法 | 含义 |
|---|---|
* |
零次或多次(贪婪) |
+ |
一次或多次(贪婪) |
? |
零次或一次 |
{n} |
恰好 n 次 |
{n,} |
至少 n 次 |
{n,m} |
n 到 m 次 |
例如,\d{3,4} 表示匹配 3 到 4 位数字。
5)分组与捕获
用圆括号 () 进行分组,将若干字符视为一个整体,会同时捕获匹配内容以便后续引用。
例如 (ab)+ 匹配 ab、abab。捕获组可以从左到右按括号顺序编号,用 $1、$2 或 \1、\2(根据工具而异)进行反向引用。
非捕获分组 (?:exp) 只分组不捕获,提高效率。
例如 \b(\w+)\b\s+\1\b 用来匹配若干个空格分割的重复的单词,如 word word、baby baby。
6)分支与转义
| 表示"或"。cat|dog 匹配 cat 或 dog。
\ 用于转义元字符,如 \. 匹配句点 .。
7)贪婪与懒惰
- 贪婪匹配:正则表达式中包含重复的限定符时,通常的行为是匹配尽可能多的字符。
- 懒惰匹配:与贪婪匹配相反,通常需要匹配尽可能少的字符。
量词默认是"贪婪"的,会尽可能多地匹配。在量词后加 ? 变为懒惰匹配,匹配尽可能少。
例如 a.*b,它将会匹配最长的以 a 开始、以 b 结束的字符串。如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab。但是此时我们可能需要匹配的是 ab,这样的话就需要用到懒惰匹配来匹配尽可能少的字符。
字符串 <div>hello</div>:使用 <.*> 来贪婪匹配整个字符串;使用 <.*?> 懒惰匹配前后两个 <div>。
三、用途与事例
理论过后,还是需要看看实际场景中的正则如何发挥作用。以下示例基于 Java 语法。
1)表单验证
验证电子邮件地址(简化版):
^[\w.-]+@[\w-]+\.[a-zA-Z]{2,}$
说明:用户名部分允许字母数字、点、下划线、百分号、加号、减号;@ 符号;域名部分允许字母数字和连字符;顶级域名至少 2 个字母。更精确的邮箱验证非常复杂,但这个版本能应对多数日常场景。
2)日志提取
从 Nginx 日志中提取 IP 和访问时间:
String log = "192.168.1.1 - - [10/May/2026:14:30:00 +0000] \"GET /index.html HTTP/1.1\" 200 2326";
String regex = "^(\\S+) .*\\[([^\\]]+)\\].*\"(\\S+) (\\S+).*\" (\\d+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(log);
匹配结果:
| 组 | 内容 |
|---|---|
$1 |
IP 地址 |
$2 |
时间戳 |
$3 |
请求方法(GET) |
$4 |
请求路径 |
$5 |
状态码 |
3)代码重构
假设需要处理一段 JavaScript 代码文本,将其中的 var 声明统一替换为 let,但仅限于变量声明,不触碰字符串或注释中的 var。利用 Java 的 replaceAll 或 Matcher.replaceAll 即可完成。
import java.util.regex.Pattern;
public class CodeRefactor {
public static void main(String[] args) {
String jsCode = "var name = 'var user'; var age = 25; /* var placeholder */";
// 正则:\bvar\b 匹配独立单词 var,\s+ 匹配空格,(\w+) 捕获变量名
String regex = "\\bvar\\b\\s+(\\w+)";
String replacement = "let $1";
String refactored = jsCode.replaceAll(regex, replacement);
System.out.println(refactored);
// 输出: let name = 'var user'; let age = 25; /* var placeholder */
}
}
4)非结构化数据整理
从一段文本中提取所有电话号码(假设格式为 xxx-xxxx-xxxx)。使用 Matcher.find() 可以迭代所有匹配,避免长数字串的误匹配则用单词边界 \b。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.List;
public class PhoneExtractor {
public static void main(String[] args) {
String text = "请联系客服:400-1234-5678,或拨打010-8888-9999,手机138-0000-1234也有效。";
String regex = "\\b\\d{3}-\\d{4}-\\d{4}\\b"; // 更精确的格式
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
List<String> phones = new ArrayList<>();
while (matcher.find()) {
phones.add(matcher.group());
}
System.out.println("提取到的电话号码: " + phones);
// 输出: [400-1234-5678, 010-8888-9999, 138-0000-1234]
}
}
四、问题与误区
正则虽强大,但用不好就是"一次编写,永远调试"的噩梦。以下罗列一些常见的误区与陷阱。
1)把正则当成万能工具
- 误区:试图用一条正则解决所有情况,比如完美匹配所有电子邮件地址。
- 纠正:正则适合处理规则明确、非递归的模式。复杂业务逻辑请结合代码判断,而不是写出几十行"面条正则"。比如邮箱验证,正则做表层过滤,再发送确认邮件才是可靠方案。
2)灾难性回溯
当正则引擎面对嵌套量词且匹配失败时,可能尝试指数级路径,导致 CPU 占用飙升。
例:使用 (a+)+b 匹配 aaaaaaaaac,会导致大量回溯。
避免方法:
- 尽量使用非捕获组和占有量词(如果引擎支持)。
- 写正则时避免
(x+)+或类似的嵌套贪婪结构。 - 使用前考虑利用特定工具或网站测试正则性能。
3)忘记转义与上下文
- 点号
.匹配任意字符,若想匹配 IP 地址中的点,必须用\.,否则192.168.1.1中的点可能匹配任意字符,造成误判。 - 连字符
-在字符类中间表示范围,若想匹配字面-,需放在字符类的开头或结尾,如[-a-z]或[a-z-]。
4)多行与单行模式的混淆
- 多行模式(m 标志):
^和$匹配每一行的开头结尾。为了匹配换行后文本需开启多行模式。 - 单行模式(s 标志):点号
.匹配包括换行在内的所有字符。
初次接触正则时,有可能在未开启相应模式时认为 ^.*$ 不跨行或点号不匹配换行,根源是没有确认这些标志。
5)忽略性能与可读性
复杂正则不易读,时间长了之后较难回溯含义。
解决方案:
- 添加注释(若引擎支持 x 模式),或在代码中拆分成多段变量。
- 优先选择简单、直接的表达。例如用
\d{3}-\d{4}-\d{4}而不是企图用一堆环视来限制号码段,除非确实有严格业务需求。
五、结语
正则表达式是一门"写一次、读万次"的手艺。它抽象、精练,能将开发过程从烦琐的字符串操作中解放出来;同时又固执、深奥,稍不留神就会引入隐蔽的 bug。
对于入门而言,首先掌握基础规则,再在真实项目中学以致用,不必试图背下所有语法,善用在线工具(regex101、RegExr 等)即时测试,方可熟能生巧。
正则不会淘汰,因为文本永远是最通用的数据格式之一,善用正则表达式,能让代码更简洁,也能让文本处理的思维变得更加"模式化"和精准。
附录:常用的实用正则表达式整理
| 用途 | 正则表达式 |
|---|---|
| 只能输入数字 | ^[0-9]*$ |
| 只能输入 n 位的数字 | ^\d{n}$ |
| 只能输入至少 n 位的数字 | ^\d{n,}$ |
| 只能输入 m~n 位的数字 | ^\d{m,n}$ |
| 只能输入零和非零开头的数字 | `^(0 |
| 只能输入有两位小数的正实数 | ^[0-9]+(\.[0-9]{2})?$ |
| 只能输入有 1~3 位小数的正实数 | ^[0-9]+(\.[0-9]{1,3})?$ |
| 只能输入非零的正整数 | ^\+?[1-9][0-9]*$ |
| 只能输入非零的负整数 | ^-[1-9][0-9]*$ |
| 只能输入长度为 3 的字符 | ^.{3}$ |
| 只能输入由 26 个英文字母组成的字符串 | ^[A-Za-z]+$ |
| 只能输入由 26 个大写英文字母组成的字符串 | ^[A-Z]+$ |
| 只能输入由 26 个小写英文字母组成的字符串 | ^[a-z]+$ |
| 只能输入由数字和 26 个英文字母组成的字符串 | ^[A-Za-z0-9]+$ |
| 只能输入由数字、26 个英文字母或者下划线组成的字符串 | ^\w+$ |
| 验证用户密码 | ^[a-zA-Z]\w{5,17}$ |
验证是否含有 ^%&',;=?$" 等字符 |
[^%&',;=?$\x22]+ |
| 只能输入汉字 | ^[\u4e00-\u9fa5]{0,}$ |
| 验证 Email 地址 | /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-])+/ |
| 验证 InternetURL | ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$ |
| 验证电话号码 | ^(\(\d{3,4}-|\d{3,4}-)?\d{7,8}$ |
| 验证身份证号(15 位或 18 位数字) | ^\d{15}|\d{18}$ |
| 验证一年的 12 个月 | ^(0?[1-9]|1[0-2])$ |
| 验证一个月的 31 天 | ^((0?[1-9])|((1|2)[0-9])|30|31)$ |

浙公网安备 33010602011771号