强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 6 章:正则表达式

第 6 章:正则表达式

正则表达式是文本处理的灵魂。掌握它,你就能在文字的海洋中精确捕捞。

6.1 正则表达式概览

三种正则标准

标准全称工具语法严格度
BREBasic Regular Expressionsed, grep(默认)最严格,需转义
EREExtended Regular Expressionawk, grep -E较宽松
PCREPerl-Compatible Regular Expressiongrep -P, PHP, Python最灵活

BRE vs ERE 核心差异

功能BREERE
任意字符..
零次或多次**
一次或多次\++
零次或一次\??
分组\( \)( )
||
重复 {n,m}\{n,m\}{n,m}
词首边界\b\<\b
词尾边界\b\>\b

💡 记忆口诀:ERE 中,元字符不需要反斜杠;BRE 中,元字符需要 \ 转义。但 . * [ ] ^ $ 在两种标准中都不需要转义。

6.2 基础元字符

核心元字符表

元字符含义BREERE示例
.任意单个字符a.c → abc, aXc
^行首^error
$行尾end$
*零次或多次ab*c → ac, abc, abbc
+一次或多次\+ab+c → abc, abbc
?零次或一次\?ab?c → ac, abc
[...]字符类[abc] → a, b, c
[^...]否定字符类[^abc] → 非 a, b, c
(...)分组\( \)(ab)+ → ab, abab
||cat|dog
{n,m}重复次数\{n,m\}a{2,4}
\n反向引用\(.\)\1 → aa
\b单词边界\bword\b
\w单词字符✅ (grep)✅ (gawk)[a-zA-Z0-9_]
\d数字❌ (PCRE)❌ (gawk 5.0+)[0-9]
\s空白字符✅ (gawk)[ \t\n]

字符类(Character Class)

# POSIX 字符类(推荐在 [] 中使用)
[[:alpha:]]   # 字母 [a-zA-Z]
[[:digit:]]   # 数字 [0-9]
[[:alnum:]]   # 字母和数字 [a-zA-Z0-9]
[[:space:]]   # 空白字符(空格、制表符、换行等)
[[:upper:]]   # 大写字母
[[:lower:]]   # 小写字母
[[:punct:]]   # 标点符号
[[:print:]]   # 可打印字符
[[:graph:]]   # 可见字符(不含空格)
[[:blank:]]   # 空格和制表符
[[:xdigit:]]  # 十六进制字符 [0-9a-fA-F]

# 使用示例
sed -E 's/[[:space:]]+/ /g' file       # 压缩多个空白为一个
awk '/[[:upper:]]/' file               # 包含大写字母的行

⚠️ 注意:POSIX 字符类必须在方括号内使用:[[:alpha:]],而不是 [:alpha:]

6.3 常用模式实战

IP 地址

# 简化匹配(0-255 精确匹配太复杂,这里用简化版)
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/

# 精确匹配(0-255)(需要 PCRE 或复杂的 ERE)
# BRE 版本(简化):
/^([0-9]{1,3}\.){3}[0-9]{1,3}/

# 更精确的(匹配 0-255):
/^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/

邮箱地址

# 简化匹配
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/

# 在 awk 中使用
awk '/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/ {print}' file

URL

# 匹配 HTTP/HTTPS URL
/https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\/[a-zA-Z0-9._~:/?#\[\]@!$&'()*+,;=-]*)?/

日期格式

# YYYY-MM-DD
/[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])/

# DD/MM/YYYY
/(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[0-2])\/[0-9]{4}/

日志时间戳

# Apache/Nginx 格式: 15/Jan/2024:10:23:45 +0800
/\[([0-9]{2}\/[A-Z][a-z]{2}\/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2} [+-][0-9]{4})\]/

# ISO 格式: 2024-01-15T10:23:45+08:00
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}/

6.4 分组与反向引用

捕获组

# BRE 分组(需要转义)
sed 's/\(.*\) \(.*\)/\2 \1/' file

# ERE 分组
awk '{match($0, /([0-9]+)-([0-9]+)/, arr); print arr[1], arr[2]}' file

# SED 反向引用(BRE)
echo "abc-def" | sed 's/\(.*\)-\(.*\)/\2-\1/'
→ def-abc

非捕获组(PCRE)

# PCRE 支持非捕获组(不影响反向引用编号)
grep -P '(?:error|warning): (.*)' logfile

# 在 AWK 中,所有组都是捕获组

贪婪 vs 非贪婪

# 贪婪匹配(默认):尽可能多
echo '<b>hello</b> world' | sed -E 's/<.*>//'
→  world           # 匹配了 <b>hello</b> world

# 非贪婪:尽可能少(使用 [^>]* 替代)
echo '<b>hello</b> world' | sed -E 's/<[^>]*>//g'
→ hello world

# PCRE 非贪婪量词
echo '<b>hello</b> world' | grep -Po '<.*?>'
→ <b>
→ </b>

零宽断言(Lookaround)

PCRE 和部分工具支持零宽断言:

# 正向前瞻 (?=pattern) — 后面是...
grep -P 'error(?=:)' logfile     # 匹配 "error" 后面跟 ":"

# 负向前瞻 (?!pattern) — 后面不是...
grep -P 'error(?!:)' logfile     # 匹配 "error" 后面不跟 ":"

# 正向后顾 (?<=pattern) — 前面是...
grep -P '(?<=error): ' logfile   # 匹配 "error" 之后的 ": "

# 负向后顾 (?<!pattern) — 前面不是...
grep -P '(?<!error): ' logfile   # 匹配不是 "error" 之后的 ": "

⚠️ 兼容性:零宽断言只在 PCRE(grep -P)和部分现代工具中支持。AWK 和 SED 不支持。

6.5 性能优化

正则性能从高到低

快 ──────────────────────────────── 慢
字面字符串 > 字符类 > 量词(.*) > 反向引用 > 零宽断言

性能优化技巧

技巧说明示例
锚定使用 ^ $ 减少匹配范围^errorerror
具体化[a-z] 代替 .^[a-z]+^.+
非贪婪尽早停止匹配[^>]* 代替 .*
避免回溯减少 .*|拆分多个简单模式
短路先简单后复杂grep -E "foo" | grep -E "complex_pattern"
锚定边界使用 \b\berror\berror 精确
# ❌ 慢:贪婪匹配 + 回溯
grep -E '.*error.*.*warning.*' logfile

# ✅ 快:两个简单 grep
grep 'error' logfile | grep 'warning'

# ❌ 慢:.* 开头
sed -E 's/.*error/error/' file

# ✅ 快:用 s/.*//  删除前缀,再处理

预编译正则(GNU AWK)

# GNU AWK 中,使用 @/pattern/ 可以"预编译"正则
gawk 'BEGIN { pattern = @/error/ } $0 ~ pattern { print }' file

# 等效但更清晰
gawk '$0 ~ /error/ { print }' file

6.6 工具间正则差异速查

特性grep (BRE)grep -E (ERE)grep -P (PCRE)sed (BRE)awk (ERE)
. * [ ] ^ $
+ ? { } ( )转义转义
|转义转义
\d \w \s部分
\1 反向引用仅 match()
(?:...) 非捕获
(?=...) 零宽断言
*? 非贪婪部分
(?i) 忽略大小写

6.7 调试正则表达式

分步构建

# 从简单开始,逐步添加复杂度
grep 'error' logfile                    # 步骤 1:基本匹配
grep 'error.*[0-9]' logfile             # 步骤 2:添加数字
grep -E 'error.*[0-9]{3,}' logfile      # 步骤 3:添加量词
grep -E '^(.*error.*[0-9]{3,})' logfile # 步骤 4:添加锚定

使用 AWK 测试

# 测试正则是否匹配
echo "test string" | awk '/pattern/ {print "MATCH: " $0} !/pattern/ {print "NO MATCH: " $0}'

# 显示匹配位置
echo "abc123def456" | awk '{
    match($0, /[0-9]+/)
    print "匹配位置:", RSTART, "长度:", RLENGTH, "内容:", substr($0, RSTART, RLENGTH)
}'

SED 的 l 命令

# l 命令显示不可见字符
echo -e "tab\there" | sed -n 'l'
→ tab\there$

# 查看替换结果的可打印形式
echo "hello world" | sed 's/world/\tAWK/' | sed -n 'l'

6.8 实战练习

🏢 练习 1:提取所有链接

# 从 HTML 中提取 href 属性
grep -oP 'href="\K[^"]+' page.html
# 或用 AWK
awk '{
    while (match($0, /href="([^"]+)"/, arr)) {
        print arr[1]
        $0 = substr($0, RSTART + RLENGTH)
    }
}' page.html

🏢 练习 2:验证输入格式

# 验证日期格式 YYYY-MM-DD
echo "2024-01-15" | awk '{
    if ($0 ~ /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/)
        print "有效日期"
    else
        print "无效日期"
}'

🏢 练习 3:提取 JSON 值

# 从简单 JSON 中提取值
echo '{"name":"Alice","age":30}' | grep -oP '"name"\s*:\s*"\K[^"]+'
→ Alice

# 更复杂的场景建议使用 jq
echo '{"name":"Alice","age":30}' | jq -r '.name'

6.9 正则表达式速查卡

基本:
  .         任意单字符
  ^         行首
  $         行尾
  *         零次或多次
  +         一次或多次 (ERE)
  ?         零次或一次 (ERE)
  {n}       恰好 n 次 (ERE)
  {n,m}     n 到 m 次 (ERE)

字符类:
  [abc]     a, b 或 c
  [^abc]    非 a, b, c
  [a-z]     a 到 z
  [[:alpha:]] 字母
  [[:digit:]] 数字
  [[:space:]] 空白

边界:
  ^         行首
  $         行尾
  \b        单词边界
  \<        词首 (BRE)
  \>        词尾 (BRE)

分组:
  (...)     捕获组 (ERE)
  \1        反向引用
  |         或 (ERE)

转义:
  \.        字面量点
  \*        字面量星号
  \\        字面量反斜杠

扩展阅读


下一章:第 7 章:文本处理实战 — 日志分析、CSV 处理、JSON 提取。