2.10 正则表达式从入门到精通
预计阅读时间:13 分钟
📖 目录
学习目标
学完本章后,你将能够:
- 理解正则表达式的基本概念与三种流派(BRE / ERE / PCRE)
- 掌握元字符、字符类、量词、分组与反向引用的用法
- 理解贪婪与懒惰匹配机制及回溯原理
- 在 grep、sed、awk 和 Bash 脚本中熟练运用正则
- 写出可维护的实用正则,避免常见陷阱
核心知识
- 元字符(Metacharacter)——
.^$*+?{}等具有特殊含义的字符 - 字符类(Character Class)——
[...]定义待匹配字符集合,[^...]取反 - 量词(Quantifier)——控制重复次数:
*+?{n,m} - 分组与捕获(Grouping & Capturing)——
()改变优先级并捕获匹配内容,反向引用\1~\9 - 零宽断言(Lookaround)——
(?=...)(?!...)(?<=...)(?<!...)匹配位置不消耗字符 - 贪婪与懒惰(Greedy vs Lazy)——默认尽可能多匹配,加
?转为尽可能少匹配 - 回溯(Backtracking)——NFA 引擎在匹配失败后回退尝试其他路径的机制
- BRE / ERE / PCRE——POSIX 基本、扩展、Perl 兼容三种正则流派
知识关联
- 前置知识:熟悉 1.9:重定向与管道 重定向与管道、2.1:Shell 脚本入门 Shell 脚本入门
- 后续影响:正则能力是学习 2.8:Shell 脚本进阶 Shell 脚本进阶、4.8:集中式日志管理 集中式日志管理的重要基础
- 配套技术:grep / sed / awk 是文本处理三剑客,正则将它们串联为一个整体
原理讲解
正则表达式的核心是一个有限状态自动机(Finite Automaton)。当你向引擎提交一个模式和一个字符串,引擎将模式编译为内部状态机,然后逐个字符扫描字符串,尝试将当前状态与字符匹配。
大多数现代正则引擎(包括 PCRE、Python re、Java、JavaScript)使用 NFA(Nondeterministic Finite Automaton)。NFA 的特点是"回溯驱动"——当一条路径失败时,引擎会回退到上一个分支点尝试另一条路。这带来了丰富的表达式能力(前瞻、后顾、反向引用),但也带来了灾难性回溯的风险。
| 特性 | NFA(非确定型) | DFA(确定型) |
|---|---|---|
| 代表实现 | PCRE、Perl、Python re、Java | awk、grep(传统版)、lex |
| 支持反向引用 | ✅ 支持 | ❌ 不支持 |
| 零宽断言 | ✅ 支持 | ❌ 不支持 |
| 匹配速度 | 较慢(可能回溯) | 快(线性时间) |
| 功能丰富度 | 高 | 低 |
贪婪 vs 懒惰:默认量词 * + {n,m} 是贪婪的——引擎在保证整体匹配的前提下,尽可能多地吞入字符。在量词后加 ? 变为懒惰模式——尽可能少地匹配字符。
文本: <div>Hello</div><span>World</span>
# 贪婪: .* 从第一个 < 一直匹配到最后一个 >
grep -Po '<.*>' input
# 输出: <div>Hello</div><span>World</span>
# 懒惰: .*? 停在第一个 > 前
grep -Po '<.*?>' input
# 输出: <div> </div> <span> </span>
(a|aa)+b 在输入 aaaaaaaaac 上会尝试 2^n 条路径。症状:小文件正常,大文件 CPU 100%。应对:使用 .*?、明确结尾字符、或用 (?>...) 原子分组防止回溯。三种正则流派:BRE / ERE / PCRE
| 特性 | BRE(基本) | ERE(扩展) | PCRE(Perl 兼容) |
|---|---|---|---|
| 启用方式 | grep / sed 默认 | grep -E、sed -E、egrep | grep -P、perl、python re |
+ ? | () | 需转义:\+ \? \| \(\) | 直接使用 | 直接使用 |
{n,m} | 需转义 \{n,m\} | 直接使用 | 直接使用 |
\d \w \s | 不支持 | 不支持 | 支持 |
| 零宽断言 | 不支持 | 不支持 | 支持 |
| 懒惰量词 | 不支持 | 不支持 | 支持 .*? |
| 反向引用 | 支持 \1 | 支持 \1 | 支持 \1 / $1 |
egrep 与 grep -E 完全等价(egrep 只是兼容别名,新版 grep 仍保留该命令);sed 默认 BRE,加 -E 切到 ERE;awk 默认 ERE 且不支持 \d(要写 [[:digit:]])。记住一条经验:跨工具移植性最差的是 PCRE 简写类,最好的是 POSIX 字符类——想让表达式在 grep/sed/awk 间通用,就用 [[:digit:]] 而非 \d。
贪婪 vs 懒惰:机制与陷阱
贪婪匹配的规则是"先吞下所有能吞的,再逐一吐出直到整体匹配成功";懒惰则是"逐个试探,一旦整体匹配就停"。两者在边界不清晰时结果截然不同:
文本: price="100" count="3"
# 贪婪:".*" 会一直匹配到最后一个引号
grep -Po '".*"' input
# 输出: "100" count="3"
# 懒惰:".*?" 停在第一个闭合引号
grep -Po '".*?"' input
# 输出: "100" "3"
# 陷阱:.* 在"只有一处匹配点"时与 .*? 结果相同,
# 只在行内存在多个候选时才有差异——调试时先用 echo 验证
回溯灾难:机制与规避
NFA 引擎在每条路径失败后都要回到上一个"岔路口"重试。当模式里多个贪婪量词互相嵌套时,尝试路径数呈指数增长——这就是 ReDoS(正则拒绝服务攻击)。例如 ^(a+)+$ 匹配 30 个 a 加一个 ! 需要约 30 亿次尝试,单线程 CPU 会被占满数分钟。
# 危险模式实例(避免在生产日志过滤中使用)
# ^(a+)+$ 输入 30 个 a 后跟 ! 时指数回溯
# ^(\w+\s?)+@ 输入 30 个 x 后跟 ! 时同样爆炸
# (a|aa)+b 参见上文 callout
# 规避手段(按优先级)
# 1. 锁定结尾:加明确的边界字符让引擎快速失败
# 2. 用懒惰量词:.*? 比 .* 在多数场景更快失败
# 3. 原子分组 (?>...) 丢弃回溯分支(PCRE / Perl 可用)
# 4. 限制输入长度:先拒绝超长字符串再匹配
# 5. 对不可信输入匹配必须设超时(如 Perl 的 alarm、Python regex 模块的 timeout)
各工具正则方言对照表
| 需求 | grep | sed | awk | perl |
|---|---|---|---|---|
| 匹配数字 | grep -P '\d+' | sed -nE '/[0-9]+/p' | awk '/[0-9]+/' | /\d+/ |
| 替换 | —(配合 sed) | s/old/new/g | gsub(/old/,"new") | s/old/new/g |
| 反向引用 | \1 | \1 | \1 或 \\1 | $1 |
| 行首 / 行尾 | ^ $ | ^ $ | ^ $ | ^ $(或 \A \z) |
| 单词边界 | \b(需 -P) | GNU sed 支持(BSD/macOS sed 不识别) | gawk 支持;mawk(Ubuntu 默认)不识别,用 (^|[^a-z]) 模拟 | \b |
| 多行匹配 | grep -z | N 命令 | 调整 RS 变量 | /m /s 修饰符 |
同一需求四工具写法不同是常态:写脚本前先确认目标工具的方言,否则表达式会"静默不匹配"——正则失败不报错,只是结果为空,这是最隐蔽的坑。
示例代码
基础元字符
# . 匹配任意单个字符
echo -e "cat\ncut\ndog" | grep -P 'c.t'
# 输出: cat cut
# ^ 和 $ 分别匹配行首和行尾
echo -e "root\nrouter\nroots" | grep -P '^root'
# 输出: root roots
# * 前一个字符重复 0 次或多次
echo -e "ac\nabc\nabbc\nabbbc" | grep -P 'ab*c'
# 输出: ac abc abbc abbbc
# + 前一个字符重复 1 次或多次
echo -e "ac\nabc\nabbc" | grep -P 'ab+c'
# 输出: abc abbc
# ? 前一个字符出现 0 次或 1 次
echo -e "color\ncolour" | grep -P 'colou?r'
# 输出: color colour
量词
# {n} 精确重复 n 次
echo -e "007\n404\n77" | grep -P '[0-9]{3}'
# 输出: 007 404
# {n,m} 重复 n 到 m 次
echo -e "23\n456\n7890" | grep -P '[0-9]{2,4}'
# 输出: 23 456 7890
# 懒惰量词: +? 匹配 1 次后立即停止
echo 'abc123def' | grep -Po '[a-z]+?[0-9]+?'
# 输出: a1
分组与反向引用
# 分组量化:匹配 error: 或 warning:
echo -e "error: timeout\ninfo: ok\nwarning: disk" | grep -P '^(error|warning):'
# 输出: error: timeout warning: disk
# 反向引用:查找叠词
echo -e "book\nlook\negg\nsky" | grep -P '([a-z])\1'
# 输出: book look egg
# sed 日期格式转换 2026-07-30 → 30-07-2026
echo "2026-07-30" | sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3-\2-\1/'
# 输出: 30-07-2026
# 非捕获分组 (?:...) 提高性能
echo -e "http://ex.com\nhttps://test.org" | grep -P 'https?://'
零宽断言
# 正向前瞻 (?=...):匹配后跟 .gz 的文件名
echo -e "backup.tar.gz\nreadme.txt\nscript.sh" | grep -Po '\w+(?=\.gz)'
# 输出: backup
# 负向前瞻 (?!...):排除 .bak 后缀
echo -e "config.yml\nconfig.yml.bak" | grep -P '\.(?!bak$).*$'
# 输出: .yml
# 正向后顾 (?<=...):提取 $ 后的金额
echo "价格: $29.99, 折扣: $5.00" | grep -Po '(?<=\$)[0-9.]+'
# 输出: 29.99 5.00
# 负向后顾 (?<!...):匹配不在 - 号后的数字
echo "值: 5, 负值: -5" | grep -Po '(?<!-)[0-9]+'
# 输出: 5 5
grep 实战
# 过滤错误日志
grep -Po 'ERROR.*?(?=\s(AT|\d{4}))' app.log
# 去除注释和空行
grep -vE '^\s*(#|$)' /etc/nginx/nginx.conf
# 统计 500 错误次数
grep -c '" 500 ' /var/log/nginx/access.log
# 访问量最高的 10 个 URL 路径
grep -oP 'GET \K[^? ]+' access.log | sort | uniq -c | sort -rn | head -10
sed 实战
# 去掉行尾空白
sed -E 's/[[:space:]]+$//' file.txt
# 只保留用户名
sed -E 's/^([^:]+):.*/\1/' /etc/passwd
# 打印 [database] 配置段
sed -n '/^\[database\]/,/^\[/p' config.ini
# 替换分隔符技巧
sed -E 's|/var/www|/srv/http|g' /etc/nginx/sites-enabled/*
# 安全替换:先测试再加 -i
sed -E 's/old/new/' file
awk 实战
# 匹配 192.168 开头的行
awk '/^192\.168/' /var/log/auth.log
# 第 6 列包含 timeout
awk '$6 ~ /timeout/' app.log
# match() 提取错误消息
awk 'match($0, /error: (.+)/, a) {print a[1]}' app.log
# Nginx 日志 top 10 IP
awk '{ips[$1]++} END {for (ip in ips) print ips[ip], ip}' access.log | sort -rn | head -10
# 组合条件
awk '$3 ~ /^10\./ && $9 ~ /^50[0-9]$/' access.log
实战:日志 IP 提取与统计
# 提取 Nginx access.log 的客户端 IP(第一列)并统计 top10
grep -oP '^\S+' access.log | sort | uniq -c | sort -rn | head -10
# 从混杂文本中提取所有 IP(PCRE)
grep -oP '(\d{1,3}\.){3}\d{1,3}' logs/*.log | sort | uniq -c | sort -rn
# 统计独立 IP 数
grep -oP '^\S+' access.log | sort -u | wc -l
# 提取 IP + 端口组合(代理日志格式 10.0.0.1:53210)
grep -oP '\d{1,3}(\.\d{1,3}){3}:\d+' proxy.log
# 严格 IP 校验(每段 0-255,配合脚本做最终判定)
grep -P '^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$' ip_list.txt
实战:URL 解析
# 提取协议 + 域名(含端口)
echo "https://user:pass@api.example.com:8443/v1/orders?id=5" | \
grep -oP '^[a-z]+://[^/]+'
# 输出: https://user:pass@api.example.com:8443
# 提取查询参数对(name=value)
echo 'page=2&sort=desc&tag=linux' | grep -oP '(?<=[?&])[^=&]+=[^&]+'
# 输出: page=2 sort=desc tag=linux
# 提取路径中的 HTML 文件名
echo "https://nb.iohow.com/articles/ch44.html?utm=x" | grep -oP '/\w+\.html'
# 输出: /ch44.html
# 参数名排序后重组(便于 URL 规范化去重)
echo "b=2&a=1&c=3" | tr '&' '\n' | sort | tr '\n' '&' | sed 's/&$//'
# 输出: a=1&b=2&c=3
# 提取域名(去协议、去路径)
echo "https://www.example.co.uk/path/to/file" | \
grep -oP '(?<=://)[^/]+' | sed 's/^www\.//'
# 输出: example.co.uk
实战:数据清洗正则集
# 去掉不可见控制字符
sed -E 's/[[:cntrl:]]//g' dirty.txt
# 折叠连续空白为单个空格
sed -E 's/[[:space:]]+/ /g' file.txt
# 删除完全重复的相邻行
awk '!seen[$0]++' file.txt
# 手机号脱敏:138****5678
echo "13812345678" | sed -E 's/(1[0-9]{2})[0-9]{4}([0-9]{4})/\1****\2/'
# 邮箱脱敏:z***@example.com
echo "zhangsan@example.com" | sed -E 's/^(.)[^@]+@/\1***@/'
# 清洗 CSV:把带引号的字段还原(去掉转义引号)
echo '"hello, ""world"""' | sed -E 's/""/"/g; s/^"|"$//g'
# 输出: hello, "world"
# 金额数字千分位格式化(perl 更直观)
echo "1234567.89" | perl -pe 's/(?<=\d)(?=(\d{3})+(\.\d+)?$)/,/g'
# 输出: 1,234,567.89
正则性能优化实战
# 优化 1:字符类替代分支
# 慢: (a|b|c|d) 快: [abcd]
# 慢: [0-9]|[a-z] 快: [0-9a-z]
# 优化 2:锚定位置优先
# grep '^abc' 比 grep 'abc' 更快(先在行首尝试)
# 优化 3:不需要捕获时用非捕获分组
# (abc)+ → (?:abc)+
# 优化 4:精确字符类替代 .*
# 提取引号内容: "([^"]*)" 比 "(.*)" 更快且结果更准确
# 实测:海量日志首选 awk(DFA,线性时间)或 grep -E
time awk '/^10\./' big_file.log
time grep -P '^10\.' big_file.log
# 结论:需要 \d / 断言等高级能力才用 grep -P,
# 且对不可信输入先限制长度,防 ReDoS
常见错误
| 常见错误 | 根因 | 正确做法 |
|---|---|---|
忘记转义 . * 等元字符 | 把元字符当普通字符使用 | 匹配字面 . 用 \.,字面 * 用 \* |
| Bash 中给正则变量加引号 | [[ $s =~ "$re" ]] 当作字面字符串 | 去掉引号:[[ $s =~ $re ]] |
sed 中 + ? 不转义也未用 -E | sed 默认 BRE,元字符需反斜杠 | 使用 sed -E 启用 ERE |
BRE 中直接写 \d \w | \d 仅在 PCRE 有效 | 用 [[:digit:]] [[:alnum:]] |
贪婪 .* 匹配过多内容 | 默认贪婪一路匹配到末尾 | 用 .*? 懒惰量词 |
| 用正则解析 HTML / JSON | 正则无法处理嵌套结构 | 使用专用解析器(如 jq / html.parser) |
最佳实践
| 最佳实践 | 理由 | 示例 |
|---|---|---|
| 先用 echo + 样本测试 | 避免生产环境执行错误正则 | echo 'test' | grep -Po 'pattern' |
优先 grep -E 而非裸 grep | ERE 无需转义 +?()|{} | grep -E '(error|fatal)' log |
| 复杂正则拆分注释 | 提高可维护性 | 用变量名说明:re_ip='^([0-9]{1,3}\\.){3}[0-9]{1,3}$' |
| 用非捕获分组 | 减少引擎开销 | (?:https?|ftp):// |
| sed 替换先不加 -i 预览 | 确认范围再写回 | 先检查输出再加 -i |
| 避免正则做数值校验 | 不适合数字范围算术 | IP 校验用脚本组合 (( $octet <= 255 )) |
练习题
- (概念)BRE、ERE、PCRE 三种正则流派的主要区别是什么?在 grep 中分别用什么选项启用?
- (概念)什么是"贪婪匹配"和"懒惰匹配"?请各举一个量词的例子并说明写法差异。
- (实操)给定文件
emails.txt,内容包含user@example.com、admin@test.org、invalid-email。写一条 grep 命令只提取有效的邮箱地址。 - (实操)将
dates.txt中的日期从2026-07-30格式转换为30/07/2026,使用一条 sed 命令完成。 - (🔍 挑战)给定 Nginx 日志
access.log,统计返回状态码为 4xx 且请求路径包含/api/的客户端 IP 列表,按出现次数降序排列。
点击查看答案
- BRE(基本正则):元字符
(){}?+需转义,grep 默认使用。ERE(扩展正则):元字符不需转义,grep -E 启用。PCRE(Perl 兼容):支持\d \w (?=...)等高级特性,grep -P 启用。 - 贪婪量词默认尽可能多匹配(如
.*从开头一直匹配到末尾)。懒惰量词加?改为尽可能少匹配(如.*?匹配到第一个符合条件的字符即停)。 grep -P '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' emails.txtsed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' dates.txtgrep '\/api\/' access.log | grep ' "4[0-9][0-9] ' | awk '{print $1}' | sort | uniq -c | sort -rn
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 BRE、ERE、PCRE 三种正则流派的区别 | 尝试向他人讲解 |
| 命令操作 | 能不查文档使用 grep -E、sed -E 配合正则表达式过滤文本 | 在终端实际执行 |
| 原理掌握 | 能说出贪婪匹配与懒惰匹配的区别以及回溯机制 | 画出匹配流程图 |
| 故障排查 | 能独立排查正则表达式导致的灾难性回溯(ReDoS)问题 | 模拟故障并优化 |
| 最佳实践 | 能说明为什么跨工具移植时应该使用 POSIX 字符类而不是 PCRE 简写 | 对比不同写法 |
本章总结
正则表达式是 Linux 文本处理的"万能钥匙"。本章从元字符基础讲起,逐步深入到量词、分组、零宽断言、贪婪懒惰机制,并通过 grep、sed、awk 三大工具的实战案例展示了正则的完整应用场景。
掌握正则的核心在于"模式思维"——将文本匹配需求抽象为模式描述。遇到复杂需求时,先拆解为多个子模式,用测试工具验证后再集成。推荐使用 regex101.com 进行交互式学习与调试。
速查表
| 类别 | 模式 | 说明 |
|---|---|---|
| 行锚定 | ^ $ | 行首 / 行尾 |
| 任意字符 | . | 除换行外的任意单个字符 |
| 字符类 | [abc] [^abc] | 匹配集合内 / 集合外字符 |
| POSIX 字符类 | [[:digit:]] | 跨语言安全的字符分类 |
| PCRE 简写 | \d \w \s | 数字 / 单词字符 / 空白 |
| 量词 | * + ? {n,m} | 重复次数控制 |
| 懒惰量词 | *? +? ?? {n,m}? | 尽可能少匹配 |
| 分组捕获 | (...) \1 | 分组 + 反向引用 |
| 非捕获分组 | (?:...) | 分组但不捕获 |
| 正向前瞻 | (?=...) | 后面跟着 |
| 负向前瞻 | (?!...) | 后面不跟着 |
| 正向后顾 | (?<=...) | 前面是 |
| 负向后顾 | (?<!...) | 前面不是 |
学习路径
- 本章工具实践 → 2.8:Shell 脚本进阶 Shell 脚本进阶(正则与脚本融合)
- 日志分析进阶 → 4.8:集中式日志管理 集中式日志管理(ELK 中的正则过滤)
- 文本处理深化 → 4.19:Shell 实战案例集 Shell实战案例集(更多自动化场景)
延伸阅读
- regex101.com — 交互式正则测试与调试工具
- Regular-Expressions.info — 最全面的正则教程与参考资料
- PCRE 官方文档 — Perl 兼容正则表达式库的完整规范
- 《精通正则表达式(第三版)》Jeffrey Friedl 著 — 正则原理的经典著作
- 1.9:重定向与管道 重定向与管道 — 组合 grep/sed/awk 的基础设施
- 2.8:Shell 脚本进阶 Shell 脚本进阶 — 更多正则驱动脚本范例