正则表达式是开发者手中的瑞士军刀。一行精妙的正则,可以替代几十行循环和条件判断。然而很多开发者只停留在"能看懂但不会写"的阶段。本文精选了 20 个最实用的正则表达式场景,覆盖表单验证、文本提取、数据清洗等常见需求,每一个都附有详细说明和代码示例。
一、表单验证类
表单验证是正则表达式最经典的应用场景。以下是最常用的验证模式。
1. 邮箱地址验证
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
emailRegex.test('user@example.com'); // true
emailRegex.test('invalid-email'); // false
注意:真正完整的邮箱验证正则极其复杂(RFC 5322 标准有上百字符)。上面的版本覆盖了 99% 的真实场景,又不至于太臃肿。实际项目中,建议发送验证邮件来确认邮箱有效性,而不是依赖正则。
2. 中国手机号验证
const phoneRegex = /^1[3-9]\d{9}$/;
phoneRegex.test('13800138000'); // true
phoneRegex.test('12345678901'); // false
中国大陆手机号都是 1 开头,第二位 3-9,共 11 位。三大运营商号段会不断增加,所以第二位用范围匹配比列举具体号段更具前瞻性。
3. 身份证号验证
const idRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
idRegex.test('110101199003074518'); // true
这个正则只能验证格式,不能验证身份证号是否真实存在(校验码需要单独计算)。真正的身份证验证还需要计算第 18 位校验码。
4. 密码强度校验
// 至少8位,包含大小写字母和数字
const strongPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
// 至少8位,包含大小写字母、数字和特殊字符
const veryStrong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;
这里用到了正向先行断言 (?=...)——它检查某个位置后面是否匹配模式,但不消耗字符。多个断言可以叠加,实现"必须包含 X、必须包含 Y"的逻辑。
5. URL 验证
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?$/;
urlRegex.test('https://www.example.com/path'); // true
二、文本提取类
6. 提取 HTML 标签中的内容
// 提取所有 a 标签的 href
const html = '<a href="https://a.com">A</a><a href="https://b.com">B</a>';
const hrefs = [...html.matchAll(/<a[^>]+href="([^"]+)"[^>]*>/g)].map(m => m[1]);
// ['https://a.com', 'https://b.com']
注意:用正则解析 HTML 只适合简单场景。复杂 HTML 解析应该使用 DOM 解析器或 Cheerio 等库。
7. 提取 Markdown 标题
const md = '# 一级标题
## 二级标题
### 三级标题
正文内容';
const headings = [...md.matchAll(/^(#{1,6})\s+(.+)$/gm)].map(m => ({
level: m[1].length,
text: m[2]
}));
8. 提取中国邮政编码
const zipRegex = /[1-9]\d{5}(?!\d)/g;
const text = '北京市朝阳区 100020,上海市浦东新区 200120';
text.match(zipRegex); // ['100020', '200120']
9. 提取日期(YYYY-MM-DD)
const dateRegex = /\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])/g;
const text = '项目于 2024-03-15 启动,预计 2024-12-31 完成';
text.match(dateRegex); // ['2024-03-15', '2024-12-31']
10. 提取十六进制颜色值
const hexColorRegex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
const css = 'color: #fff; background: #4F46E5; border: #333;';
css.match(hexColorRegex); // ['#fff', '#4F46E5', '#333']
三、数据清洗与替换类
11. 去除字符串首尾空格
// 虽然有 trim(),但了解原理有好处
const str = ' hello world ';
str.replace(/^\s+|\s+$/g, ''); // 'hello world'
12. 手机号中间四位脱敏
const phone = '13800138000';
phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
// '138****8000'
13. 数字千分位格式化
const num = '1234567.89';
num.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
// '1,234,567.89'
这是一个经典的正则面试题。核心是用 \B 匹配非单词边界,用正向先行断言找到"后面有 3 的倍数个数字"的位置,然后插入逗号。
14. 驼峰命名转下划线
function camelToSnake(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}
camelToSnake('userNameAndAge'); // 'user_name_and_age'
15. 下划线转驼峰命名
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
}
snakeToCamel('user_name_and_age'); // 'userNameAndAge'
四、实用技巧类
16. 验证 IPv4 地址
const ipv4Regex = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
ipv4Regex.test('192.168.1.1'); // true
ipv4Regex.test('256.1.1.1'); // false
17. 匹配中文字符
const chineseRegex = /[一-龥]+/g;
const text = 'Hello世界,你好World';
text.match(chineseRegex); // ['世界', '你好']
18. 去除 HTML 标签
function stripHtml(html) {
return html.replace(/<[^>]+>/g, '');
}
stripHtml('<p>Hello <b>World</b></p>'); // 'Hello World'
19. 统计单词数
function countWords(str) {
return str.trim().split(/\s+/).length;
}
countWords('Hello world from regex'); // 4
20. 验证文件名与扩展名
// 提取文件名和扩展名
const fileRegex = /^(.+)\.([^.]+)$/;
const match = 'document.pdf'.match(fileRegex);
// match[1] = 'document', match[2] = 'pdf'
// 检查是否为图片格式
const isImage = /\.(jpe?g|png|gif|webp|svg)$/i.test(filename);
性能与调试注意事项
写正则时一定要注意性能问题。灾难性回溯(Catastrophic Backtracking)是正则的性能杀手——当正则中有嵌套的可选量词(如 (a+)+),而输入不匹配时,引擎会尝试指数级的回溯路径,导致 CPU 飙升甚至挂起。
避免方法:尽量用具体字符类替代点号、避免嵌套量词、使用非贪婪匹配要谨慎、对长字符串加锚点。推荐使用 regex101.com 进行调试,可以看到详细的匹配步骤和回溯次数。
最后一条建议:能用字符串方法解决的就不用正则。简单的查找、替换、分割用 indexOf、includes、startsWith 等方法更清晰、更快。正则虽强大,但也是可读性的敌人,复杂正则务必写好注释。
五、进阶技巧
21. 捕获组与命名捕获组
普通捕获组用数字索引($1, $2...),但组多了之后容易搞混。ES2018 引入了命名捕获组,给每个组起个名字,代码可读性大大提升。
// 传统捕获组
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = '2024-08-15'.match(dateRegex);
console.log(match[1], match[2], match[3]); // 2024 08 15
// 命名捕获组
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-08-15'.match(dateRegex);
console.log(match.groups.year); // 2024
console.log(match.groups.month); // 08
console.log(match.groups.day); // 15
// 解构赋值
const { year, month, day } = match.groups;
22. 反向引用
在正则中引用前面匹配到的内容,比如匹配重复的单词:
// 匹配重复的单词(the the)
const repeatRegex = /(\w+)\s+/gi;
const text = 'The the quick brown fox fox jumps';
text.replace(repeatRegex, '$1');
// 'The quick brown fox jumps'
// 匹配匹配的 HTML 标签(简化版)
const tagRegex = /<(\w+)[^>]*>.*?<\/>/g;
23. 正向后行断言与负向后行断言
ES2018 引入了后行断言,检查某个位置前面是否(不)匹配某个模式。
// 正向后行断言:前面是 $ 的数字
const priceRegex = /(?<=\$)\d+/;
'Price: $100'.match(priceRegex); // ['100']
// 负向后行断言:前面不是 # 的数字
const numRegex = /(?<!#)\d+/;
'#100 and 200'.match(numRegex); // ['200']
24. 正则的 flags(修饰符)
/pattern/g // global:全局匹配,找到所有匹配
/pattern/i // ignoreCase:不区分大小写
/pattern/m // multiline:多行模式,^ 和 $ 匹配每行的开头结尾
/pattern/s // dotAll:让 . 匹配换行符
/pattern/u // unicode:启用完整 Unicode 支持
/pattern/y // sticky:粘性匹配,从 lastIndex 位置开始匹配
25. 用正则解析 URL
function parseUrl(url) {
const regex = /^(?<protocol>https?):\/\/(?<domain>[^\/?#]+)(?<path>\/[^?#]*)?(?<query>\?[^#]*)?(?<hash>#.*)?$/;
const match = url.match(regex);
if (!match) return null;
const groups = match.groups;
return {
protocol: groups.protocol,
domain: groups.domain,
path: groups.path || '/',
query: groups.query ? groups.query.slice(1) : '',
hash: groups.hash ? groups.hash.slice(1) : ''
};
}
parseUrl('https://example.com/path/to/page?foo=bar#section');
// {
// protocol: 'https',
// domain: 'example.com',
// path: '/path/to/page',
// query: 'foo=bar',
// hash: 'section'
// }
六、什么时候不该用正则
正则是强大的工具,但不是万能的。有些场景用正则反而更糟:
- 解析 HTML/XML:HTML 不是正则语言,用 DOM 解析器或 Cheerio
- 解析 JSON:用 JSON.parse,别自己写正则
- 复杂的日期解析:用 date-fns 或 dayjs 等专门的库
- 邮箱的完整验证:RFC 5322 的完整正则长达几页,发送验证邮件更靠谱
- 密码强度检测:多条简单规则比一条巨长的正则更易读和维护
判断标准:如果写完正则后,你自己都需要盯着看五分钟才能确认它的意思,那很可能就不该用正则。正则的可读性和可维护性很差,团队项目中复杂正则务必写好注释,并配合单元测试。
用 DevToolHub 的 正则表达式测试工具可以快速验证你的正则表达式,实时查看匹配结果和分组信息,是写正则时的好帮手。
七、正则调试技巧
正则表达式写起来容易调起来难。分享一些调试的经验和技巧。
分步构建法
不要试图一次写出完整的正则。从最简单的部分开始,逐步添加复杂的匹配条件,每一步都测试一下。比如写邮箱验证,先匹配 @ 前面的部分,再加上 @ 和域名,最后加上顶级域名。
注释正则
复杂的正则一定要写注释。JavaScript 中可以用字符串拼接的方式为正则添加注释,或者使用 x 修饰符(需要通过 RegExp 构造函数开启)。
// 把长正则拆成多行并添加注释
const phoneRegex = new RegExp(
'^' + // 字符串开头
'1' + // 以 1 开头
'[3-9]' + // 第二位为 3-9
'\d{9}' + // 后面 9 位数字
'$', // 字符串结尾
'g'
);
// 或者直接写行内注释
// 匹配 IPv4 地址
const ipv4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
// 第一段范围 第二段范围 第三段范围 最后一段范围
用测试用例验证
写正则的同时写一组测试用例,包括应该匹配的、不应该匹配的、边界情况。这样修改正则时可以运行测试,确保不会引入回归。
// 手机号正则测试用例
const tests = [
['13800138000', true], // 正常手机号
['13912345678', true], // 正常手机号
['12345678901', false], // 第二位不是 3-9
['1380013800', false], // 长度不够
['138001380000', false], // 长度太长
[' 13800138000', false], // 前面有空格
['abcdefghijk', false], // 全是字母
['', false], // 空字符串
];
const regex = /^1[3-9]\d{9}$/;
const allPass = tests.every(([input, expected]) => {
const result = regex.test(input);
const pass = result === expected;
if (!pass) console.log(`FAIL: "${input}" expected ${expected}, got ${result}`);
return pass;
});
console.log(allPass ? 'All tests passed!' : 'Some tests failed!');
可视化调试工具
工欲善其事,必先利其器。推荐几个好用的正则调试工具:
- regex101.com:最流行的正则测试工具,支持多种语言
- regexper.com:把正则可视化为铁路图,一目了然
- DevToolHub 正则测试器:在线测试正则,支持实时匹配和分组查看
- RegExLib.com:正则表达式库,很多常用正则可以直接搜
写正则遇到困难时,先把正则贴到 regex101 里,它会详细解释每一部分的含义,还会显示匹配步骤和回溯次数,帮助你理解正则为什么匹配、为什么不匹配、为什么慢。