正则表达式入门:实用指南

CodeKit
正则表达式教程

什么是正则表达式?

正则表达式(Regular Expression,简称 regex 或 regexp)是一种描述字符串模式的紧凑语法。它源于 1950 年代数学家 Stephen Kleene 的形式语言理论,如今几乎被所有编程语言和文本工具支持。

一个简单的例子:检查一个字符串是否是有效的邮箱格式。手动编写验证逻辑可能需要几十行代码,而正则表达式只需一行:

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
console.log(emailRegex.test('user@example.com')); // true
console.log(emailRegex.test('invalid-email'));    // false

正则表达式的威力在于模式匹配——用极少的代码描述复杂的文本结构。

基础语法

字符匹配

语法含义示例
abc字面量匹配abc 匹配 “abc”
.任意单个字符(除换行)a.c 匹配 “abc”
\d数字 [0-9]\d\d 匹配 “42”
\w单词字符 [A-Za-z0-9_]\w+ 匹配 “hello”
\s空白字符\s 匹配空格
\D非数字\D 匹配 “a”
\W非单词字符\W 匹配 ”@“
\S非空白字符\S 匹配 “x”

量词

量词指定前一个元素出现的次数。

语法含义示例
*0 次或多次ab*c 匹配 “ac”, “abc”
+1 次或多次ab+c 匹配 “abc”, “abbc”
?0 次或 1 次colou?r 匹配 “color”
{n}恰好 n 次\d{4} 匹配 “2024”
{n,}至少 n 次\d{2,} 匹配 “42”, “123”
{n,m}n 到 m 次\d{2,4} 匹配 “42”, “1234”

贪婪 vs 非贪婪:默认情况下量词是”贪婪”的,会匹配尽可能多的字符。加 ? 变为”非贪婪”(lazy)。

const html = '<div>hello</div><div>world</div>';

// 贪婪:匹配整个字符串
console.log(html.match(/<div>.*<\/div>/)[0]);
// "<div>hello</div><div>world</div>"

// 非贪婪:匹配到第一个 </div>
console.log(html.match(/<div>.*?<\/div>/)[0]);
// "<div>hello</div>"

字符类和锚点

// 字符类:匹配方括号中的任意一个字符
console.log(/[aeiou]/.test('hello')); // true

// 范围
console.log(/[a-z]/.test('A'));  // false
console.log(/[A-Za-z]/.test('A')); // true

// 取反
console.log(/[^0-9]/.test('a')); // true

// 锚点
console.log(/^hello/.test('hello world')); // true(开头)
console.log(/world$/.test('hello world')); // true(结尾)
console.log(/^hello$/.test('hello'));       // true(完全匹配)

分组和捕获

捕获组

用括号 () 创建捕获组,可以提取匹配的子串。

const date = '2024-06-15';
const match = date.match(/(\d{4})-(\d{2})-(\d{2})/);
console.log(match[1]); // "2024"(年)
console.log(match[2]); // "06"(月)
console.log(match[3]); // "15"(日)

// 命名捕获组(ES2018+)
const matchNamed = date.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);
console.log(matchNamed.groups.year);  // "2024"
console.log(matchNamed.groups.month); // "06"

非捕获组

(?:...) 创建不捕获的分组,性能略好。

// 捕获
'abc'.match(/(a)(b)(c)/); // ["abc", "a", "b", "c"]

// 非捕获
'abc'.match(/(?:a)(b)(c)/); // ["abc", "b", "c"]

反向引用

\1\2 引用之前的捕获组,用于匹配重复内容。

// 匹配重复的单词
console.log(/\b(\w+)\s+\1\b/.test('hello hello')); // true
console.log(/\b(\w+)\s+\1\b/.test('hello world')); // false

断言

断言检查某个位置前后是否满足条件,但不消耗字符

// 前瞻(lookahead):(?=...) 后面跟着
console.log(/\d+(?=元)/.exec('价格100元')); // ["100"]

// 否定前瞻:(?!...) 后面不跟
console.log(/\d+(?!元)/.exec('价格100美元')); // ["100"]

// 后顾(lookbehind,ES2018+):(?<=...) 前面是
console.log(/(?<=\$)\d+/.exec('$100')); // ["100"]

// 否定后顾:(?<!...) 前面不是
console.log(/(?<!\$)\d+/.exec('价格100')); // ["100"]

常用模式

邮箱

// 简单版本
const simpleEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

// 更严格版本(RFC 5322 简化)
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

URL

const urlRegex = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
console.log(urlRegex.test('https://example.com/path?q=1')); // true

电话号码

// 中国手机号
const phoneRegex = /^1[3-9]\d{9}$/;
console.log(phoneRegex.test('13812345678')); // true

// 国际格式
const intlPhone = /^\+?[\d\s-]{7,}$/;

IP 地址

// IPv4
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
// 更严格(每段 0-255)
const ipv4Strict = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;

性能优化

1. 避免灾难性回溯

嵌套量词(如 (a+)+)可能导致指数级回溯,使正则引擎卡死。

// 危险:对 "aaaaaaaaaaaaaaaaaaaa!" 会卡住
/(a+)+b/.test('aaaaaaaaaaaaaaaaaaaa!');

// 安全:使用原子组或占有量词(部分引擎支持)
/(a++)b/; // 占有量词

2. 预编译正则

在循环中复用编译后的正则对象。

// 不好:每次循环都创建新正则
items.forEach(item => /pattern/.test(item));

// 好:预编译
const regex = /pattern/;
items.forEach(item => regex.test(item));

3. 使用锚点

如果匹配整个字符串,用 ^$ 锚定,避免不必要的回溯。

4. 避免不必要的捕获

不需要提取子串时,用非捕获组 (?:...)

总结

正则表达式是强大的文本处理工具,但也是”双刃剑”——复杂的正则难以维护,性能问题难以调试。掌握基础语法和常用模式,足以应对 90% 的日常需求。对于复杂场景,考虑分步处理或使用专门的解析器。

需要测试正则表达式?试试 CodeKit 上的 正则表达式测试器——实时匹配、高亮显示、支持常见标志,所有处理在浏览器中完成。