正则表达式 20 个实用场景

从验证到提取,每个都能直接用在项目里

正则表达式是开发者手中的瑞士军刀。一行精妙的正则,可以替代几十行循环和条件判断。然而很多开发者只停留在"能看懂但不会写"的阶段。本文精选了 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 进行调试,可以看到详细的匹配步骤和回溯次数。

最后一条建议:能用字符串方法解决的就不用正则。简单的查找、替换、分割用 indexOfincludesstartsWith 等方法更清晰、更快。正则虽强大,但也是可读性的敌人,复杂正则务必写好注释。

五、进阶技巧

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'
// }

六、什么时候不该用正则

正则是强大的工具,但不是万能的。有些场景用正则反而更糟:

判断标准:如果写完正则后,你自己都需要盯着看五分钟才能确认它的意思,那很可能就不该用正则。正则的可读性和可维护性很差,团队项目中复杂正则务必写好注释,并配合单元测试。

用 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 里,它会详细解释每一部分的含义,还会显示匹配步骤和回溯次数,帮助你理解正则为什么匹配、为什么不匹配、为什么慢。