前言

正则表达式(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
1[3-9]\d{9}
1
2
匹配: 13800138000, 15912345678
不匹配: 12345678901 (第一位不是 1), 12000138000 (第二位不在 3-9)

场景 3:匹配邮箱

1
[\w.-]+@[\w.-]+\.\w+
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)) # 010-8080 (完整匹配)
print(match.group(1)) # 010 (第一个捕获组)

# 查找所有
phones = re.findall(r"1[3-9]\d{9}", text) # ['13800138000']

# 替换
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); // ['13800138000']
const masked = text.replace(regex, "***");

Shell

1
2
3
4
5
6
# grep
grep -E "ERROR|FATAL" app.log
grep -oE '1[3-9][0-9]{9}' data.txt # 只输出匹配部分

# sed
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*$

正则表达式就是一上手看着吓人,用了几次之后就发现它的规律其实很简单——就那几个元字符和量词反复排列组合。遇到复杂的匹配需求,分解成一段一段地写,比一口气写完再调试要快得多。