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 兼容三种正则流派

知识关联

原理讲解

正则表达式的核心是一个有限状态自动机(Finite Automaton)。当你向引擎提交一个模式和一个字符串,引擎将模式编译为内部状态机,然后逐个字符扫描字符串,尝试将当前状态与字符匹配。

大多数现代正则引擎(包括 PCRE、Python re、Java、JavaScript)使用 NFA(Nondeterministic Finite Automaton)。NFA 的特点是"回溯驱动"——当一条路径失败时,引擎会回退到上一个分支点尝试另一条路。这带来了丰富的表达式能力(前瞻、后顾、反向引用),但也带来了灾难性回溯的风险。

特性NFA(非确定型)DFA(确定型)
代表实现PCRE、Perl、Python re、Javaawk、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 -Esed -E、egrepgrep -P、perl、python re
+ ? | ()需转义:\+ \? \| \(\)直接使用直接使用
{n,m}需转义 \{n,m\}直接使用直接使用
\d \w \s不支持不支持支持
零宽断言不支持不支持支持
懒惰量词不支持不支持支持 .*?
反向引用支持 \1支持 \1支持 \1 / $1

egrepgrep -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)

各工具正则方言对照表

需求grepsedawkperl
匹配数字grep -P '\d+'sed -nE '/[0-9]+/p'awk '/[0-9]+/'/\d+/
替换—(配合 sed)s/old/new/ggsub(/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 -zN 命令调整 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 中 + ? 不转义也未用 -Esed 默认 BRE,元字符需反斜杠使用 sed -E 启用 ERE
BRE 中直接写 \d \w\d 仅在 PCRE 有效[[:digit:]] [[:alnum:]]
贪婪 .* 匹配过多内容默认贪婪一路匹配到末尾.*? 懒惰量词
用正则解析 HTML / JSON正则无法处理嵌套结构使用专用解析器(如 jq / html.parser

最佳实践

最佳实践理由示例
先用 echo + 样本测试避免生产环境执行错误正则echo 'test' | grep -Po 'pattern'
优先 grep -E 而非裸 grepERE 无需转义 +?()|{}grep -E '(error|fatal)' log
复杂正则拆分注释提高可维护性用变量名说明:re_ip='^([0-9]{1,3}\\.){3}[0-9]{1,3}$'
用非捕获分组减少引擎开销(?:https?|ftp)://
sed 替换先不加 -i 预览确认范围再写回先检查输出再加 -i
避免正则做数值校验不适合数字范围算术IP 校验用脚本组合 (( $octet <= 255 ))

练习题

  1. (概念)BRE、ERE、PCRE 三种正则流派的主要区别是什么?在 grep 中分别用什么选项启用?
  2. (概念)什么是"贪婪匹配"和"懒惰匹配"?请各举一个量词的例子并说明写法差异。
  3. (实操)给定文件 emails.txt,内容包含 user@example.comadmin@test.orginvalid-email。写一条 grep 命令只提取有效的邮箱地址。
  4. (实操)将 dates.txt 中的日期从 2026-07-30 格式转换为 30/07/2026,使用一条 sed 命令完成。
  5. (🔍 挑战)给定 Nginx 日志 access.log,统计返回状态码为 4xx 且请求路径包含 /api/ 的客户端 IP 列表,按出现次数降序排列。
点击查看答案
  1. BRE(基本正则):元字符 (){}?+ 需转义,grep 默认使用。ERE(扩展正则):元字符不需转义,grep -E 启用。PCRE(Perl 兼容):支持 \d \w (?=...) 等高级特性,grep -P 启用。
  2. 贪婪量词默认尽可能多匹配(如 .* 从开头一直匹配到末尾)。懒惰量词加 ? 改为尽可能少匹配(如 .*? 匹配到第一个符合条件的字符即停)。
  3. grep -P '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' emails.txt
  4. sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' dates.txt
  5. grep '\/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分组 + 反向引用
非捕获分组(?:...)分组但不捕获
正向前瞻(?=...)后面跟着
负向前瞻(?!...)后面不跟着
正向后顾(?<=...)前面是
负向后顾(?<!...)前面不是

学习路径

延伸阅读

常见问题

正则表达式里 [sS] 是什么意思?
s 匹配空白字符,S 匹配非空白字符,[sS] 的组合匹配任意字符(包括换行)。常用于跨行匹配。在 JavaScript 中等价于 [^],在 Python 中需要 re.DOTALL 标志。简单场景用 .*? 默认不跨行,需要跨行时用 [sS]*?。
贪婪和懒惰匹配有什么区别?
贪婪(默认)尽可能多匹配:<div>.*</div> 从第一个 <div> 匹配到最后一个 </div>。懒惰(加 ?)尽可能少匹配:<div>.*?</div> 从第一个 <div> 匹配到第一个 </div>。解析 HTML/XML 总是用懒惰匹配。尽量用专门的解析器(如 BeautifulSoup)而非正则处理结构化文档。
零宽断言有什么用?
零宽断言匹配位置而非字符。常见场景:① (?=pattern) 先行断言:匹配后跟 pattern 的位置(如 d(?=px)找出后跟 px 的数字);② (?<=pattern) 后发断言:匹配前跟 pattern 的位置;③ (?!pattern) 否定先行:匹配不后跟 pattern 的位置。典型应用:密码强度规则(同时包含大小写和数字)。
↑ 回到顶部