正则表达式是 grep、sed、awk 共同的"底层语言"。前面三篇反复提到 BRE、ERE、PCRE,这一篇把它们放一起讲透:元字符怎么写、转义差异在哪、贪婪和非贪婪怎么回事,再给几个能直接抄的实战模式。正则这东西不靠背,靠对照和练。
一、三种方言先看清楚
Linux 上常见的正则方言有三种,搞混了写出来的命令要么不匹配,要么报语法错:
| 方言 | 全称 | 谁在用 | 特点 |
|---|---|---|---|
| BRE | Basic Regular Expression 基本正则 | 默认 grep、sed |
`+ ? |
| ERE | Extended Regular Expression 扩展正则 | grep -E、sed -E、awk |
元字符直接写,不支持 \d \w \s |
| PCRE | Perl Compatible Regular Expression | grep -P、rg、python -c、大多数现代语言 |
支持 \d \w \s \b、非贪婪、零宽断言 |
一个直观对比:匹配"一个或多个数字"。
grep "[0-9]\+" app.log # BRE:+ 要加反斜杠
grep -E "[0-9]+" app.log # ERE:直接写 +
grep -P "\d+" app.log # PCRE:\d 最简洁
同一件事三种写法,差别就在于方言。
二、基础元字符表
| 元字符 | 含义 | 示例 |
|---|---|---|
. |
任意一个字符(除换行) | a.c 匹配 abc、a1c |
^ |
行首 | ^error 匹配行首是 error |
$ |
行尾 | log$ 匹配以 log 结尾 |
* |
前面那一项出现 0 次或多次 | ab*c 匹配 ac、abc、abbc |
+ |
前面那一项出现 1 次或多次(ERE/PCRE) | ab+c 匹配 abc、abbc,不匹配 ac |
? |
前面那一项出现 0 次或 1 次(ERE/PCRE) | colou?r 匹配 color、colour |
[abc] |
字符类,匹配其中任意一个 | [aeiou] 匹配元音 |
[^abc] |
取反,匹配不在其中的字符 | [^0-9] 匹配非数字 |
[a-z] |
范围 | [A-Za-z] 匹配字母 |
(...) |
分组,捕获(ERE/PCRE 直接写;BRE 要写 \(...\)) |
(ab)+ 匹配 abab |
| |
或(ERE/PCRE) | cat|dog 匹配 cat 或 dog |
\ |
转义 | \. 匹配真实的点 |
三、量词:*、+、?、{m,n}
量词决定"前面那个东西重复几次"。
| 写法 | 含义 |
|---|---|
* |
0 次或多次 |
+ |
1 次或多次 |
? |
0 次或 1 次 |
{n} |
正好 n 次 |
{n,} |
至少 n 次 |
{n,m} |
n 到 m 次 |
BRE 下 + ? | () {} 全是字面字符,要写得加反斜杠:\+、\?、\|、\(\)、\{n,m\}。ERE 下直接写。
grep -E "error [0-9]{3,5}" app.log # ERE:直接写 {3,5}
grep "error [0-9]\{3,5\}" app.log # BRE:要加反斜杠
四、分组、捕获和反向引用
把一段模式用括号包起来就是一个捕获组。后面可以用 \1、\2 引用它匹配到的内容。
比如把 employees.csv 里工号和姓名互换:
sed -E 's/^([0-9]+),([^,]+),/\2,\1,/' employees.csv
([0-9]+) 是第 1 个分组,([^,]+) 是第 2 个分组。替换串里 \1 代表第一个分组匹配到的内容,\2 代表第二个。
只分组不捕获的写法是 (?:...),但这是 PCRE 语法,BRE/ERE 不支持。
五、BRE vs ERE 转义差异对照表
这张表是本章核心,建议收藏:
| 元字符 | BRE 写法 | ERE 写法 | 含义 |
|---|---|---|---|
| 或 | a|b |
a|b 或 a|b |
匹配 a 或 b |
| 分组 | \(ab\)+ |
(ab)+ |
把 ab 看成整体 |
| 一次或多次 | ab\+ |
ab+ |
b 重复 ≥1 次 |
| 0 次或 1 次 | ab\? |
ab? |
b 出现 0 或 1 次 |
| 重复 n 次 | a\{3\} |
a{3} |
a 正好 3 次 |
| 重复 n 到 m 次 | a\{2,4\} |
a{2,4} |
a 重复 2~4 次 |
| ` | ( ) + ? { }` |
字面字符 | 元字符 |
规律一句话:BRE 里的元字符在 ERE 里直接写;BRE 想把它们当元字符用,必须加反斜杠。
写脚本时最省事的做法:只要用到任何量词和括号,直接 grep -E 或 sed -E,从源头避开转义噩梦。
六、POSIX 字符类
在 [...] 里面还有一套 POSIX 定义的字符类,跨语言一致性比 [0-9]、[a-z] 更好:
| 写法 | 等价于 | 含义 |
|---|---|---|
[[:alnum:]] |
[A-Za-z0-9] |
字母数字 |
[[:alpha:]] |
[A-Za-z] |
字母 |
[[:digit:]] |
[0-9] |
数字 |
[[:lower:]] |
[a-z] |
小写字母 |
[[:upper:]] |
[A-Z] |
大写字母 |
[[:space:]] |
空白字符 | 空格、Tab、换行 |
[[:punct:]] |
标点符号 | --- |
[[:xdigit:]] |
[0-9a-fA-F] |
十六进制字符 |
注意它必须写在 [] 里:[[:digit:]] 是"任意数字",而 [:digit:] 单独写只是个普通字符列表,含义完全不同。这是新手高频坑。
grep -E "[[:digit:]]{4}" employees.csv # 匹配连续 4 位数字
七、PCRE 扩展:grep -P 才有的东西
ERE 已经够用了,但遇到复杂需求 PCRE 才舒服:
| 写法 | 含义 |
|---|---|
\d |
数字,等价 [0-9] |
\D |
非数字 |
\w |
单词字符,等价 [A-Za-z0-9_] |
\W |
非单词字符 |
\s |
空白(空格、Tab、换行) |
\S |
非空白 |
\b |
单词边界(不占字符,只是个位置) |
(?=...) |
正向前瞻:后面必须匹配 ... |
(?!...) |
负向前瞻:后面不能匹配 ... |
(?:...) |
非捕获分组 |
*?、+? |
非贪婪量词 |
示例:用 \b 做全词匹配
grep -P "\berror\b" app.log
这能匹配 error、error: 这种独立单词,但不会匹配 terror、errors。grep -w 也能做类似的事,\b 在脚本里更灵活。
八、贪婪与非贪婪原理
默认情况下,正则量词是贪婪的:能多吃就多吃。
拿一段文本:<div>hello</div><div>world</div>,模式是 <div>.*</div>。
-
贪婪匹配 :
.*从第一个<div>一直吃到最后一个</div>,结果是整段。 -
非贪婪匹配 :
.*?吃到第一个</div>就停,结果是<div>hello</div>。echo '
helloworld' | grep -oP '.*?'
预期输出:
<div>hello</div>
<div>world</div>
去掉 ?,结果会变成一整行。这个差别在提取 HTML、XML、JSON 片段时是天壤之别。
非贪婪只在 PCRE 里有,ERE 不支持。
九、实战案例
案例 1:提取 IPv4 地址
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log
这个模式能抓出所有形如 192.168.1.10 的字符串。它不验证 IP 合法性(999.999.999.999 也能匹配),但日志里够用。要严格合法 IP 得写更长的模式,日志场景没必要。
案例 2:从 Nginx 日志提取状态码和 URL
awk '{ status[$9]++ } END { for (s in status) print s, status[s] }' access.log
这其实是 awk 的活。正则单独做这事更脆,因为 Nginx 日志里 URL 里可能含引号和空格。
案例 3:邮箱匹配(够用版)
grep -oE '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' emails.txt
想做"严格符合 RFC 5322"的邮箱正则长得根本没法看,实际场景用这种宽松版就够。
案例 4:把日志里的手机号脱敏
sed -E 's/1[3-9][0-9]{9}/1**********/g' app.log
11 位手机号(第二位 3~9)全部替换成掩码。
案例 5:找代码里 TODO/FIXME
grep -rnE "(TODO|FIXME|XXX):" src/
十、⚠️ 常见错误
- 点号没转义 。匹配
192.168.1.1时写成grep "192.168.1.1",点号被当成"任意字符",把192x168y1z1也匹配出来。匹配真实的点永远写\.。 - 在 BRE 里写
+、?、|。结果要么匹配不到,要么把它们当字面字符搜。养成"用复杂正则就加-E"的习惯。 - 字符类写反 。
[0-9]是数字,^[0-9]是"行首是数字",[^0-9]是"非数字"。^在[]里面和外面含义完全不同。 - 贪婪模式翻车 。提取 HTML/JSON 时用
.*,结果一次吞掉整段。不确定时直接加?改成非贪婪。 - 把
\d写进grep。grep "\d+" file在大多数系统上匹配不到任何东西,因为 grep 默认 BRE 不认识\d。要写grep -P "\d+"。 - 多行匹配问题 。
.默认不匹配换行。要跨行匹配,PCRE 用(?s)打开"点匹配换行"模式,或者用[\s\S]代替.。
十一、知识扩展:什么时候该跳出 grep/sed 用 PCRE 工具
正则方言这事说到底是个工程取舍:
-
简单替换、找行 :
grep、sed默认 BRE 就够,别给自己加戏。 -
要分组、或、量词 :直接
grep -E、sed -E。 -
要
\d \w \b、非贪婪、零宽断言 :上grep -P,或者干脆用 Python 一行:python3 -c 'import re,sys; [print(m.group()) for line in sys.stdin for m in re.finditer(r"\b\d{1,3}(?:.\d{1,3}){3}\b", line)]' < access.log
-
搜代码库 :装个
rg(ripgrep),默认 PCRE2 开关,速度快 10 倍以上。 -
JSON/HTML/XML 解析 :别用正则。
jq、pup、xmllint这些专门的解析器比正则健壮一个数量级。用正则抠 HTML 的人,到后来都会回来用解析器。
正则是把利器,但它不是万能。知道什么时候该放下正则,比会写多复杂的正则更值钱。