前言
正则表达式(Regular Expression)是一套用来匹配和提取文本的语法。它看起来像乱码(\b\d{3}-\d{4}\b),但一旦理解几个核心概念,写起来其实很自然。
这篇文章不讲理论,只讲最常用的语法和实际场景。
一、字面匹配与元字符
最简单的正则就是直接匹配文字:
1 2
| 正则: hello 匹配: "hello world" 中的 "hello"
|
真正强大的是元字符——有特殊含义的符号:
| 元字符 |
含义 |
示例 |
. |
匹配任意单个字符(换行除外) |
h.t 匹配 hat, hot, h3t |
\d |
数字 |
\d{3} 匹配 123, 456 |
\w |
字母数字下划线 |
\w+ 匹配 hello, var_1 |
\s |
空白字符(空格、Tab、换行) |
a\sb 匹配 a b |
\D |
非数字 |
|
\W |
非字母数字 |
|
\S |
非空白 |
|
二、量词:控制匹配次数
| 量词 |
含义 |
示例 |
* |
0 次或多次 |
ab*c 匹配 ac, abc, abbc |
+ |
1 次或多次 |
ab+c 匹配 abc, abbc(不匹配 ac) |
? |
0 次或 1 次 |
colou?r 匹配 color, colour |
{n} |
恰好 n 次 |
\d{4} 匹配 2025 |
{n,} |
至少 n 次 |
\d{4,} 匹配 2025, 12345 |
{n,m} |
n 到 m 次 |
\d{2,4} 匹配 12, 123, 1234 |
三、贪婪匹配 vs 非贪婪匹配
默认是贪婪匹配(尽可能多匹配):
1 2 3
| 字符串: "<div>Hello</div>" 正则: <.*> 贪婪匹配结果: "<div>Hello</div>" -- 匹配了整个字符串
|
加 ? 变成非贪婪匹配(尽可能少匹配):
1 2
| 正则: <.*?> 非贪婪匹配结果: "<div>" -- 只匹配第一个标签
|
实际开发中,提取 HTML 标签内容、解析 JSON 片段等场景,非贪婪匹配用得非常频繁。
四、字符组:匹配一组字符中的任意一个
1 2 3 4 5 6
| [aeiou] 匹配 a, e, i, o, u 中的任意一个 [a-z] 匹配任意小写字母 [A-Z] 匹配任意大写字母 [0-9] 匹配任意数字(等同于 \d) [a-zA-Z0-9] 匹配任意字母或数字 [^abc] 匹配 a,b,c 之外的任意字符(取反)
|
常见组合:
1 2
| [0-9a-fA-F] 十六进制字符 [一-龥] 中文字符(Unicode 范围)
|
五、锚点:匹配位置而非字符
| 锚点 |
含义 |
示例 |
^ |
行首 |
^Error 匹配以 Error 开头的行 |
$ |
行尾 |
;$ 匹配以分号结尾的行 |
\b |
单词边界 |
\bcat\b 匹配 cat,不匹配 catch |
\B |
非单词边界 |
|
六、分组与捕获
() 有两个作用:分组(把一部分正则当整体)和捕获(提取匹配的子串)。
1 2 3 4 5 6 7
| # 分组 (abc)+ 匹配 abc, abcabc, abcabcabc
# 捕获(用括号包住想提取的部分) (\d{3})-(\d{4}) 在 "电话: 010-8080" 中 捕获组1: 010 捕获组2: 8080
|
非捕获分组(只想分组,不想捕获,性能更好):
1
| (?:abc)+ 和 (abc)+ 一样的分组效果,但不捕获
|
七、实际场景
场景 1:提取日志中的 IP 地址
1
| \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
|
1 2
| 日志: 192.168.1.100 - - [30/May/2025:14:00:00] "GET /api HTTP/1.1" 200 提取结果: 192.168.1.100
|
场景 2:匹配手机号
1 2
| 匹配: 13800138000, 15912345678 不匹配: 12345678901 (第一位不是 1), 12000138000 (第二位不在 3-9)
|
场景 3:匹配邮箱
1
| 匹配: july@example.com, user.name@mail.qq.com
|
场景 4:匹配 URL
1
| https?://[\w.-]+(?:/[\w./?%&=-]*)?
|
1 2
| 匹配: https://blog.iot2045.cn http://example.com/path?page=1
|
场景 5:提取 HTML 标签中的内容
1
| <a\s+href="([^"]+)"[^>]*>([^<]+)</a>
|
1 2 3
| HTML: <a href="/about" class="nav">关于我们</a> 捕获组1: /about (链接地址) 捕获组2: 关于我们 (链接文字)
|
场景 6:日志时间段提取
1
| sed -n '/14:00/,/15:00/p' /var/log/nginx/access.log
|
场景 7:代码中的 TODO/FIXME 注释
1
| (TODO|FIXME|HACK|XXX):?\s*(.*)
|
1 2 3
| 代码: // TODO: 这里需要加异常处理 捕获组1: TODO 捕获组2: 这里需要加异常处理
|
场景 8:密码复杂度校验
1
| ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,32}$
|
1 2
| 必须包含: 小写字母 + 大写字母 + 数字 + 特殊字符 长度: 8-32 位
|
这里的 (?=.*[a-z]) 叫正向先行断言——它要求”后面必须出现一个小写字母”,但并不消耗字符。四个断言同时满足,意味着密码中至少要各出现一次四类字符。
八、在各语言中使用
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import re
text = "电话: 010-8080, 手机: 13800138000"
match = re.search(r"(\d{3})-(\d{4})", text) if match: print(match.group(0)) print(match.group(1))
phones = re.findall(r"1[3-9]\d{9}", text)
new_text = re.sub(r"1[3-9]\d{9}", "***", text)
phone_pattern = re.compile(r"1[3-9]\d{9}") phone_pattern.findall(text)
|
JavaScript
1 2 3 4
| const text = "电话: 010-8080, 手机: 13800138000"; const regex = /1[3-9]\d{9}/g; const phones = text.match(regex); const masked = text.replace(regex, "***");
|
Shell
1 2 3 4 5 6
| grep -E "ERROR|FATAL" app.log grep -oE '1[3-9][0-9]{9}' data.txt
sed -E 's/([0-9]{3})[0-9]{4}([0-9]{4})/\1****\2/g'
|
Java
1 2 3 4 5 6 7
| import java.util.regex.*;
Pattern p = Pattern.compile("1[3-9]\\d{9}"); Matcher m = p.matcher("手机: 13800138000"); while (m.find()) { System.out.println(m.group()); }
|
九、常用正则速查
| 场景 |
正则 |
| 手机号 |
1[3-9]\d{9} |
| 邮箱 |
[\w.-]+@[\w.-]+\.\w+ |
| 身份证 18 位 |
\d{17}[\dXx] |
| URL |
https?://[\w.-]+(?:/[\w./?%=&-]+)? |
| IPv4 |
\d{1,3}(?:\.\d{1,3}){3} |
| 中文字符 |
[一-龥] |
| 空白行 |
^\s*$ |
| 日期 YYYY-MM-DD |
\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]) |
| 命名规则检查 |
^[a-zA-Z_]\w*$ |
正则表达式就是一上手看着吓人,用了几次之后就发现它的规律其实很简单——就那几个元字符和量词反复排列组合。遇到复杂的匹配需求,分解成一段一段地写,比一口气写完再调试要快得多。