正则表达式:从入门到精通
正则表达式:解锁文本处理的终极利器
无论是前端表单验证、后端数据清洗,还是日志分析、批量文本处理,正则表达式始终是程序员手中最高效的文本处理工具。它看似晦涩难懂的符号背后,藏着一套极简且强大的文本匹配逻辑。本文将带你从零开始,系统掌握正则表达式的核心语法、实战场景与进阶技巧,让你真正驾驭这门“文本处理的魔法语言”。
前言
你是否遇到过这些场景:
- 前端页面需要校验用户输入的手机号、邮箱格式是否合法
- 从成千上万行日志中,快速提取出所有报错的接口地址和时间
- 批量修改文档中所有符合特定格式的文本,手动修改要花几小时
- 爬虫抓取网页后,从杂乱的HTML中精准提取出想要的内容
这些问题,用正则表达式只需要一行代码就能解决。正则表达式(Regular Expression,常简写为Regex/RegExp),本质上是一套用于描述文本匹配规则的语法体系,它通过一套预定义的符号组合,就能实现对文本的精准匹配、提取、替换和校验。
从1950年代数学家斯蒂芬·克莱尼提出正则表达式概念,到如今成为所有主流编程语言的标配能力,它历经70余年依然是文本处理领域无可替代的工具。接下来,我们就从基础到实战,彻底搞懂正则表达式。
一、正则表达式的核心基础
1.1 什么是正则表达式
简单来说,正则表达式是一个由普通字符和特殊元字符组成的匹配模式。这个模式会告诉计算机,我们要在文本中查找什么样的内容。
举个最简单的例子:
- 正则表达式
hello,会精准匹配文本中所有出现的hello字符串,这就是纯普通字符的匹配。 - 而正则表达式
h.llo,其中.是特殊元字符,代表匹配任意单个字符,它就能匹配hello、hallo、h1llo、h@llo等所有符合“h+任意字符+llo”格式的内容。
这就是正则的核心魅力:通过少量符号,就能描述复杂的文本规则。
1.2 正则的典型应用场景
正则表达式的应用几乎覆盖了开发的全流程,最常见的场景包括:
| 场景分类 | 具体用途 |
|---|---|
| 表单校验 | 手机号、邮箱、身份证号、密码强度、URL格式合法性校验 |
| 文本提取 | 从日志、HTML、文档中提取符合规则的内容(如链接、数字、关键词) |
| 文本替换 | 批量替换文档中符合规则的内容,支持保留部分匹配结果 |
| 数据清洗 | 处理脏数据,过滤无效字符、格式化文本内容 |
| 语法高亮 | 代码编辑器、Markdown渲染器通过正则实现语法关键字高亮 |
| 路由匹配 | Web框架(如Vue Router、Express)通过正则实现动态路由匹配 |
二、正则表达式核心语法全解
正则的语法体系并不复杂,核心可以分为元字符、量词、字符类、分组、断言五大模块,我们逐个拆解,配合实例理解,记忆更轻松。
注:本文所有示例均采用通用正则语法,兼容绝大多数编程语言,特殊语法差异会单独标注。
2.1 基础元字符:正则的“关键字”
元字符是正则中具有特殊含义的字符,是构成匹配规则的基础,最核心的元字符如下表:
| 元字符 | 匹配规则 | 示例 | 匹配结果 |
|---|---|---|---|
. | 匹配任意单个字符(换行符\n除外) | a.b | 匹配aab、a1b、a@b,不匹配ab、a\nb |
^ | 匹配字符串的开头 | ^hello | 匹配以hello开头的文本,不匹配中间出现的hello |
$ | 匹配字符串的结尾 | world$ | 匹配以world结尾的文本,不匹配中间出现的world |
\d | 匹配任意数字,等价于[0-9] | phone:\d{11} | 匹配phone:13800138000这类11位手机号格式 |
\D | 匹配任意非数字字符,等价于[^0-9] | \D+ | 匹配abc、@#$、中文等非数字内容 |
\w | 匹配字母、数字、下划线,等价于[a-zA-Z0-9_] | \w+ | 匹配user_name123,不匹配user-name、中文 |
\W | 匹配非字母、数字、下划线的字符 | \W | 匹配-、@、空格、中文等 |
\s | 匹配任意空白字符(空格、制表符\t、换行符\n、回车符\r) | a\sb | 匹配a b、a\tb、a\nb |
\S | 匹配任意非空白字符 | \S+ | 匹配除空格外的所有连续字符 |
\ | 转义符,将特殊元字符转为普通字符 | \. | 匹配普通的.符号,而非任意字符 |
核心示例:匹配以数字开头,以字母结尾的字符串
^\d.*[a-zA-Z]$
^限定开头必须是数字\d匹配开头的数字.*匹配中间任意数量的任意字符[a-zA-Z]$限定结尾必须是字母
2.2 量词:控制匹配的次数
量词用于指定前面的字符/规则需要匹配多少次,是正则中实现灵活匹配的核心,分为贪婪量词和非贪婪量词(后续进阶部分详解),基础量词如下:
| 量词 | 匹配规则 | 示例 | 匹配结果 |
|---|---|---|---|
* | 匹配前面的规则0次或多次,等价于{0,} | a*b | 匹配b、ab、aab、aaab |
+ | 匹配前面的规则1次或多次,等价于{1,} | a+b | 匹配ab、aab,不匹配b |
? | 匹配前面的规则0次或1次,等价于{0,1} | colou?r | 匹配color、colour(美式/英式拼写兼容) |
{n} | 匹配前面的规则恰好n次 | \d{11} | 匹配恰好11位数字,用于手机号校验 |
{n,} | 匹配前面的规则至少n次 | \d{6,} | 匹配至少6位数字,用于密码强度校验 |
{n,m} | 匹配前面的规则至少n次,最多m次 | \d{4,6} | 匹配4-6位数字,如验证码 |
核心示例:匹配8-16位密码,必须包含字母和数字
^(?=.*[a-zA-Z])(?=.*\d)\w{8,16}$
^和$限定整个字符串的开头和结尾(?=.*[a-zA-Z])断言必须包含至少一个字母(?=.*\d)断言必须包含至少一个数字\w{8,16}匹配8-16位的字母、数字、下划线
2.3 字符类:自定义匹配范围
当我们需要匹配特定范围内的字符时,就需要用到字符类,用[]包裹,代表匹配方括号内的任意一个字符。
基础用法
[abc]:匹配a、b、c中的任意一个字符[^abc]:排除型字符类,匹配除了a、b、c之外的任意一个字符[a-z]:匹配任意小写字母,-代表范围[A-Z]:匹配任意大写字母[0-9]:匹配任意数字,等价于\d[a-zA-Z0-9]:匹配任意字母和数字
实用示例
- 匹配中文:
[\u4e00-\u9fa5](Unicode中文编码范围) - 匹配十六进制字符:
[0-9a-fA-F] - 匹配除了数字和字母之外的特殊字符:
[^a-zA-Z0-9]
2.4 分组与捕获:提取匹配内容
分组用()包裹,将多个规则合并为一个整体,不仅可以控制量词的作用范围,还能捕获匹配到的内容,用于后续的提取或替换。
1. 基础分组
将多个字符视为一个整体,控制量词作用:
- 正则
(ab)+:匹配ab、abab、ababab,+作用于整个ab分组 - 正则
ab+:匹配ab、abb、abbb,+仅作用于b
2. 捕获分组
括号内的匹配内容会被正则引擎“捕获”,可以通过$1、$2(替换场景)或分组索引(提取场景)获取,序号从1开始,按左括号顺序计数。
示例:提取手机号的号段和后8位
^(\d{3})(\d{8})$
- 对于手机号
13800138000,$1捕获到138,$2捕获到00138000
示例:日期格式替换
将2024-05-20替换为05/20/2024,正则匹配规则:
^(\d{4})-(\d{2})-(\d{2})$
替换表达式:
$2/$3/$1
3. 非捕获分组
如果我们只需要分组的整体功能,不需要捕获内容,可以用(?:)语法,减少正则引擎的内存开销:
(?:ab)+:匹配ab的重复,但不会捕获分组内容,无法通过$1获取
2.5 零宽断言:精准定位匹配位置
零宽断言(也叫环视)是正则的进阶能力,它只匹配位置,不匹配字符,就像给匹配规则加了一个“定位器”,只在符合条件的位置才进行匹配。
零宽断言分为四大类,核心语法如下:
| 断言类型 | 语法 | 匹配规则 |
|---|---|---|
| 正向先行断言 | (?=pattern) | 匹配后面紧跟着pattern的位置 |
| 负向先行断言 | (?!pattern) | 匹配后面没有紧跟着pattern的位置 |
| 正向后行断言 | (?<=pattern) | 匹配前面紧跟着pattern的位置 |
| 负向后行断言 | (?<!pattern) | 匹配前面没有紧跟着pattern的位置 |
实用示例
-
正向先行断言:匹配
@前面的邮箱用户名\w+(?=@)对于
user123@example.com,只会匹配到user123,不会匹配@和后面的内容。 -
正向后行断言:匹配
id=后面的数字(?<=id=)\d+对于
user?id=12345,只会匹配到12345。 -
负向先行断言:匹配不包含
abc的字符串^(?!.*abc).*$ -
负向后行断言:匹配前面不是
$的数字(?<!\$)\d+对于
$100和200,只会匹配到200。
三、高频实战场景案例
掌握了基础语法,我们来看看开发中最常用的正则实战案例,每一个都可以直接复制到项目中使用。
3.1 表单校验场景
表单校验是正则最常用的场景,以下是国内开发中最常用的校验规则:
1. 手机号校验(中国大陆)
^1[3-9]\d{9}$
- 规则说明:以1开头,第二位是3-9,后面跟着9位数字,总共11位
2. 邮箱地址校验
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
- 规则说明:用户名支持字母、数字、特殊符号,域名支持多级,后缀至少2位字母
3. 身份证号校验(18位)
^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$
- 规则说明:前6位地址码、8位出生日期(19/20开头)、3位顺序码、1位校验位(数字或X/x)
4. 密码强度校验
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,20}$
- 规则说明:8-20位,必须包含大小写字母、数字、特殊符号
3.2 文本提取场景
1. 提取文本中所有的URL
https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}[^\s]*
- 匹配
http://或https://开头的所有链接,支持带路径、参数的URL
2. 提取HTML中的图片地址
(?<=<img.*?src=")[^"]+(?=")
- 结合后行/先行断言,精准提取
<img>标签中src属性的内容,不会匹配标签其他部分
3. 提取文本中所有的中文
[\u4e00-\u9fa5]+
3.3 批量文本替换场景
1. 隐藏手机号中间4位
匹配规则:
^(\d{3})\d{4}(\d{4})$
替换表达式:
$1****$2
- 效果:
13800138000→138****8000
2. Markdown标题批量添加序号
匹配规则(匹配二级标题):
^## (.*)$
替换表达式:
## 1. $1
3. 去除文本中所有的HTML标签
<[^>]+>
- 替换为空字符串,即可清除所有
<xxx>格式的HTML标签,保留纯文本
3.4 日志分析场景
从Nginx访问日志中,提取所有404错误的请求路径和IP地址:
^(\S+) .*?" \s404\s.*?"(.*?)" ".*?$
$1捕获客户端IP,$2捕获404的请求路径
四、进阶技巧与避坑指南
4.1 贪婪匹配 vs 非贪婪匹配
正则的量词默认是贪婪模式,会尽可能匹配最长的符合规则的内容;而在量词后加?,就会转为非贪婪模式(惰性模式),会尽可能匹配最短的符合规则的内容。
示例对比
文本内容:
<div>标题1</div><div>标题2</div>
-
贪婪匹配:
<div>.*</div>- 匹配结果:整个字符串
<div>标题1</div><div>标题2</div>,因为.*会尽可能往后匹配,直到最后一个</div>
- 匹配结果:整个字符串
-
非贪婪匹配:
<div>.*?</div>- 匹配结果:分别匹配到
<div>标题1</div>和<div>标题2</div>,.*?匹配到第一个</div>就停止
- 匹配结果:分别匹配到
使用场景:提取HTML标签、成对的符号内容时,优先使用非贪婪匹配,避免匹配范围超出预期。
4.2 正则性能优化:避免回溯陷阱
正则引擎在匹配失败时,会进行回溯(回到之前的匹配位置尝试其他可能),不当的正则写法会导致回溯次数呈指数级增长,造成程序卡顿甚至崩溃。
优化核心原则
- 避免嵌套的量词:比如
(a+)*,这种写法在匹配失败时会产生海量回溯,是最常见的性能陷阱 - 精准限定匹配范围:能用
\d{11}就不用\d+,能用[^"]*就不用.*,减少匹配的不确定性 - 优先使用非捕获分组:不需要提取内容时,用
(?:)代替(),减少引擎的捕获开销 - 避免开头使用通配符:
^.*abc的性能远低于^abc,因为通配符开头会让引擎遍历整个字符串 - 合理使用零宽断言:减少不必要的分组和匹配,提升定位效率
4.3 常见的坑与解决方案
-
.不匹配换行符- 问题:正则
.*无法匹配包含换行的多行文本 - 解决方案:开启正则的单行模式(DotAll),或用
[\s\S]*代替.*,匹配所有字符(包括换行)
- 问题:正则
-
^和$的行匹配问题- 问题:默认情况下,
^和$只匹配整个字符串的开头和结尾,无法匹配每一行的开头结尾 - 解决方案:开启正则的多行模式(Multiline)
- 问题:默认情况下,
-
字符类中的元字符失效
- 问题:在
[]中,绝大多数元字符都会变成普通字符,比如[.]只会匹配.本身,而非任意字符 - 解决方案:字符类中仅需转义
]、-、\这几个特殊字符,其他无需转义
- 问题:在
-
大小写匹配问题
- 问题:默认正则区分大小写,
abc无法匹配ABC - 解决方案:开启正则的忽略大小写模式(IgnoreCase)
- 问题:默认正则区分大小写,
五、主流编程语言中的正则使用
正则是跨语言的能力,不同语言的API略有差异,核心语法完全通用,以下是4种主流语言的极简使用示例。
5.1 Python 中的 re 模块
Python通过内置的re模块实现正则功能,核心API如下:
import re
# 1. 校验匹配:判断字符串是否符合规则
phone_pattern = r"^1[3-9]\d{9}$"
is_valid = re.match(phone_pattern, "13800138000")
print(bool(is_valid)) # 输出 True
# 2. 提取内容:提取文本中所有符合规则的内容
text = "我的邮箱是user1@example.com,备用邮箱是user2@test.com"
email_pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
emails = re.findall(email_pattern, text)
print(emails) # 输出 ['user1@example.com', 'user2@test.com']
# 3. 文本替换
phone_text = "13800138000"
hide_pattern = r"^(\d{3})\d{4}(\d{4})$"
result = re.sub(hide_pattern, r"\1****\2", phone_text)
print(result) # 输出 138****8000
5.2 JavaScript 中的正则对象
JS中正则有两种写法,字面量/pattern/flags和new RegExp()构造函数,核心用法:
// 1. 校验匹配
const phonePattern = /^1[3-9]\d{9}$/;
const isValid = phonePattern.test("13800138000");
console.log(isValid); // 输出 true
// 2. 提取内容
const text = "我的邮箱是user1@example.com,备用邮箱是user2@test.com";
const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
const emails = text.match(emailPattern);
console.log(emails); // 输出 ['user1@example.com', 'user2@test.com']
// 3. 文本替换
const phoneText = "13800138000";
const hidePattern = /^(\d{3})\d{4}(\d{4})$/;
const result = phoneText.replace(hidePattern, "$1****$2");
console.log(result); // 输出 138****8000
5.3 Java 中的 Pattern 与 Matcher
Java通过java.util.regex包下的Pattern和Matcher类实现正则功能:
import java.util.regex.*;
import java.util.ArrayList;
import java.util.List;
public class RegexDemo {
public static void main(String[] args) {
// 1. 校验匹配
String phonePattern = "^1[3-9]\\d{9}$";
boolean isValid = Pattern.matches(phonePattern, "13800138000");
System.out.println(isValid); // 输出 true
// 2. 提取内容
String text = "我的邮箱是user1@example.com,备用邮箱是user2@test.com";
String emailPattern = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}";
Pattern pattern = Pattern.compile(emailPattern);
Matcher matcher = pattern.matcher(text);
List<String> emails = new ArrayList<>();
while (matcher.find()) {
emails.add(matcher.group());
}
System.out.println(emails); // 输出 [user1@example.com, user2@test.com]
// 3. 文本替换
String phoneText = "13800138000";
String hidePattern = "^(\\d{3})\\d{4}(\\d{4})$";
String result = phoneText.replaceAll(hidePattern, "$1****$2");
System.out.println(result); // 输出 138****8000
}
}
5.4 Go 中的 regexp 包
Go通过内置的regexp包实现正则功能,核心用法:
package main
import (
"fmt"
"regexp"
)
func main() {
// 1. 校验匹配
phonePattern := regexp.MustCompile(`^1[3-9]\d{9}$`)
isValid := phonePattern.MatchString("13800138000")
fmt.Println(isValid) // 输出 true
// 2. 提取内容
text := "我的邮箱是user1@example.com,备用邮箱是user2@test.com"
emailPattern := regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
emails := emailPattern.FindAllString(text, -1)
fmt.Println(emails) // 输出 [user1@example.com user2@test.com]
// 3. 文本替换
phoneText := "13800138000"
hidePattern := regexp.MustCompile(`^(\d{3})\d{4}(\d{4})$`)
result := hidePattern.ReplaceAllString(phoneText, "$1****$2")
fmt.Println(result) // 输出 138****8000
}
六、学习资源与调试工具
6.1 在线调试工具
写正则的过程中,调试是必不可少的,推荐几个顶尖的在线工具:
- Regex101:https://regex101.com/ (最推荐,支持多语言、实时匹配、语法解释、回溯分析)
- Regexr:https://regexr.com/ (界面友好,有详细的语法参考和示例)
- RegEx Testing:https://www.regextester.com/ (轻量简洁,支持多语言切换)
6.2 系统学习资源
- 《精通正则表达式》:正则领域的圣经级著作,深入讲解正则引擎原理和高级技巧
- MDN 正则表达式指南:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions (JS正则权威参考,适合入门)
- 正则表达式30分钟入门教程:https://deerchao.cn/tutorials/regex/regex.htm (国内最经典的入门教程,通俗易懂)
总结
正则表达式不是一门需要死记硬背的语法,而是一套解决文本处理问题的思维工具。很多人觉得正则晦涩,只是因为没有找到它的逻辑规律——从基础的元字符、量词,到分组、断言,所有的规则都是为了更精准地描述“我们要找什么”。
本文覆盖了正则从入门到进阶的所有核心内容,从基础语法到实战案例,再到性能优化。想要真正掌握正则,最好的方式就是边学边练:遇到文本处理问题时,先尝试用正则解决,在调试中理解每一个符号的含义,很快你就能驾驭这把文本处理的“瑞士军刀”。
最后记住:正则不是越复杂越厉害,能用最简单的规则解决问题,才是正则的最高境界。
更多推荐

所有评论(0)