正则表达式(Regular Expression,简称 Regex)是一种强大的文本模式匹配工具,几乎所有现代编程语言都支持它。无论是数据验证、文本搜索替换,还是日志分析,正则表达式都是开发者必备的核心技能。本指南将带你从零开始,全面掌握正则表达式。
目录
核心要点
- 模式匹配:正则表达式是描述字符串模式的语言,用于搜索、匹配和操作文本。
- 通用性:几乎所有编程语言都支持正则表达式,语法大体相同。
- 强大功能:可以用简洁的表达式描述复杂的文本模式。
- 性能考量:复杂的正则表达式可能导致性能问题,需要谨慎设计。
- 可读性:正则表达式可能难以阅读,建议添加注释或拆分复杂模式。
- 测试重要:在生产环境使用前,务必充分测试正则表达式。
想要快速测试你的正则表达式?试试我们的免费在线工具,支持实时匹配和多种编程语言语法。
什么是正则表达式?
正则表达式(Regular Expression)是一种用于描述字符串模式的形式语言。它起源于1950年代的数学理论,由数学家 Stephen Cole Kleene 首次提出。如今,正则表达式已成为文本处理的标准工具,被广泛应用于:
- 数据验证:验证用户输入(邮箱、电话、密码等)
- 文本搜索:在大量文本中查找特定模式
- 文本替换:批量修改符合模式的文本
- 数据提取:从文本中提取结构化信息
- 日志分析:解析和分析日志文件
正则表达式的核心思想是用特殊字符和规则来描述一类字符串,而不是具体的某个字符串。
正则表达式基础语法
字符匹配
最基本的正则表达式就是普通字符,它们匹配自身:
| 模式 | 描述 | 示例 |
|---|---|---|
abc |
匹配字面字符串 "abc" | "abc" ✓, "abcd" ✓ |
. |
匹配任意单个字符(除换行符) | "a.c" 匹配 "abc", "a1c" |
\d |
匹配任意数字 [0-9] | "\d\d" 匹配 "42" |
\D |
匹配任意非数字 | "\D" 匹配 "a" |
\w |
匹配单词字符 [a-zA-Z0-9_] | "\w+" 匹配 "hello_123" |
\W |
匹配非单词字符 | "\W" 匹配 "@" |
\s |
匹配空白字符(空格、制表符等) | "a\sb" 匹配 "a b" |
\S |
匹配非空白字符 | "\S+" 匹配 "hello" |
\\ |
匹配反斜杠本身 | "\\" 匹配 "\" |
量词
量词用于指定前面的元素可以出现的次数:
| 量词 | 描述 | 示例 |
|---|---|---|
* |
匹配0次或多次 | a* 匹配 "", "a", "aaa" |
+ |
匹配1次或多次 | a+ 匹配 "a", "aaa",不匹配 "" |
? |
匹配0次或1次 | a? 匹配 "", "a" |
{n} |
精确匹配n次 | a{3} 匹配 "aaa" |
{n,} |
匹配至少n次 | a{2,} 匹配 "aa", "aaa", "aaaa" |
{n,m} |
匹配n到m次 | a{2,4} 匹配 "aa", "aaa", "aaaa" |
位置锚点
锚点用于匹配位置而不是字符:
| 锚点 | 描述 | 示例 |
|---|---|---|
^ |
匹配字符串开头 | ^hello 匹配以 "hello" 开头的字符串 |
$ |
匹配字符串结尾 | world$ 匹配以 "world" 结尾的字符串 |
\b |
匹配单词边界 | \bcat\b 匹配 "cat" 但不匹配 "category" |
\B |
匹配非单词边界 | \Bcat 匹配 "category" 中的 "cat" |
分组与捕获
分组允许你将多个字符作为一个单元处理:
| 语法 | 描述 | 示例 |
|---|---|---|
(abc) |
捕获组,匹配并记住 "abc" | (ab)+ 匹配 "abab" |
(?:abc) |
非捕获组,匹配但不记住 | (?:ab)+ 匹配 "abab" |
\1, \2 |
反向引用第n个捕获组 | (a)(b)\1\2 匹配 "abab" |
(?<name>abc) |
命名捕获组 | (?<year>\d{4}) |
(a|b) |
或运算,匹配 a 或 b | (cat|dog) 匹配 "cat" 或 "dog" |
字符类
字符类定义一组可以匹配的字符:
| 语法 | 描述 | 示例 |
|---|---|---|
[abc] |
匹配 a、b 或 c 中的任意一个 | [aeiou] 匹配元音字母 |
[^abc] |
匹配除 a、b、c 外的任意字符 | [^0-9] 匹配非数字 |
[a-z] |
匹配 a 到 z 的任意字符 | [A-Za-z] 匹配任意字母 |
[0-9] |
匹配 0 到 9 的任意数字 | 等同于 \d |
高级特性
零宽断言
零宽断言(Lookaround)匹配位置而不消耗字符:
| 语法 | 名称 | 描述 |
|---|---|---|
(?=pattern) |
正向先行断言 | 匹配后面是 pattern 的位置 |
(?!pattern) |
负向先行断言 | 匹配后面不是 pattern 的位置 |
(?<=pattern) |
正向后行断言 | 匹配前面是 pattern 的位置 |
(?<!pattern) |
负向后行断言 | 匹配前面不是 pattern 的位置 |
示例:
# 正向先行断言:匹配后面跟着 "元" 的数字
\d+(?=元)
输入:"100元" → 匹配 "100"
# 负向先行断言:匹配后面不是 "test" 的 "foo"
foo(?!test)
输入:"foobar" → 匹配 "foo"
输入:"footest" → 不匹配
# 正向后行断言:匹配前面是 "$" 的数字
(?<=\$)\d+
输入:"$100" → 匹配 "100"
# 负向后行断言:匹配前面不是 "un" 的 "happy"
(?<!un)happy
输入:"happy" → 匹配
输入:"unhappy" → 不匹配
贪婪与非贪婪匹配
默认情况下,量词是贪婪的,会尽可能多地匹配字符。在量词后加 ? 可以变成非贪婪(懒惰)匹配:
| 贪婪 | 非贪婪 | 描述 |
|---|---|---|
* |
*? |
匹配0次或多次,尽可能少 |
+ |
+? |
匹配1次或多次,尽可能少 |
? |
?? |
匹配0次或1次,尽可能少 |
{n,m} |
{n,m}? |
匹配n到m次,尽可能少 |
示例:
输入:"<div>hello</div><div>world</div>"
贪婪匹配:<div>.*</div>
结果:"<div>hello</div><div>world</div>"(匹配整个字符串)
非贪婪匹配:<div>.*?</div>
结果:"<div>hello</div>"(匹配第一个 div)
修饰符
修饰符(Flags)改变正则表达式的匹配行为:
| 修饰符 | 描述 |
|---|---|
i |
忽略大小写 |
g |
全局匹配(查找所有匹配) |
m |
多行模式(^ 和 $ 匹配每行的开头和结尾) |
s |
单行模式(. 匹配包括换行符在内的所有字符) |
u |
Unicode 模式 |
x |
扩展模式(忽略空白,允许注释) |
常用正则模式
邮箱验证
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
解析:
^- 字符串开头[a-zA-Z0-9._%+-]+- 用户名部分,包含字母、数字和特殊字符@- @ 符号[a-zA-Z0-9.-]+- 域名部分\.- 点号[a-zA-Z]{2,}- 顶级域名,至少2个字母$- 字符串结尾
测试用例:
- ✓
user@example.com - ✓
john.doe+tag@company.co.uk - ✗
invalid@ - ✗
@nodomain.com
手机号验证
中国大陆手机号:
^1[3-9]\d{9}$
解析:
^1- 以1开头[3-9]- 第二位是3-9\d{9}- 后面跟9位数字$- 字符串结尾
国际手机号(带国家代码):
^\+?[1-9]\d{1,14}$
URL验证
^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$
更完整的URL验证:
^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$
解析:
^(https?|ftp):\/\/- 协议部分[^\s/$.?#]- 域名首字符[^\s]*- 其余部分$- 字符串结尾
IP地址验证
IPv4地址:
^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$
解析:
25[0-5]- 匹配 250-2552[0-4]\d- 匹配 200-249[01]?\d\d?- 匹配 0-199\.- 点号分隔{3}- 前三组- 最后一组不需要点号
IPv6地址:
^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$
密码强度验证
至少8位,包含大小写字母和数字:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$
更强的密码(包含特殊字符):
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$
解析:
(?=.*[a-z])- 至少一个小写字母(?=.*[A-Z])- 至少一个大写字母(?=.*\d)- 至少一个数字(?=.*[@$!%*?&])- 至少一个特殊字符{8,}- 至少8个字符
身份证号验证
中国大陆18位身份证:
^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$
解析:
[1-9]\d{5}- 6位地区代码(19|20)\d{2}- 4位年份(1900-2099)(0[1-9]|1[0-2])- 2位月份(01-12)(0[1-9]|[12]\d|3[01])- 2位日期(01-31)\d{3}- 3位顺序码[\dXx]- 校验码(数字或X)
代码示例
JavaScript
// 基本匹配
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const email = "user@example.com";
console.log(emailRegex.test(email)); // true
// 使用 match 提取匹配
const text = "联系方式:13812345678 或 13987654321";
const phoneRegex = /1[3-9]\d{9}/g;
const phones = text.match(phoneRegex);
console.log(phones); // ["13812345678", "13987654321"]
// 使用捕获组
const urlRegex = /^(https?):\/\/([^\/]+)(\/.*)?$/;
const url = "https://example.com/path/to/page";
const match = url.match(urlRegex);
if (match) {
console.log("协议:", match[1]); // "https"
console.log("域名:", match[2]); // "example.com"
console.log("路径:", match[3]); // "/path/to/page"
}
// 使用命名捕获组
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const dateMatch = "2026-01-12".match(dateRegex);
console.log(dateMatch.groups.year); // "2026"
console.log(dateMatch.groups.month); // "01"
console.log(dateMatch.groups.day); // "12"
// 替换操作
const masked = "13812345678".replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
console.log(masked); // "138****5678"
// 使用 exec 进行迭代匹配
const regex = /\d+/g;
const str = "价格:100元,数量:50个";
let result;
while ((result = regex.exec(str)) !== null) {
console.log(`找到 ${result[0]},位置 ${result.index}`);
}
// 输出:
// 找到 100,位置 3
// 找到 50,位置 12
Python
import re
# 基本匹配
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
email = "user@example.com"
if re.match(email_pattern, email):
print("邮箱格式正确")
# 查找所有匹配
text = "联系方式:13812345678 或 13987654321"
phones = re.findall(r'1[3-9]\d{9}', text)
print(phones) # ['13812345678', '13987654321']
# 使用捕获组
url_pattern = r'^(https?):\/\/([^\/]+)(\/.*)?$'
url = "https://example.com/path/to/page"
match = re.match(url_pattern, url)
if match:
print(f"协议: {match.group(1)}") # https
print(f"域名: {match.group(2)}") # example.com
print(f"路径: {match.group(3)}") # /path/to/page
# 使用命名捕获组
date_pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
date_match = re.match(date_pattern, "2026-01-12")
if date_match:
print(date_match.group('year')) # 2026
print(date_match.group('month')) # 01
print(date_match.group('day')) # 12
# 替换操作
phone = "13812345678"
masked = re.sub(r'(\d{3})\d{4}(\d{4})', r'\1****\2', phone)
print(masked) # 138****5678
# 编译正则表达式(提高性能)
pattern = re.compile(r'\d+')
numbers = pattern.findall("价格:100元,数量:50个")
print(numbers) # ['100', '50']
# 使用 finditer 获取匹配对象
for match in re.finditer(r'\d+', "价格:100元,数量:50个"):
print(f"找到 {match.group()},位置 {match.start()}-{match.end()}")
Java
import java.util.regex.*;
import java.util.ArrayList;
import java.util.List;
public class RegexExample {
public static void main(String[] args) {
// 基本匹配
String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
String email = "user@example.com";
boolean isValid = email.matches(emailPattern);
System.out.println("邮箱有效: " + isValid);
// 查找所有匹配
String text = "联系方式:13812345678 或 13987654321";
Pattern phonePattern = Pattern.compile("1[3-9]\\d{9}");
Matcher matcher = phonePattern.matcher(text);
List<String> phones = new ArrayList<>();
while (matcher.find()) {
phones.add(matcher.group());
}
System.out.println(phones); // [13812345678, 13987654321]
// 使用捕获组
String urlPattern = "^(https?)://([^/]+)(/.*)?$";
String url = "https://example.com/path/to/page";
Pattern pattern = Pattern.compile(urlPattern);
Matcher urlMatcher = pattern.matcher(url);
if (urlMatcher.matches()) {
System.out.println("协议: " + urlMatcher.group(1)); // https
System.out.println("域名: " + urlMatcher.group(2)); // example.com
System.out.println("路径: " + urlMatcher.group(3)); // /path/to/page
}
// 使用命名捕获组(Java 7+)
String datePattern = "(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})";
Pattern dateRegex = Pattern.compile(datePattern);
Matcher dateMatcher = dateRegex.matcher("2026-01-12");
if (dateMatcher.matches()) {
System.out.println("年: " + dateMatcher.group("year"));
System.out.println("月: " + dateMatcher.group("month"));
System.out.println("日: " + dateMatcher.group("day"));
}
// 替换操作
String phone = "13812345678";
String masked = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
System.out.println(masked); // 138****5678
}
}
Go
package main
import (
"fmt"
"regexp"
)
func main() {
// 基本匹配
emailPattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
emailRegex := regexp.MustCompile(emailPattern)
email := "user@example.com"
fmt.Println("邮箱有效:", emailRegex.MatchString(email))
// 查找所有匹配
text := "联系方式:13812345678 或 13987654321"
phoneRegex := regexp.MustCompile(`1[3-9]\d{9}`)
phones := phoneRegex.FindAllString(text, -1)
fmt.Println(phones) // [13812345678 13987654321]
// 使用捕获组
urlPattern := `^(https?)://([^/]+)(/.*)?$`
urlRegex := regexp.MustCompile(urlPattern)
url := "https://example.com/path/to/page"
matches := urlRegex.FindStringSubmatch(url)
if len(matches) > 0 {
fmt.Println("协议:", matches[1]) // https
fmt.Println("域名:", matches[2]) // example.com
fmt.Println("路径:", matches[3]) // /path/to/page
}
// 使用命名捕获组
datePattern := `(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`
dateRegex := regexp.MustCompile(datePattern)
dateMatch := dateRegex.FindStringSubmatch("2026-01-12")
names := dateRegex.SubexpNames()
for i, name := range names {
if name != "" && i < len(dateMatch) {
fmt.Printf("%s: %s\n", name, dateMatch[i])
}
}
// 替换操作
phone := "13812345678"
replaceRegex := regexp.MustCompile(`(\d{3})\d{4}(\d{4})`)
masked := replaceRegex.ReplaceAllString(phone, "$1****$2")
fmt.Println(masked) // 138****5678
// 使用 ReplaceAllStringFunc 进行复杂替换
text2 := "价格100元"
numRegex := regexp.MustCompile(`\d+`)
result := numRegex.ReplaceAllStringFunc(text2, func(s string) string {
return "[" + s + "]"
})
fmt.Println(result) // 价格[100]元
}
正则表达式最佳实践
1. 保持简单
复杂的正则表达式难以维护和调试。如果可能,将复杂模式拆分成多个简单的正则表达式:
// 不推荐:一个复杂的正则
const complexRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// 推荐:多个简单的检查
function validatePassword(password) {
if (password.length < 8) return false;
if (!/[a-z]/.test(password)) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/\d/.test(password)) return false;
if (!/[@$!%*?&]/.test(password)) return false;
return true;
}
2. 使用非捕获组
如果不需要捕获匹配内容,使用非捕获组 (?:...) 可以提高性能:
// 捕获组(会保存匹配结果)
/(cat|dog) food/
// 非捕获组(不保存匹配结果,更高效)
/(?:cat|dog) food/
3. 避免灾难性回溯
某些正则表达式可能导致指数级的回溯,造成性能问题:
// 危险:可能导致灾难性回溯
/(a+)+$/
// 安全:使用原子组或更精确的模式
/a+$/
4. 预编译正则表达式
在循环中使用正则表达式时,应该预先编译:
import re
# 不推荐:每次循环都编译
for line in lines:
if re.match(r'\d+', line):
process(line)
# 推荐:预编译
pattern = re.compile(r'\d+')
for line in lines:
if pattern.match(line):
process(line)
5. 使用锚点
当知道匹配位置时,使用锚点可以提高性能:
// 不推荐:会搜索整个字符串
/hello/
// 推荐:如果知道在开头
/^hello/
6. 测试边界情况
在生产环境使用前,务必测试各种边界情况:
- 空字符串
- 超长字符串
- 特殊字符
- Unicode 字符
- 换行符
常见问题
正则表达式和通配符有什么区别?
通配符(如 * 和 ?)是简化的模式匹配,主要用于文件名匹配。正则表达式更强大,支持复杂的模式描述、捕获组、断言等高级功能。
| 特性 | 通配符 | 正则表达式 |
|---|---|---|
* |
匹配任意字符 | 匹配前一个字符0次或多次 |
? |
匹配单个字符 | 匹配前一个字符0次或1次 |
| 复杂度 | 简单 | 强大但复杂 |
| 使用场景 | 文件名匹配 | 文本处理、数据验证 |
如何调试复杂的正则表达式?
- 使用在线工具:如我们的正则表达式测试工具,可以实时查看匹配结果
- 分步构建:从简单模式开始,逐步添加复杂性
- 添加注释:使用扩展模式(x 修饰符)添加注释
- 使用可视化工具:将正则表达式转换为可视化图表
正则表达式的性能如何优化?
- 使用锚点限制搜索范围
- 避免不必要的捕获组
- 使用非贪婪匹配
- 预编译正则表达式
- 避免嵌套量词(如
(a+)+) - 使用更具体的字符类
不同编程语言的正则表达式有什么区别?
大多数编程语言使用相似的正则表达式语法(PCRE 风格),但有一些细微差别:
| 特性 | JavaScript | Python | Java | Go |
|---|---|---|---|---|
| 后行断言 | ✓ (ES2018+) | ✓ | ✓ | ✗ |
| 命名捕获组 | (?<name>) |
(?P<name>) |
(?<name>) |
(?P<name>) |
| Unicode 支持 | 需要 u 标志 | 默认支持 | 默认支持 | 默认支持 |
| 原子组 | ✗ | ✗ | ✓ | ✗ |
如何不写代码测试正则表达式?
你可以使用在线工具,如我们的免费正则表达式测试工具,无需编写任何代码即可:
- 实时测试正则表达式
- 查看匹配结果和捕获组
- 获取多种编程语言的代码示例
- 保存和分享你的正则表达式
总结
正则表达式是每个开发者都应该掌握的核心技能。虽然学习曲线可能有些陡峭,但一旦掌握,它将大大提高你的文本处理效率。
快速总结:
- 从基础语法开始:字符匹配、量词、锚点
- 掌握分组和捕获组的使用
- 了解零宽断言等高级特性
- 记住常用正则模式(邮箱、手机号、URL等)
- 注意性能优化和最佳实践
- 多练习,多测试
准备好测试你的正则表达式了吗?试试我们的免费在线工具: