正则表达式(Regular Expression,简称 Regex)是一种强大的文本模式匹配工具,几乎所有现代编程语言都支持它。无论是数据验证、文本搜索替换,还是日志分析,正则表达式都是开发者必备的核心技能。本指南将带你从零开始,全面掌握正则表达式。

目录

核心要点

  • 模式匹配:正则表达式是描述字符串模式的语言,用于搜索、匹配和操作文本。
  • 通用性:几乎所有编程语言都支持正则表达式,语法大体相同。
  • 强大功能:可以用简洁的表达式描述复杂的文本模式。
  • 性能考量:复杂的正则表达式可能导致性能问题,需要谨慎设计。
  • 可读性:正则表达式可能难以阅读,建议添加注释或拆分复杂模式。
  • 测试重要:在生产环境使用前,务必充分测试正则表达式。

想要快速测试你的正则表达式?试试我们的免费在线工具,支持实时匹配和多种编程语言语法。

立即测试正则表达式 - 免费在线正则测试工具

什么是正则表达式?

正则表达式(Regular Expression)是一种用于描述字符串模式的形式语言。它起源于1950年代的数学理论,由数学家 Stephen Cole Kleene 首次提出。如今,正则表达式已成为文本处理的标准工具,被广泛应用于:

  1. 数据验证:验证用户输入(邮箱、电话、密码等)
  2. 文本搜索:在大量文本中查找特定模式
  3. 文本替换:批量修改符合模式的文本
  4. 数据提取:从文本中提取结构化信息
  5. 日志分析:解析和分析日志文件

正则表达式的核心思想是用特殊字符和规则来描述一类字符串,而不是具体的某个字符串。

正则表达式基础语法

字符匹配

最基本的正则表达式就是普通字符,它们匹配自身:

模式 描述 示例
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-255
  • 2[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次
复杂度 简单 强大但复杂
使用场景 文件名匹配 文本处理、数据验证

如何调试复杂的正则表达式?

  1. 使用在线工具:如我们的正则表达式测试工具,可以实时查看匹配结果
  2. 分步构建:从简单模式开始,逐步添加复杂性
  3. 添加注释:使用扩展模式(x 修饰符)添加注释
  4. 使用可视化工具:将正则表达式转换为可视化图表

正则表达式的性能如何优化?

  1. 使用锚点限制搜索范围
  2. 避免不必要的捕获组
  3. 使用非贪婪匹配
  4. 预编译正则表达式
  5. 避免嵌套量词(如 (a+)+
  6. 使用更具体的字符类

不同编程语言的正则表达式有什么区别?

大多数编程语言使用相似的正则表达式语法(PCRE 风格),但有一些细微差别:

特性 JavaScript Python Java Go
后行断言 ✓ (ES2018+)
命名捕获组 (?<name>) (?P<name>) (?<name>) (?P<name>)
Unicode 支持 需要 u 标志 默认支持 默认支持 默认支持
原子组

如何不写代码测试正则表达式?

你可以使用在线工具,如我们的免费正则表达式测试工具,无需编写任何代码即可:

  • 实时测试正则表达式
  • 查看匹配结果和捕获组
  • 获取多种编程语言的代码示例
  • 保存和分享你的正则表达式

总结

正则表达式是每个开发者都应该掌握的核心技能。虽然学习曲线可能有些陡峭,但一旦掌握,它将大大提高你的文本处理效率。

快速总结:

  • 从基础语法开始:字符匹配、量词、锚点
  • 掌握分组和捕获组的使用
  • 了解零宽断言等高级特性
  • 记住常用正则模式(邮箱、手机号、URL等)
  • 注意性能优化和最佳实践
  • 多练习,多测试

准备好测试你的正则表达式了吗?试试我们的免费在线工具:

立即测试正则表达式 - 免费在线正则测试工具