正则表达式完全教程:从入门到精通
这两年我带过几个刚入行的同事,发现一个挺有意思的现象:他们不是写不出正则,而是看不懂别人写的正则。给他们一段 /^1[3-9]\d{9}$/,半天反应不过来这是手机号校验。我自己当年也一样——最早接触正则时,我把那些符号当天书背,\d、\w、+、* 死记硬背,结果第二周全忘光,遇到稍微长一点的表达式就两眼一抹黑。
后来才想明白,正则真正难的地方不在"写",在"读"。你能把一串符号从左到右拆开、知道每一段在干嘛,写就是水到渠成的事。所以这篇我换个思路:不堆语法清单,而是带你从零学会"读"正则——把它拆成字符类、量词、分组、断言几块,一块一块吃透。我自己就是靠这套拆解法,才算真正把正则从"玄学"变成了"工具"。
目录
什么是正则表达式
正则表达式是一种描述字符串模式的语言。它可以用来:
- 验证数据格式:检查邮箱、手机号、身份证号等是否符合规范
- 搜索文本:在大量文本中快速找到符合特定模式的内容
- 替换字符串:批量修改文本中的特定部分
- 提取信息:从非结构化文本中提取结构化数据
实际应用场景
// 验证邮箱格式
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(emailRegex.test('user@example.com')); // true
// 提取URL中的域名
const urlRegex = /https?:\/\/([^\/]+)/;
const match = 'https://www.example.com/path'.match(urlRegex);
console.log(match[1]); // www.example.com
// 替换敏感词
const text = '请联系我,电话:13812345678';
const maskedText = text.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
console.log(maskedText); // 请联系我,电话:138****5678
先有张地图:四块拆解速查
我带新人时一定先甩这张表给他们。正则看着千奇百怪,拆开其实就四类东西:字符类(匹配什么)、量词(匹配几次)、分组(怎么组合)、断言(什么位置)。读任何一段正则,无非就是把它切成这四块,逐段翻译成人话。后面遇到看不懂的表达式,回来对一眼就行。
| 类别 | 常见写法 | 一句话理解 | 新人最容易混的点 |
|---|---|---|---|
| 字符类 | \d \w \s [a-z] [^abc] | 匹配"哪些字符" | \w 是带下划线的,. 在 [] 里就是普通的点,不用转义 |
| 量词 | * + ? {n} {n,m} | 匹配"重复几次" | * 是 0 次起,+ 是 1 次起;? 跟在量词后变"懒惰" |
| 分组 | ( ) (?: ) (?<name> ) \1 | 把一段"打包/捕获" | (?:) 只分组不捕获;\1 是反向引用,不是第一个分组本身 |
| 断言 | ^ $ \b (?= ) (?<= ) | 匹配"位置"而非字符 | 前瞻 (?=) 不消耗字符,匹配完位置还停在原地 |
读正则的顺序我一般是:先找锚点(^、$)确定范围,再看里面被量词修饰的字符类一段段拆,遇到括号就当一个整体先跳过、回头再展开。习惯了之后,再长的表达式也能一眼看出骨架。
基础语法
1. 普通字符
最简单的正则表达式就是普通字符本身:
const regex = /hello/;
console.log(regex.test('hello world')); // true
console.log(regex.test('Hello world')); // false (大小写敏感)
2. 元字符
正则表达式的强大之处在于元字符(Metacharacters):
| 元字符 | 说明 | 示例 |
|---|---|---|
. | 匹配任意单个字符(除换行符) | a.c 匹配 "abc", "adc" 等 |
^ | 匹配字符串开始 | ^hello 只匹配以 "hello" 开头的字符串 |
$ | 匹配字符串结束 | world$ 只匹配以 "world" 结尾的字符串 |
* | 匹配前面的字符0次或多次 | ab*c 匹配 "ac", "abc", "abbc" 等 |
+ | 匹配前面的字符1次或多次 | ab+c 匹配 "abc", "abbc" 但不匹配 "ac" |
? | 匹配前面的字符0次或1次 | ab?c 匹配 "ac" 或 "abc" |
\d | 匹配数字 [0-9] | \d{3} 匹配三位数字 |
\w | 匹配字母数字下划线 [a-zA-Z0-9_] | \w+ 匹配一个或多个单词字符 |
\s | 匹配空白字符(空格、制表符、换行等) | \s+ 匹配一个或多个空白字符 |
3. 字符类
使用方括号 [] 定义字符类:
// 匹配元音字母
const vowelRegex = /[aeiou]/;
console.log(vowelRegex.test('hello')); // true
// 匹配数字
const digitRegex = /[0-9]/;
console.log(digitRegex.test('abc123')); // true
// 匹配不是数字的字符
const nonDigitRegex = /[^0-9]/;
console.log(nonDigitRegex.test('123')); // false
4. 量词
控制匹配次数:
// {n}: 精确匹配 n 次
const phoneRegex = /\d{11}/;
console.log(phoneRegex.test('13812345678')); // true
// {n,m}: 匹配 n 到 m 次
const usernameRegex = /^[a-zA-Z0-9]{3,16}$/;
console.log(usernameRegex.test('user123')); // true
// {n,}: 至少匹配 n 次
const passwordRegex = /.{8,}/;
console.log(passwordRegex.test('pass123')); // false(少于8个字符)
元字符详解
贪婪匹配 vs 非贪婪匹配
默认情况下,量词是贪婪的,会尽可能多地匹配字符:
const html = '<div>content</div>';
// 贪婪匹配:匹配到最后一个 >
const greedyRegex = /<.*>/;
console.log(html.match(greedyRegex)[0]); // <div>content</div>
// 非贪婪匹配:在量词后加 ?,尽可能少地匹配
const lazyRegex = /<.*?>/;
console.log(html.match(lazyRegex)[0]); // <div>
分组与捕获
使用圆括号 () 进行分组,并捕获匹配的内容:
// 捕获日期中的年月日
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = '2024-01-15'.match(dateRegex);
console.log(match[1]); // 2024
console.log(match[2]); // 01
console.log(match[3]); // 15
// 非捕获分组:(?:...)
const urlRegex = /https?:\/\/(?:www\.)?([^\/]+)/;
const url = 'https://www.example.com/path';
const urlMatch = url.match(urlRegex);
console.log(urlMatch[1]); // example.com(不包含 www.)
前瞻和后顾
高级匹配技巧:
// 正向前瞻 (?=...):匹配后面跟着特定模式的位置
const regex1 = /\d+(?=px)/;
console.log('width: 100px'.match(regex1)[0]); // 100
// 负向前瞻 (?!...):匹配后面不跟特定模式的位置
const regex2 = /\d+(?!px)/;
console.log('width: 100em'.match(regex2)[0]); // 100
// 正向后顾 (?<=...):匹配前面是特定模式的位置
const regex3 = /(?<=\$)\d+/;
console.log('price: $100'.match(regex3)[0]); // 100
// 负向后顾 (?<!...):匹配前面不是特定模式的位置
const regex4 = /(?<!\$)\d+/;
console.log('count: 100'.match(regex4)[0]); // 100
实战案例
案例1:验证表单输入
// 手机号验证(中国大陆)
function validatePhone(phone) {
const regex = /^1[3-9]\d{9}$/;
return regex.test(phone);
}
// 身份证号验证
function validateIDCard(id) {
const regex = /(^\d{15}$)|(^\d{17}(\d|X|x)$)/;
return regex.test(id);
}
// 密码强度验证(至少8位,包含大小写字母和数字)
function validatePassword(password) {
const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{8,}$/;
return regex.test(password);
}
// 测试
console.log(validatePhone('13812345678')); // true
console.log(validateIDCard('110101199001011234')); // true
console.log(validatePassword('Abc12345')); // true
console.log(validatePassword('weak')); // false
案例2:提取HTML标签内容
function extractHTMLContent(html) {
// 提取所有标签的内容(不包括标签本身)
const regex = /<[^>]+>([^<]+)<\/[^>]+>/g;
const matches = [];
let match;
while ((match = regex.exec(html)) !== null) {
matches.push(match[1]);
}
return matches;
}
const html = `
<div>Title</div>
<p>Content here</p>
<span>Footer</span>
`;
console.log(extractHTMLContent(html));
// ['Title', 'Content here', 'Footer']
案例3:格式化货币
function formatCurrency(number) {
// 添加千位分隔符
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
console.log(formatCurrency(1234567.89)); // 1,234,567.89
console.log(formatCurrency(1000)); // 1,000
案例4:解析URL参数
function parseQueryString(url) {
const regex = /[?&]([^=&]+)=([^&]*)/g;
const params = {};
let match;
while ((match = regex.exec(url)) !== null) {
params[match[1]] = decodeURIComponent(match[2]);
}
return params;
}
const url = 'https://example.com?name=John&age=30&city=Beijing';
console.log(parseQueryString(url));
// { name: 'John', age: '30', city: 'Beijing' }
高级技巧
1. 命名捕获组(ES2018+)
const dateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-01-15'.match(dateRegex);
console.log(match.groups.year); // 2024
console.log(match.groups.month); // 01
console.log(match.groups.day); // 15
2. Unicode属性转义(ES2018+)
// 匹配所有中文字符
const chineseRegex = /\p{Script=Han}+/u;
console.log(chineseRegex.test('你好世界')); // true
// 匹配所有表情符号
const emojiRegex = /\p{Emoji}+/u;
console.log(emojiRegex.test('😀😃😄')); // true
3. 替换回调函数
const text = 'hello world, hello javascript';
const result = text.replace(/hello/g, (match, offset) => {
console.log(`Found "${match}" at position ${offset}`);
return 'hi';
});
console.log(result);
// Found "hello" at position 0
// Found "hello" at position 13
// hi world, hi javascript
性能优化
1. 避免回溯灾难
// ❌ 糟糕:可能导致灾难性回溯
const badRegex = /(a+)+b/;
// 对于没有 b 的长字符串 'aaaa...aaa',会尝试所有可能的组合
// ✅ 优化:使用原子组或占有量词
const goodRegex = /a+b/;
2. 使用非捕获组
// ❌ 不必要的捕获
const regex1 = /(\d{4})-(\d{2})-(\d{2})/;
// ✅ 只捕获需要的部分
const regex2 = /\d{4}-(\d{2})-\d{2}/; // 只捕获月份
3. 预编译正则表达式
// ❌ 每次都重新编译
function validate(text) {
return /\d{3}-\d{4}/.test(text);
}
// ✅ 预编译,性能更好
const phoneRegex = /\d{3}-\d{4}/;
function validate(text) {
return phoneRegex.test(text);
}
常见陷阱
1. 忘记转义特殊字符
// ❌ 错误:. 会匹配任意字符
const wrong = /www.example.com/;
console.log(wrong.test('wwwXexample.com')); // true(不是我们想要的)
// ✅ 正确:转义 .
const correct = /www\.example\.com/;
console.log(correct.test('wwwXexample.com')); // false
2. 全局匹配的状态问题
const regex = /test/g;
console.log(regex.test('test')); // true
console.log(regex.test('test')); // false(lastIndex 已经改变)
// 解决方案:重置 lastIndex 或每次创建新的 RegExp
regex.lastIndex = 0;
console.log(regex.test('test')); // true
3. 多行模式的误解
const text = `line1
line2
line3`;
// 默认情况下,^ 和 $ 只匹配整个字符串的开始和结束
const regex1 = /^line/;
console.log(text.match(regex1)); // ['line'] (只匹配第一行)
// 使用 m 标志,^ 和 $ 匹配每一行的开始和结束
const regex2 = /^line/gm;
console.log(text.match(regex2)); // ['line', 'line', 'line']
写在最后
教了这么多新人,我最想说的一句话是:正则别背。
我见过有人抱着一本"正则速查手册"啃了一礼拜,结果还是写不出能用的表达式。真正管用的是理解,而理解的核心就这三块——字符类、量词、分组。把这三块吃透,日常碰到的需求九成以上你都能从零拼出来。断言、命名捕获那些是锦上添花,等你需要的时候再查也来得及。那张拆解速查表我建议你存下来,遇到读不懂的正则就对着切,切顺手了自然就会写了。
另一个习惯也想安利给你:写完一定上工具测,别凭脑子想当然。我自己写正则从来不相信"看起来对",哪怕是 \d{11} 这种简单的,也会随手贴几个真实样本跑一遍——边界值、空字符串、多匹配几个,往往就是在这一步抓出 bug 的。我们自己做了个 在线正则测试器,纯浏览器本地跑,输入的内容不会上传服务器,左边写表达式右边实时高亮匹配结果,调试的时候比脑补靠谱太多。
剩下的就是熟能生巧了。真没什么捷径,多写几个真实需求,那些符号慢慢就从天书变成肌肉记忆。加油。
推荐资源
- MDN 正则表达式文档
- Regex101 - 在线正则测试工具
- RegExr - 可视化正则表达式学习工具
相关工具
关键词: 正则表达式, RegEx, 模式匹配, 文本处理, JavaScript正则, 数据验证
更新时间: 2026-01-04
我们是一支开发者小团队,负责构建和维护 ToolsForge —— 150+ 个完全在浏览器本地运行、隐私优先的工具。这些文章来自我们日常的工程实践,而不是纸上谈兵。
了解我们