为什么正则表达式是文本处理的利器
正则表达式是处理文本的瑞士军刀。从表单验证到数据提取,从 URL 解析到日志分析,正则表达式无处不在。但正则也是出了名的难读难写——/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ 这种表达式让很多人望而却步。
这篇文章让你真正理解正则表达式,从基础到高级,学会写出可维护的正则。
一、正则表达式基础
1.1 创建正则表达式
// 字面量方式(推荐)
const regex1 = /hello/;
const regex2 = /hello/i; // i = 不区分大小写
const regex3 = /hello/g; // g = 全局匹配
const regex4 = /hello/gi; // 组合使用
// 构造函数方式(动态构建)
const pattern = 'hello';
const regex5 = new RegExp(pattern, 'gi');
// 需要转义的字符
const escaped = new RegExp('file/special\\.txt', 'g');
1.2 基础匹配
// 精确匹配
/hello/.test('hello world'); // true
// 元字符
. // 任意字符(换行除外)
\d // 数字 [0-9]
\D // 非数字 [^0-9]
\w // 单词字符 [a-zA-Z0-9_]
\W // 非单词字符
\s // 空白字符
\S // 非空白字符
// 边界
^ // 字符串开头
$ // 字符串结尾
\b // 单词边界
\B // 非单词边界
// 字符类
[abc] // 匹配 a、b 或 c
[^abc] // 匹配除 a、b、c 外的字符
[a-z] // 匹配 a 到 z
[0-9] // 匹配 0 到 9
二、捕获组
2.1 基本捕获组
// 圆括号创建捕获组
const date = '2024-04-08';
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const match = date.match(regex);
console.log(match[0]); // '2024-04-08' — 完整匹配
console.log(match[1]); // '2024' — 第一组
console.log(match[2]); // '04' — 第二组
console.log(match[3]); // '08' — 第三组
console.log(match.groups); // undefined — 未命名组
2.2 命名捕获组
// ES2018 引入命名捕获组
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-04-08'.match(regex);
console.log(match.groups.year); // '2024'
console.log(match.groups.month); // '04'
console.log(match.groups.day); // '08'
// 在 replace 中使用命名组
const formatted = '2024-04-08'.replace(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
'$<month>/$<day>/$<year>'
);
console.log(formatted); // '04/08/2024'
2.3 非捕获组
// (?:...) 不创建捕获组,更高效
const regex1 = /(\d{4})-(?:\d{2})-(?:\d{2})/; // 非捕获组
const match1 = '2024-04-08'.match(regex1);
console.log(match1[1]); // '2024' — 只有年份被捕获
// (?:...)? 可选非捕获组
/(?:https?)?(?::\/\/)?/.test('http://example.com'); // true
三、环视(Lookahead / Lookbehind)
3.1 前置断言(Lookahead)
// 正向前瞻 (?=...) — 后面跟着
const regex1 = /\d+(?=px)/;
console.log('100px'.match(regex1)); // '100'
console.log('100em'.match(regex1)); // null
// 负向前瞻 (?!...) — 后面不跟着
const regex2 = /\d+(?!px)/;
console.log('100em'.match(regex2)); // '100'
console.log('100px'.match(regex2)); // null
3.2 后置断言(Lookbehind)
// 正向后顾 (?<=...) — 前面是
const regex1 = /(?<=\$)\d+/;
console.log('$100'.match(regex1)); // '100'
console.log('€100'.match(regex1)); // null
// 负向后顾 (?<!...) — 前面不是
const regex2 = /(?<!\$)\d+/;
console.log('€100'.match(regex2)); // '100'
3.3 实际应用
// 密码验证:至少8位,必须包含数字和字母
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/;
// 提取汇率数值(跳过货币符号)
const extractAmount = /(?<=USD )\d+\.?\d*/;
'Price: USD 99.99'.match(extractAmount); // '99.99'
// 驼峰转蛇形
const toSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
toSnakeCase('helloWorld'); // 'hello_world'
四、贪婪 vs 非贪婪
4.1 贪婪匹配
// 默认贪婪:匹配尽可能多
'hello world'.match(/hello.*world/); // ['hello world']
// 数字匹配
'123abc456'.match(/\d+/g); // ['123', '456'] — 贪婪但不会跨域
4.2 非贪婪匹配
// 加 ? 变成非贪婪:匹配尽可能少
'hello world'.match(/hello.*?world/); // ['hello world']
// 提取 HTML 标签内容
'<div>content</div><span>more</span>'.match(/<(\w+)>.*?<\/\1>/g);
// ['<div>content</div>', '<span>more</span>']
// 常用场景:从 JSON 中提取值
const jsonStr = '{"name":"Alice","age":30}';
jsonStr.match(/"(\w+)":/g); // ['"name":', '"age":']
五、exec 与 lastIndex
5.1 exec 方法
// exec 返回详细匹配信息,支持捕获组
const regex = /(\w+)@(\w+)\.(\w+)/;
const result = regex.exec('[email protected]');
console.log(result[0]); // '[email protected]'
console.log(result[1]); // 'user'
console.log(result[2]); // 'example'
console.log(result[3]); // 'com'
console.log(result.index); // 0 — 匹配位置
console.log(result.input); // '[email protected]'
5.2 带 g 标志的 lastIndex
// 带 g 标志时,exec 会维护 lastIndex
const regex = /\w+/g;
const str = 'hello world';
console.log(regex.exec(str)); // ['hello', index: 0]
console.log(regex.lastIndex); // 5
console.log(regex.exec(str)); // ['world', index: 6]
console.log(regex.lastIndex); // 11
console.log(regex.exec(str)); // null — 结束
console.log(regex.lastIndex); // 0 — 重置
// 常用模式:循环匹配
const allMatches = [];
let match;
while ((match = regex.exec(str)) !== null) {
allMatches.push(match[0]);
}
六、实际场景
6.1 表单验证
// 邮箱
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// URL
const isUrl = /^https?:\/\/[\w\-]+(\.[\w\-]+)+[/#?]?.*$/;
// 手机号(中国)
const isPhoneCN = /^1[3-9]\d{9}$/;
// 身份证(中国)
const isIdCardCN = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
6.2 文本提取
// 提取 URL
const extractUrls = text => text.match(/https?:\/\/[^\s]+/g) || [];
// 提取 @mention
const extractMentions = text => text.match(/@\w+/g) || [];
// 提取 hashtag
const extractHashtags = text => text.match(/#[^\s#]+/g) || [];
// 提取 HTML 标签
const stripHtml = html => html.replace(/<[^>]+>/g, '');
// 提取数字
const extractNumbers = text => text.match(/-?\d+\.?\d*/g) || [];
6.3 字符串替换
// 清除多余空格
' hello world '.replace(/\s+/g, ' ').trim(); // 'hello world'
// 首字母大写
'hello world'.replace(/\b\w/g, c => c.toUpperCase()); // 'Hello World'
// 千分位分隔符
'1234567'.replace(/\B(?=(\d{3})+(?!\d))/g, ','); // '1,234,567'