blog / gadflysu

希望與熱烈的風
Talk is Cheap

  1. 1. 基本结构
  2. 2. 方括号表达式与字符类
  3. 3. 匹配空串的特殊表达式
  4. 4. 反向引用与子表达式

正则表达式 (Regular Expression, regex, regexp) 使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。

A regular expression is a pattern that describes a set of strings.

常用的工具 grep 中的「re」就是指正则表达式,Ken Thompson 在 44 年前 (Nov. 1974) 将此符号系统引入 grep。正则表达式的 POSIX 规范,分为基本型正则表达式 (Basic Regular Expression, BRE) 和扩展型正则表达式 (Extended Regular Express, ERE)。顾名思义,后者会更强大。在 GNU 实现的 grep 中,基本语法和扩展语法的可用功能没有区别。Perl 兼容正则表达式 (Perl Compatible Regular Expressions, PCRE) 支持更多的功能,它是由 Philip Hazel 在上个世纪末 (1997) 开发的一个正则表达式引擎,实现与 Perl 5 一致的语法和语义,被用于许多现代的著名项目,如 Apache、PHP、KDE、Postfix、Nmap 以及 Apple Safari。

基本结构

我们暂且不管形式语言自动机之类的理论,先来看一个简单的例子:hello*。这个正则表达式是含有 6 个字符的字符串,它匹配 hellohelloo 以及 hellooo 等等无穷多个以「hello」开头、后接 0 个或多个「o」的字符串。

正则表达式的基本结构是匹配一个字符的模式 (pattern)。大部分普通的字符,包括所有的字母和数字,都匹配它们自身,如 h 匹配 he 匹配 e

一些有特殊功能的字符,如上例中表示匹配前面的子表达式 (o) 任意次的 *,叫作元字符 (meta-character)。若要匹配元字符自身,则需要一个前缀的反斜杠 (\, backslash) 进行转义 (escape),如 \* 匹配一个星号 (*, asterisk)。

元字符有:^ $ ( ) * + ? . [ \ { |

句点 . 是元字符,匹配除回车符 (carriage return, CR) 和换行符 (newline / line feed, LF) 之外的任意的单字符。例如,w.ll 能匹配 willwell

一个正则表达式可后接像 * 这样的表达式,表示重复操作 (repetition)。详见下表:

表达式 描述
* 匹配前面的子表达式 0 次或更多次。
? 匹配前面的子表达式 0 次或 1 次。
+ 匹配前面的子表达式 1 次或更多次。
{n} 匹配前面的子表达式恰好 n 次。
{n,} 匹配前面的子表达式至少 n 次。
{n,m} 匹配前面的子表达式至少 n 次、至多 m 次。

从上表可见重复操作不一定由单个字符完成,更复杂的操作可能需要成对出现的花括号 ({ & }, curly braces) 以及数字。* 等价于 {0,}? 等价于 {0,1}+ 等价于 {1,}。此外 GNU grep 中还支持 {,m},表示匹配至多 m 次,这是 GNU 的扩展 (extension)。

两个正则表达式可以直接连接,表示串接操作 (concatenation),其结果匹配任何由分别匹配原表达式的字符串串接而得的字符串。为了表达得严谨些,看起来有点绕,举个例子来看就会很简单:正则表达式 ab 串接的结果 ab,匹配由字符串 ab 串接而得的字符串 ab。显然,这种操作是基本而广泛的。

两个正则表达式可以用竖线符 | 连接起来,表示选择操作 (alternation),其结果匹配两个表达式中的任意一个。举个例子:a|b 匹配 ab。(没错,这和或运算 (or) 的中缀表示法一致。)

重复操作优先于串接操作,串接操作优先于选择操作。例如,hi* 能匹配 hii 而不能匹配 hihifuc|nk 能匹配 fuc 而不能匹配 funk

整个表达式可以使用圆括号 (( & ), parentheses) 括起来,形成一个新的子表达式 (subexpression),其优先级比前述三种操作都更高。例如,(hi)* 能匹配 hihifu(c|n)k 能匹配 funk

空的正则表达式匹配空字符串 ``。

方括号表达式与字符类

除了前文介绍的花括号和圆括号,还有一种常见的括号——方括号 ([ & ], (square) brackets)。 由方括号括起来的一系列字符叫作方括号表达式 (bracket expression),匹配方括号内字符序列中的任意一个字符。例如,[fox] 匹配 fo 或者 x,等价于 f|o|x

若序列中第一个字符为脱字符 (^, caret),则匹配排除序列中字符的任何字符。例如,[^cat] 则无法匹配 cat 但能匹配 d0g 等等。

方括号内仍有特殊功能的元字符仅余用于转义的反斜杠 \ 以及在第一位的脱字符 ^例如,[(fu)*k] 能匹配 (fu)* 或者 k

在方括号表达式中,由两个字符及其之间的连字符 (-, hyphen) 组成的字符串叫作字符范围表达式 (range expression),匹配在字符集中处于这两个字符之间范围的任意一个字符。还是举例子比较简单,对于一般的字符编码,[a-z] 匹配任意一个小写字母,[0-9] 匹配一位阿拉伯数字。所以,[A-D] 等价于 [ABCD]A|B|C|D

典型的 8 位编码字符集:ASCII

此外,方括号表达式中预定义了一些命名字符类 (named character class),表示一系列特定的字符。详见下表:

类名 描述
[:lower:] 小写字母字符,同 [a-z]
[:upper:] 大写字母字符,同 [A-Z]
[:alpha:] 字母字符,即 [:lower:][:upper:],同 [A-Za-z]
[:digit:] 数字字符,同 [0-9]
[:xdigit:] 十六进制数字字符,同 [A-Fa-f0-9]
[:alnum:] 字母和数字字符,即 [:alpha:][:digit:],同 [0-9A-Za-z]
[:blank:] 空白符,指空格 (space, SP) 和制表符 (horizontal tab, HT),同 [ \t]
[:space:] 空格符,指制表符 (HT)、换行符 (LF)、垂直制表符 (vertical tab, VT)、换页符 (form feed, FF)、回车符 (CR) 和空格 (SP)
[:punct:] 标点字符,有:! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
[:graph:] 有形字符,即 [:alnum:][:punct:]
[:print:] 可打印字符,即 [:alnum:][:punct:] 和空格 (SP)
[:cntrl:] 控制字符,在 ASCII 中指从十进制的 0 到 37,以及 177 (DEL)

注意上表首列中的中括号是类名的一部分,含有命名符号类的正则表达式应该形如 [[:name:]]例如,正则表达式 [[:lower:]01] 匹配所有小写字母,以及两个数字 01

这里提及一个有趣的问题:我们知道 [[:digit:]] 等价于 [0-9],那么如何写一个等价的一般的方括号表达式以在 shell 中使用 GNU grep 匹配 [:punct:] 这样一个复杂的符号类?这个问题主要是将有特殊功能的字符表示为普通字符,涉及 shell 中的引号使用以及中括号内的字符顺序。答案是:

1
2
3
4
5
6
# all shells, single quote is outside the quotes
grep '[][!"#$%&'\''()*+,./:;<=>?@\^_`{|}~-]' file
# all shells, single quote is inside double quotes
grep '[][!"#$%&'"'"'()*+,./:;<=>?@\^_`{|}~-]' file
# ksh, bash, and zsh only, does not expand variables
grep $'[][!"#$%&\'()*+,./:;<=>?@\\^_`{|}~-]' file

我们在 shell 中通常用单引号来引用正则表达式,因为单引号最大限度地避免扩展 (expansion),保留特殊字符的字面值 (literal value),如 $ 仅仅是美元符 (dollar sign) 而非变量标志。前文说方括号内的元字符并未提到方括号 ([ & ]),也就是说 [[] 匹配左方括号,[]] 匹配右方括号。这是没错的,虽然看起来有点不合常理的机智,因为一般而言 ] 不是标志着这个子表达式的终结吗?——类似于「在第一位的 ^ 代表排除式内字符」的规定,特别地,方括号内在第一位的 ] 匹配自身,而非结束方括号表达式。所以若要匹配 ],必须将它放在第一位;正如若要匹配 ^,不得将它放在第一位。除此二之外,更多规则详见下表:

模式 描述
[. 表示 collating symbol 的开始
.] 表示 collating symbol 的结束
[= 表示 equivalence class 的开始
=] 表示 equivalence class 的结束
[: 表示命名字符类的开始,后跟有效类名
:] 表示命名字符类的结束
- 表示字符范围,当在非首位且非末位时

匹配空串的特殊表达式

反斜杠符 \ 作为转义字符(港台称「跳脫字元」),后跟一些特定的字符,表示一些特殊的模式匹配。

模式 描述
\b 匹配单词边缘的空字符串
\B 匹配非单词边缘的空字符串
\< 匹配单词开头位置的空字符串
\> 匹配单词末尾位置的空字符串
\w 匹配单词成分,等价于 [_[:alnum:]]
\W 匹配非单词成分,等价于 [^_[:alnum:]]
\s 匹配空白符,等价于 [[:space:]]
\S 匹配非空白符,等价于 [^[:space:]]

其中「单词边缘」?「空字符串」?不太好理解。

举个例子,字符串 self-censorship 的每对相邻字符 (eg. s e)之间以及第一个 s 的左边、末尾 p 的右边的位置上,可以看作存在一个(或任意个)「空字符串」``。

那么位于第一个 sc 的左边、fp 的右边,也就是「单词边缘」的位置上的那个空串,能够由 \b 匹配。于是 \bs. 能匹配该串中的 se 而不匹配 sosh。所谓单词边缘就是指一个其左右两个字符一个属于单词成分 (\w)、一个不属于单词成分的位置。于是不难理解 \B\<\> 应匹配什么位置的空串。

匹配特定空串实际上就是匹配位置。

脱字符 ^ 匹配行首的空串,美元符 $ 匹配行末的空串。这两个字符被称为锚 (anchor),因为它们分别被锚定到行首和行末。例如,一文本文件有如下字符:

1
Hip-Hop

^H.p 匹配该行内的 HipH.p$ 匹配该行内的 Hop

反向引用与子表达式

反向引用 (back-reference) 的语法为 \n,其中 n 是一位数字。\n 引用了正则表达式中第 n 个由圆括号形成的子表达式 (parenthesized subexpression)。例如,(f)\1 相当于 (f)(f),匹配 ff

当被引用的子表达式被用于选择操作 (|) 中,假如该子表达式所在的部分不参与匹配,则整个匹配失败。例如,a(.)|b\1 无法匹配 ba。GNU grep 给出的错误是「Invalid back reference」。

当有多个正则表达式进行匹配(比如 grep 使用 -e <PATTERN>-f <FILE> 参数)时,向后引用的编号 (n) 是对于每一个正则表达式而言的。


以上是「正则表达式」的基本内容。

📖 [Ref] GNU grep docs

Author : gadflysu
本文采用「知识共享署名 - 非商业性使用 - 相同方式共享 4.0 国际许可协议 (CC BY-NC-SA 4.0)」进行许可。你可自由分享演绎,惟须遵照:署名非商业性使用相同方式共享不得增加额外限制
Link to this article : https://blog.gadflysu.com/general/regular-expression/

This article was last updated on days ago, and the information described in the article may have changed.