第三章了解正则表达式语法

表达式

正则表达式和算术表达式思想类似:

  1. 算术表达式:字面量+运算符,有运算优先级,算出数值结果。例:2+3*4,乘法优先级高于加法;括号()改变计算顺序。
  2. 正则表达式:字面字符+元字符(特殊操作符号),描述文本模式,不是字面字符串。

字面量:写出来是什么,它的值就是什么,不需要再解析、计算。
字面字符:字符写出来是什么,就去文本中匹配一模一样的这个字符。没有特殊功能,不做任何特殊处理。

  • 基础操作:字符拼接,字符依次相邻匹配。ABE 只匹配连续字符串ABE;区分大小写,A不等于a。
  • 程序工作流程:grep/sed/awk读取每一行文本,拿正则模式和该行做匹配;匹配失败就向后移动字符重新尝试匹配。

简单元字符举例

  • .(点):通配符,匹配任意1个字符(换行除外;awk可以匹配换行);A.E 匹配 ACE、ABE、ALE。
  • *(星号):修饰前面那一个单元,匹配前面单元出现0次或多次;A.*E:A后面任意数量字符,最后E。

重点区分:shell里*本身代表任意字符;正则里*不独立工作,只修饰它前面的表达式。

两类元字符:

  1. 占位类元字符:自身代表一个字符,例如 .
  2. 修饰量词元字符:不匹配字符,只修改前面的匹配规则,例如 *

成排的字符

除元字符以外的所有字符,都是字面量,匹配自身。

表3‑1:元字符汇总

BRE基础正则(grep、sed使用)

特殊字符 用途
. 匹配除换行以外任意单个字符;awk中可以匹配换行
* 匹配它前面单元0次或多次
[...] 字符类,匹配括号内任意1个字符;开头写^表示否定排除;-表示范围;]放第一个位置代表字面];括号内元字符失去特殊含义
^ 正则开头:匹配行首;awk匹配字符串开头
$ 正则末尾:匹配行尾;awk匹配字符串结尾
\{n,m\} 次数限定。\{n\}恰好n次;\{n,\}至少n次;\{n,m\}n~m次
\ 转义,把元字符变为普通字符,或者把普通字符变为元字符

ERE扩展正则(egrep、awk使用,不需要反斜杠转义+ ? | () {})

特殊字符 用途
+ 前面单元出现1次或多次
? 前面单元出现0次或1次
| 多选“或”,匹配左边或者右边模式
() 分组,把一组字符当成一个整体
{n,m} 次数限定,不需要反斜杠

备注:传统awk对{n,m}支持不好;GNU awk/sed完整支持。

普遍存在的反斜杠 \

反斜杠两大作用:

  1. 转义元字符→普通字符:如果你要匹配符号本身,加\。
    • 匹配字面小数点:\.
    • 匹配反斜杠本身:\\
    • 示例:匹配troff宏.nf:\.nf
  2. sed中,把普通字符变成元字符:\( \) 、\{ \} 、\n;sed必须写反斜杠,否则没有特殊含义。
    • \1‑ \9:反向引用,第五章sed详细讲解。

通配符 .

. = 匹配任意单个字符,换行除外(awk除外)。

  • 示例:80.86,匹配80286、80386;注意也会匹配错误文本,不是只匹配数字。
  • 示例:Chapter.,匹配Chapter后面紧跟任意1个字符;
    • ❗行尾的Chapter不会被匹配:.不能匹配换行;直接写Chapter可以匹配行尾。
1
grep 'Chapter.' sample

编写正则表达式

正则写出来不生效,大多不是工具bug,而是模式描述不符合真实文本情况。

编写正则三步法

  1. 明确:我要匹配什么?文本中这些内容长什么样,会出现在什么上下文?
  2. 写模式,描述目标文本。
  3. 一定要测试模式,核对输出结果。

测试结果四类情况

  1. Hits(击中):成功匹配,正是我想要的行
  2. Misses(未击中):完全无关,没有匹配,正确。
  3. Omissions(遗漏):我想要匹配,但是没匹配到,需要扩大匹配范围。
  4. False alarms(假警报):不该匹配,却匹配到了,需要增加条件缩小范围。

调优思路:出现假警报 → 增加限制条件缩小匹配;出现遗漏 → 扩展模式捕获更多情况。

经验:固定字符串去掉字符,匹配范围变大;增加字符,匹配范围变小;元字符用来灵活平衡范围。

字符类 []

“类” = 一组字符的集合, 作用:指定某一个位置可以允许哪些字符,[]整体只匹配1个字符。

示例:

  1. [Ww]hat:首字符大写W或者小写w;匹配what / What;注意也会匹配Whatever、somewhat。
  2. \.H[12345]:匹配troff宏.H1、.H2….H5。

shell也支持[]语法,shell会解析方括号,命令行写正则必须用单引号包裹模式,交给grep处理,不让shell提前解析。

1
grep '\.H[123]' ch0[12]

重要规则:在[]方括号内部,大部分元字符丧失特殊含义,.就是普通点号。

表3‑2 字符类内部特殊字符

字符 功能
\ 仅awk内有效,用来转义字符
- 不在首尾位置,代表字符范围;放在最前/最后,就是字面连字符
^ 必须写在方括号第一个,代表排除(否定)

字符的范围

用-简写连续字符:

  • [A‑Z]全部大写字母
  • [0‑9]全部数字
  • [a‑zA‑Z]大小写字母

示例:

  1. [cC]hapter[1‑9]匹配chapter后接1位数字;会误匹配chapter 12(假警报,因为只匹配第一位数字)。
  2. 可以同时写多个范围,混合普通字符:[0‑9a‑z?,.;:'"]
  3. -想要作为字面符号,放到方括号第一个位置:[-+*/]匹配加减乘除符号。
  4. ]右括号,放到方括号第一个位置代表字面]。

日期简单示例(不会校验日期合法性,只管格式)

1
[0‑1][0‑9][-/][0‑3][0‑9][-/][0‑9][0‑9]

排除字符类 [^...]

^写在方括号的第一个字符,代表:匹配所有不在括号内的字符(换行除外,awk可以匹配换行)。

示例:

  1. [^0‑9]匹配任意非数字字符
  2. [^aeiou]匹配非元音字符(包含辅音、标点、大写字母)
  3. \.DS "[^1]":匹配.DS "X",X不能是数字1;用来避开.DS "1"这一行。

shell中^有特殊含义,命令中模式务必使用单引号包裹。

POSIX字符类补充

背景:[a‑z]只支持英文字母;POSIX为多语言设计标准字符类;GNU sed、grep、awk支持。 POSIX概念:我们说的[]字符类,标准名称叫括号表达式;POSIX三种括号内部标记:

  1. POSIX字符类:[:关键字:],必须写在[]内部,例 [[:alpha:]]
  2. 整理符号 [.ch.]:GNU工具大多不支持
  3. 等价类 [=e=]:处理重音字符,GNU工具大多不支持

表3‑3 POSIX字符类

类 匹配字符
[:alnum:] 字母+数字
[:alpha:] 字母字符(大小写)
[:blank:] 空格、制表符tab
[:cntrl:] 控制字符
[:digit:] 数字0‑9
[:graph:] 可见可打印非空白字符
[:lower:] 小写字母
[:print:] 全部可打印字符,含空格
[:punct:] 标点符号
[:space:] 全部空白字符
[:upper:] 大写字母
[:xdigit:] 十六进制字符

原书提示:本书脚本不依赖POSIX字符类,兼容性考虑。

重复出现的字符(量词)

BRE基础正则(sed/grep)

  • *:前面单元0次或多次。
    • [15]0*:匹配1或者5,后面0可以出现0次或者多次。
    • 匹配1个或多个空格:*(空格+星号)
    • ".*":匹配双引号之间尽可能多字符,贪婪匹配,匹配到行末尾最后一个引号。

ERE扩展正则(egrep / awk)

  • +:前面单元1次或多次;匹配1个及以上空格直接写 +
  • ?:前面单元0次或者1次,可有可无。
    • 80[234]?86:匹配8086、80286、80386、80486。

区分shell的?:shell问号代表任意单个字符,等价正则的.。

易错示例对比

  • can [no']*t:can和t之间,n/o/‘任意字符,任意次数;可以匹配cannot / can’t / cant。
  • can.*t:can和t之间任意字符;会把肯定句I can do it也错误匹配。

术语:closure(闭合)描述“0次或多次重复”。

符号 BRE (grep/sed 默认) ERE(egrep/awk/grep‑E)
或 | ` `
分组 \( \) ()
一次或多次 \+ +
0 或 1 次 \? ?
次数范围 \{n,m\} {n,m}

只有 .* ^ $ [] 在两种模式下行为一样。

  • BRE(Basic 基础):sed、grep 默认;+ ? | () {} 要写反斜杠 \+ \? \| \( \) \{ \}
  • ERE(Extended 扩展):awk、egrep、grep‑E;+ ? | () {} 直接写,不加反斜杠。

单词是什么?第一部分

直接写 book(前后空格)匹配完整单词会遇到很多问题:

  1. 匹配不到行开头、行结尾的book。
  2. 单词后跟标点(. , ?),空格不存在,匹配失败。
  3. book.*会匹配bookish、bookworm这类包含book的长单词(假警报)。
  • egrep可用book.?:book后面最多跟一个字符;可以匹配book / books;但还会匹配booky。
  • sed没有?,只能靠字符类来约束。

小结:简单空格包裹方式只能处理一小部分场景,后面第二部分给出完整方案。

定位元字符(锚点)^ $

定位符:不匹配实际字符,匹配文本的位置。

  1. ^:写正则最开头,匹配行首。

    • ^ :匹配以tab制表符开头的行。
  2. $:写正则末尾,匹配行尾。

    • *$:匹配行末尾有若干空格的行。
  3. ^$:匹配空行(一行里面完全没有任何字符)

    1
    
    grep -c '^$' ch04 #统计文件空行数量
    
  4. ^ *$:匹配只有空格的“伪空行”。

  5. ^.*$:匹配整行全部内容。

重要差异:

  • sed/grep(BRE):^只在开头特殊;$只在结尾特殊;中间出现^/$就只是普通字符。
  • awk:^和$无论出现在哪里都有锚点含义;想匹配字面^ $,必须反斜杠转义 \^ \$。

短语

这里的短语,不是正则语法术语!是自然语言概念:指由多个单词组成的一段文字,例如 Almond Joy,两个单词构成一个短语。

grep/sed默认按行处理;模式不能跨换行匹配。 如果目标短语被换行拆分到两行,单行正则很难直接捕获。

后面sed章节会讲解多行模式,还有shell脚本处理跨行短语。

字符的跨度(次数限定大括号)

工具 语法 说明
sed/grep(BRE) \{n,m\} 必须带反斜杠
egrep/awk(ERE) {n,m} 不用反斜杠

参数规则:

  1. \{n\}:严格匹配前面单元n次
  2. \{n,\}:至少n次,无上限
  3. \{n,m\}:n次~m次之间

等价对照:

  • * 等价 \{0,\}
  • + 等价 \{1,\}
  • ? 等价 \{0,1\}

示例:

  1. 10\{2,4\}1:匹配1后面2‑4个0再跟1,匹配1001、10001,不匹配101。
  2. 美国社保号:[0‑9]\{3\}-[0‑9]\{2\}-[0‑9]\{4\}
  3. 电话号码:[0‑9]\{3\}-[0‑9]\{4\}

老版本awk不支持大括号,只能手动重复字符类写多次。

选择性操作 竖线 |(ERE扩展正则 egrep/awk)

| 代表或,两边整个模式二选一匹配。

1
UNIX|LINUX

匹配包含UNIX,或者LINUX的行。可以写多个 A|B|C。

sed基础正则BRE没有|,要实现或逻辑,需要写多个模式。

分组操作 ()(ERE扩展正则 egrep/awk)

圆括号(),把一组字符打包成一个整体,量词、|可以对整组生效。

示例1:BigOne( Computer)? 匹配BigOne 或者 BigOne Computer;括号内整体出现0次或1次。

示例2:Lab(oratorie)?s 匹配Labs、Laboratories

1
egrep "Lab(oratorie)?s" mail.list

示例3:compan(y|ies) 匹配company / companies。

注意:传统sed(grep BRE)不能直接用()分组;sed要用\( \)。

单词是什么?第二部分:真正匹配完整单词

直接空格包裹的缺陷:处理不了:行首、行尾、单词前后标点符号 " ( ) ? . ,。

方案思路

  1. 单词前后可能出现:空格、行开头^、行结尾$;
  2. 单词左右可能附着标点符号:引号、括号、问号逗号句号等;
  3. 使用egrep的分组+或(^| ),代表:行开头 或者 空格。

完整模式:

1
(^| )[\"[{(]*book[]})\"?!.,;:'s]*( |$)
  • (^| ):book前面,要么是行开头,要么空格
  • [\"[{(]*:book前面可以有若干左引号、左方括号、左括号

重点语法规则:字符类 [ 之后,如果第一个字符写的是 ],这个]就被当成字面右方括号,不会关闭字符类!

1
2
[ ] } ) \" ? ! . , ; : ' s ] *
↑第一个字符是 ],是字面右方括号,不结束字符类!
  • book:目标单词
  • []})\"?!.,;:'s]*:book后面可以有若干右括号、引号、标点、复数s
  • ( |$):book后面,要么空格,要么行结尾

执行命令:

1
egrep "(^| )[\"[{(]*book[]})\"?!.,;:'s]*( |$)" bookwords

vi/ex编辑器提供专门单词边界元字符:\<单词开头,\>单词结尾;grep/sed不一定支持。

在这里替换(匹配范围,贪婪匹配)

grep只关心“整行是否匹配”;做替换的时候,要搞清楚到底哪一段文本被正则捕获。

贪婪匹配(正则默认行为)

A.*Z .*会尽可能吃最多字符,从第一个A,匹配到这一行最后一个Z,而不是最近的Z。 示例行: All of us, including Zippy and Ziggy and Zelda A.*Z会匹配从A一直到最后的Zelda的Z。

工具gres(非标准unix,简化sed,书中用来演示替换逻辑),脚本实现放在原文,等价sed替换。

限制范围(解决贪婪匹配)

问题场景:引号内文本。 文本:.Se "Appendix" "Full Program Listings"

  • 正则 ".*" 贪婪,会匹配从第一个引号一直到本行最后一个引号,把两个参数全部吞掉。
  • 非贪婪思路:排除引号的字符类 [^"]:匹配任意非引号字符。
  • 正确模式 "[^"]*":匹配一对引号内部最短内容。
1
gres '"[^"]*"' '00' sampleLine

另外案例:匹配至少5个点做引导符:

1
[0‑9][0‑9]*\.\{5,\}[0‑9][0‑9]*

sed替换命令示例:

1
sed 's/\([0‑9][0‑9]*\)\.\{5,\}\([0‑9][0‑9]*\)/\1‑\2/' sample

\( \)分组捕获,\1 \2反向引用分组内容,第五章详细讲解。

使用喜欢的元字符:表3‑4 常用正则速查表

项目 正则表达式
州邮政缩写 [A‑Z][A‑Z]
城市,州 ^.*, [A‑Z][A‑Z]
城市、州、邮编(egrep) ^.*, [A‑Z][A‑Z] [0‑9]{5}(-[0‑9]{4})?
月 日 年 [A‑Z][a‑z]\{3,9\} [0‑9]\{1,2\}, [0‑9]\{4\}
美国社保号 [0‑9]\{3\}-[0‑9]\{2\}-[0‑9]\{4\}
北美电话号码 [0‑9]\{3\}-[0‑9]\{4\}
美元金额 \$[ 0‑9]*\.[0‑9][0‑9]
troff字体请求 \\f([BIRP]C*[BW]*)
troff请求 ^\.[a‑z]\{2\}
troff宏 ^\.[A‑Z]\{2\}
带参数troff宏 ^\.[A‑Z]\{2\} .*".*"
HTML标签 <[^>]*>
Ventura样式代码 ^@.* = .*
空行 ^$
匹配整行 ^.*$
1个或多个空格 *

本章核心要点总结

  1. 两套正则语法分清
    • BRE基础正则:sed/grep;+ ? | () {} 需要加反斜杠转义。
    • ERE扩展正则:egrep/awk;直接写+ ? | () {}不用反斜杠。
  2. *不独立,只修饰前面单元;.*贪婪匹配,尽量匹配最长文本;想要最短匹配用否定字符类 [^xxx]。
  3. []字符类只匹配单个字符;^放方括号第一个才是排除;-放首尾代表字面减号。
  4. 锚点^行首、$行尾;不匹配字符,匹配位置;awk中^ $任何位置都是特殊符号。
  5. 反斜杠\:转义元字符;sed里面分组、次数大括号必须带\。
  6. 写正则三步:明确文本特征 → 写模式 → 必须测试;区分击中、遗漏、假警报。
  7. 默认按行匹配,正则不能直接跨换行;跨行需要sed高级多行功能。
  8. POSIX字符类[:digit:]这类,必须嵌套在[]中,例如[[:digit:]]。