正则表达式入门:实用指南
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 上的 正则表达式测试器——实时匹配、高亮显示、支持常见标志,所有处理在浏览器中完成。