正则表达式
这一课你将能做到
- 看懂并手写 BRE 与 ERE 两种风格的正则,说出 grep 默认用哪种、-E 切换到哪种
- 用正则从日志中提取 IP、时间戳、状态码,校验手机号、IP 格式
- 把正则用进 sed -E 批量替换、awk 条件过滤、grep -o 精准摘取三类实战场景
为什么要学
日志和配置都是文本,而「按样子找内容」就是正则的主场。排查 502 要按时间窗过滤、统计异常要按模式计数、脱敏日志要批量替换手机号——没有正则,这些活全得靠人眼。Java 后端也逃不开它:String.matches、Pattern 类、Nginx location 匹配、IDE 全局替换,底层同一套语法。面试聊到「怎么从日志里捞特定模式的行」,正则是绕不过的基本功。
人话:正则是「文本的通配符进阶版」。通配符 *.log 只会匹配文件名,正则能描述「一个以 2026 开头、跟着日期、中间出现 ERROR 的行」这种复杂形状——它是描述文本形状的语言。
先建立图像
两套方言对照表(BRE = grep 默认,ERE = grep -E / sed -E / awk):
| 功能 | BRE(基础) | ERE(扩展) | 含义 |
|---|
| 任意单字符 | . | . | 除换行外任意字符 |
| 零或多次 | * | * | 重复前一项 ≥0 次 |
| 一或多次 | + | + | 重复 ≥1 次 |
| 零或一次 | ? | ? | 可有可无 |
| 或 | | | | | 左边或右边 |
| 分组 | ( ) | ( ) | 括起来当整体 |
| n 到 m 次 | {n,m} | {n,m} | 精确控制次数 |
| 行首/行尾 | ^ $ | ^ $ | 锚定位置 |
结论:ERE 少打一半转义符。日常一律 grep -E,BRE 只在维护老脚本时能读懂即可。
元字符三兄弟的直觉:
^ 行首锚定 ^ERROR → 只匹配行首的 ERROR
$ 行尾锚定 done$ → 只匹配行尾的 done
. 任意一个字符 a.c → abc aXc a0c 都行
* 前面重复0+ ab*c → ac abc abbc
+ 前面重复1+ ab+c → abc abbc(不含 ac)
? 前面0或1次 ab?c → ac 或 abc
[0-9] 集合任选 [0-9]{3} → 恰好三位数字
[^0-9] 集合取反 非 数字字符
跟着做
步骤 1:准备练习语料
mkdir -p ~/re-lab && cd ~/re-lab
cat > demo.log << 'EOF'
2026-10-06 09:00:01 INFO request from 192.168.1.10 cost 12ms
2026-10-06 09:00:05 ERROR db connect timeout from 192.168.1.11
2026-10-06 09:00:09 WARN slow query 3000ms
2026-10-06 09:01:12 INFO request from 10.0.0.5 cost 8ms
2026-10-06 09:02:33 ERROR null pointer at OrderService.java:88
EOF
步骤 2:锚定与集合
grep -E "^2026" demo.log # 以日期开头的行(全部)
grep -E "java:88$" demo.log # 以 java:88 结尾的行
grep -E "^2026-10-06 09:0[01]" demo.log # 09:00 或 09:01 分的行
grep -E "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" demo.log # IPv4 形状
. 要匹配「点本身」必须转义成 \.,否则它匹配任意字符——不转义时 192.168.1.10 连 192x168 1y10 都能中。
步骤 3:分组、或、引用
grep -E "ERROR|WARN" demo.log # 错误或警告
grep -E "(timeout|pointer)" demo.log # 分组内二选一
grep -E "cost [0-9]+ms" demo.log # cost 后跟数字加 ms
# 用分组提取内容:sed 的 \1 引用第 1 个分组
sed -E 's/.*from ([0-9.]+).*/IP: \1/' demo.log
# 输出 IP: 192.168.1.10 等行——从行里抠出 IP
分组引用 分组引用 \1 \2 是 sed 替换的灵魂:把想要的片段括起来,替换时按序号摆位置。
步骤 4:grep -o 摘取片段 + 计数
grep -oE "ERROR|WARN" demo.log | sort | uniq -c
# 1 WARN
# 2 ERROR ← 不用看整行,直接数模式出现次数
grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" demo.log # 摘出所有 IP
([0-9]{1,3}\.){3} 是分组 + 次数的组合拳:「三位数加点」重复 3 次,再接一段三位数。这就是面试让你现场写 IP 正则的答案骨架(教学简化版,严格校验需排除 256+,生产够用)。
步骤 5:sed -E 实战:脱敏与格式转换
# 日志脱敏:手机号打码
echo "user 13812345678 login" | sed -E 's/(1[3-9])[0-9]{9}/\1*********/'
# 输出:user 13********* login
# 逗号分隔转制表符
echo "a,b,c" | sed -E 's/,/\t/g'
# 时间格式统一:2026-10-06 → 2026/10/06
sed -E 's/2026-([0-9]{2})-([0-9]{2})/2026\/\1\/\2/' demo.log
生产红线:对正式文件 sed -i 之前,先不带 -i 跑一遍看输出,确认无误伤再落盘。
步骤 6:awk 按正则选行选列
awk '$0 ~ /ERROR/' demo.log # 整行匹配正则(~ 是匹配运算符)
awk '$0 !~ /INFO/' demo.log # 不匹配 INFO 的行
awk '/ERROR/ {print $NF}' demo.log # 含 ERROR 的行的最后一列
awk -F: '/OrderService/ {print $2}' demo.log # 冒号分隔取第 2 列
$0 ~ /正则/ 把正则当条件、把列当输出对象,awk 是「正则 + 字段处理」的二合一工具。
步骤 7:校验类正则(面试手写高频)
echo "13812345678" | grep -qE "^1[3-9][0-9]{9}$" && echo "手机号合法"
echo "10.0.0.5" | grep -qE "^([0-9]{1,3}\.){3}[0-9]{1,3}$" && echo "IP 格式合法"
echo "2026-10-06" | grep -qE "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" && echo "日期格式合法"
^ 和 $ 锚定首尾做「全匹配」,少一个都可能让 xx13812345678yy 混进来。grep -q 静默模式只返回退出码,专配 if 使用。
讲清楚要点
- grep 默认 BRE,
-E 开 ERE,-o 只输出匹配片段:-o 把「整行命中」变成「片段摘取」,配 sort/uniq 就是模式计数器。-P(PCRE,部分系统需安装)才有懒惰匹配等高级功能,优先用 ERE 解决。
- 正则是贪婪的:
.*from 会一直吃到行内最后一个 from,不是第一个。想停在第一个匹配,用排除法:[^ ]*from 或缩小集合。Java 正则里的 ?(懒惰量词)在 ERE 里没有对应物。
.、*、[、] 等元字符要匹配字面量必须转义:日志里最常见的误伤是拿 . 匹配 IP 和文件名。匹配「字面点」永远写 \.。
- 锚定符 ^ $ 让匹配从「包含」升级为「全等」:校验类场景(手机号、日期)必加;过滤类场景(捞 ERROR 行)反而常常不需要。
- sed 分组引用从 \1 开始按左括号顺序编号:
s/(a)(b)/\2\1/ 实现 ab→ba 倒序。分组嵌套时数左括号:第 2 个 ( 对应 \2。
- 先测试后落盘是正则操作的生命线:grep 预览 → sed 不带 -i 预览 → 加 -i.bak 落盘。正则的杀伤力和它的便利性成正比。
你可能会懵的地方
-
为什么我写的 [0-9]+ 在 grep 里报错或没效果?
grep 默认 BRE,+ 是普通字符。加 -E 用 ERE,或 BRE 写法 \+。同理 |、()、{n,m} 在 BRE 里都要加反斜杠。
-
* 和 + 到底差在哪?
* 允许出现 0 次(可空),+ 要求至少 1 次。ab*c 能匹配 ac,ab+c 不能。写正则时先问自己「空的情况要不要」。
-
正则里的空格有意义吗?
有,空格就是普通字符。「502」会匹配 1502,加空格 502 才是独立字段。要么加空格,要么用 [^0-9]502[^0-9] 边界思路,要么换 awk 按列精确比对。
-
grep -o 和 sed -E 's/.../\1/' 都能提取,怎么选?
只要「片段本身」用 grep -oE,一条命令搞定;要把片段重新组织(加前后缀、调整顺序、多分组重排)用 sed 分组引用。awk 则胜在还能做统计计算。
-
正则能校验邮箱、URL 吗?
理论能,工程上不建议追求完美——RFC 邮箱正则长到离谱。运维场景用「够用的近似」:^[^@]+@[^@]+\.[^@]+$ 验邮箱形状,配合业务层二次校验。Java 里同理。
错法对照
| 错法 | 问题 | 正解 |
|---|
grep "192.168.1.1" 匹配 IP | . 匹配任意字符,误伤 | grep -E "192\.168\.1\.1" |
| `grep "ERROR | WARN"` 不加 -E | | 在 BRE 是普通字符 |
sed 's/(a)/\1/' 未开 -E | BRE 里括号无分组含义 | sed -E 's/(a)/\1/' |
| 校验手机号漏了 ^ $ | 子串命中即通过,如 9913812345678x | 首尾锚定 ^1[3-9][0-9]{9}$ |
.* 到处用导致匹配过宽 | 贪婪吃到行尾,跨过目标 | 用 [^x]*、具体集合或 -o 缩小 |
卡点急救
- 正则「应该匹配却没匹配」:三步排查——① 引号问题,模式用单引号防 Shell 抢解释;② 方言问题,确认 grep 加没加 -E;③ 数据问题,
cat -A 文件 看有没有隐藏的 ^M(Windows 换行)和行尾空格。
- sed 替换后的结果和预期不符:先去掉
-i 在屏幕预览;贪婪匹配问题改用排除字符类;特殊字符(/、&、\)转义,或换分隔符 s#a#b#g。
- 不同机器上同一正则行为不一致:grep 版本差异(busybox 精简版功能少)、别名带了
--color 或 -P。用 type grep 查看是否别名,脚本里用 command grep 或全路径。
面试常问
-
写一个匹配 IPv4 的正则。
参考答案:^([0-9]{1,3}\.){3}[0-9]{1,3}$,进阶加分点:指出它放行 999.999.999.999,严格校验每段 0-255 要展开成 (25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]) 并说明工程上常用「形状校验 + 业务校验」分层处理。
-
BRE 和 ERE 的区别?
字符集与锚定一致,差异在量词和分组:ERE 中 +、?、|、()、{n,m} 直接写,BRE 中要加反斜杠转义(+、?、|、( )、{n,m})。grep 默认 BRE,-E 切 ERE;sed 默认 BRE,-E 切 ERE;awk 原生就是 ERE。
-
怎么把日志里的手机号全部脱敏?
sed -E 's/(1[3-9])[0-9]{9}/\1*********/g' app.log。要点:分组保留前两位、g 全局替换、先预览再 -i 落盘。追问合规场景可补「落盘前用 -i.bak 备份、脱敏后核对行数一致」。
自测三问
[0-9]{3}、[0-9]{3,}、[0-9]{3,5} 分别匹配什么?
- 写出「以 2026-10 开头且包含 ERROR」的 grep -E 命令。
- sed 分组引用里
\2 代表什么?
小结一句话
正则是描述文本形状的语言:锚定 ^$ 定位置、集合 [] 选字符、量词 * + ? {} 控次数、分组 () 配引用做提取——用 ERE 风格,先预览后落盘。
延伸