不只是在开发中,在一般的文件搜索中,我们总是会遇到需要匹配/查找/替换某一类文字的场景,正则表达式在这时是非常有用的。
而长时间以来,在畏难心理的作用下,自己一直处于被正则表达式的强大支配的恐惧下,总是担心hold不住它,每次要用到就很紧张。但是,并不希望这种状态一直延续下去,所以下定决心好好学习一下正则,参考书是Jeffrey E.F. Friedl的《精通正则表达式》。
正则表达式,这个名称我一直觉得好拗口,简单说来,它的用途就是 描述一串文本的特征。
完整的正则表达式由两种字符组成:
- 元字符(metacharacters) - 元字符是具有特殊意义的字符,它的定义在正则表达式中并不是统一的(在下文的例子中将看到),它的含义取决于具体的情况。了解具体情况(包括但不限于正则表达式,比如还有shell, 字符串等)中元字符及其作用是非常重要的。
- 文字(普通文本字符, literal/normal text characters)
正则表达式由小的模块(building block unit)组成,每个模块由元字符或文本字符构成,每个单独的模块都很简单,但是它们有无穷多的组合方式,由此为正则表达式提供了强大、灵活的处理问题的能力。(至于如何将它们组合起来实现特定的目标则必须依靠不断实践积攒的经验了。)
先吃栗子
这几个栗子可以用于匹配或替换一些文本,先来直观感受一下正则表达式长什么样子:
- 匹配一个空行
^$
- 匹配变量名
[a-zA-Z_][a-zA-Z_0-9]*
- 匹配引号内的字符串
("|')[^\1]*\1
- 匹配带正负号的浮点数
[+-]?[0-9]+(\.[0-9]*)?
- 匹配以com,edu,info的主机名(假设主机名可以包括连字符,数字和字母)
[-a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*\.(com|edu|info)
- 为数值添加逗号,例如将”1234567”替换为”1,234,567”(以Perl语言为例)
s/(?<=\d)(?=(?:\d\d\d)+(?!\d))/,/g
上面这些栗子看起来似乎有些让人头晕目眩,一堆乱七八糟的符号凑在一起是要怎样?不要着急,接下来我们就来一一解释这些符号的含义及用法。
先来看最基本的元字符。
常见元字符
注:本篇中提到的元字符(除特殊标除外)大部分以egrep(Extended Global Search Regular Expression and Print out the line)为例,只是部分常用的元字符介绍,并没有(也并不能)包含全部的元字符。
不同的正则表达式的流派(flavor)所支持的元字符以及元字符的确切含义是有一定差别的,在使用时请参考相应的文档。 这一点请特别注意,通常我们也是这正是这些定义和使用方式间琐碎细微的差别造成了“正则表达式很难很复杂”的表象。但其实这一点跟sql有点像,它们的核心都是一样,只是各自有自己的方言,了解了核心再在使用时按照具体的文档稍作调整,就不会感到那么晕头转向了。
字符表示法
转义
如果要匹配的某个字符本身就是元字符,通过在元字符前面加上\
表示转义。举个例子,若要匹配”www.google.com",则"."作为元字符需要进行转义,正则表达式的写法应该是这样`www\.google\.com`。
字符缩略表示法
主要针对一些难以输入或观察的字符,例如:
\t
水平制表符(HT)\v
垂直制表符(VT)\n
换行符(LF/CR[MacOS])\r
回车符(CR/LF[MacOS])\e
Escape字符(ESC)
尤其是想\n
,\r
这种在不同操作系统中代表的含义不同的字符,在使用时须格外小心。通常使用\n
作为最最通用的“换行符”。
八进制转义
\num
使用八进制转义可以以2-3位数字表示该值所代表的字节或字符。使用八进制转义可以方便的在正则表达式中插入平时难以输入的字符。
举几个栗子:
[\015\012]
可用于表示ASCII中的CR/LF序列。- awk中不支持
\e
但支持八进制转义,则可以直接使用其ASCII码\033
表示escape字符。
十六进制/Unicode转义
\xnum
,\x{num}
,\unum
,\Unum
左边的四种形式通常表示十六进制转义或Unicode转义,具体的形式不同语言或工具的实现也不同,须参照具体的说明文档使用。
举个栗子
\x0D\x0A
则是CR/LF的十六进制转义表示法\u4f60\u597d\uff0c\u4e16\u754c
是中文字符串“你好,世界”的Unicode转义表示。通常Unicode的匹配格式以\uFFFF
表示,即\u
后面接4位十六进制字符
控制字符
\cX
很多流派还支持使用\cX
匹配的控制字符,例如\cH
匹配Ctrl-H,即ASCII中的退格符,\cM
匹配回车
字符组及相关结构
普通字符组
[···]
匹配列出的字符中的一个。例如 ‘gr[ae]y’ 可以匹配gray或者grey[^···]
匹配一个未列出的字符。这里值得注意的一点是,作为字符组,即使是排除型字符组,它也是需要匹配一个字符的。因此有可能出现如下情况:假如我们想找出q后面的字母不是u的单词,使用正则表达式’q[^u]’来匹配,而刚好有个单词Iraq处于行尾,那么这个单词中的q后面米有能够匹配u以外的字符,它将不会被搜索出来。
另外,在这里我们还可以看到,
^
在字符组中(而且必须是紧接在字符组的左括号之后)的含义与它作为行起始锚点的含义完全不同。
-
连字符用在字符组内部可表示一个范围。如 ‘[1-6]’ 等价于 ‘[123456]’ 。- 同上,
-
也只有在字符组内部并且不是处于第一个字符的情况下才表示范围,否则它就是一个普通字符。
- 同上,
一些常用字符组举例:
[0-9]
数字[a-z]
小写字母[A-Z]
大写字母[0-9a-fA-F]
多重范围也是容许的,例如本例可用于匹配十六进制数字(其中0-9,a-f,A-F的顺序无所谓)
任意字符 - 点号
.
点号被用来匹配任意字符。但在有些工具中.
被用来匹配 除了换行符外 的任意字符。
注意:在字符组里面和外面,元字符的定义和意义是不一样的。例如,在字符组 ‘[-./]’ 中的连字符和点号都不是元字符,它们在这里就是普通字符。而在正则表达式’2017.09.30’中的点号就是表示任意字符的元字符了,它可以匹配如”2017/09/30”,”2017-09-30”,”2017a09b30”这些字符串。
写正则表达式时我们要在对被检索文字的了解程度和检索精确性之间求得平衡。例如在我们确切知道某些情况一定不会发生时,可以采用更笼统但是更易理解更简洁的正则表达式。因此,要想正确使用正则表达式,清楚地了解目标文本是非常重要的。
字符组的缩略表示法
相比于egrep, 很多其他流派的正则表达式提供了很多有用的缩略表示
\s
任何空白字符(空格、制表符、进纸符等)\S
除\s
之外的任何字符\w
ASCII字母和数字、下划线,有些系统中还包括一些非ASCII字母\W
除\w
之外的任何字符\d
[0-9]
,即数字\D
除\d
之外的任何字符,即[^0-9]
注:可以看到上面三对大小写字母\s\S
, \w\W
, \d\D
正好表示互斥的两组字符集合。
Unicode属性:字母表和区块
Unicode除了定义了一套字符映射规则外,还定义了每个字符的性质,比如大小写,书写顺序,是否标记字符等等。不同的正则表达式实现对这些属性的支持不同。但是很多都支持使用\p{quality}
和\P{quality}
支持其中的一部分。例如\p{L}
表示字母,L在这里表示letter。
以下是一些基本的Unicode属性分类和子属性举例
\p{L}
\p{Letter}
,字母\p{Ll}
\p{Lowercase_Letter}
, 小写字母\p{M}
\p{Mark}
,不能单独出现,必须与其他基本字符一起出现的字符(如重音符号等)\pMn
\p{Non_Spacing_Mark
,用于修饰其他字符的字符,例如重音符号、变音符号、语调标记等\p{N}
\p{Number}
,任何数字字符\p{Nd}
\p{Decimal_Digit_Number}
,各种字母表中从0-9的数字,不包括中日韩文- ……
Unicode定义了许多可以通过\p{···}
结构访问的属性,其中包括字符的书写顺序、与字符相关的元音以及其他属性。请参考具体的程序的说明文档了解细节!
字母表,有的系统能够按照字母表的名称\p{···}
来匹配。例如\p{Hebrew}
匹配希伯来文独有的字符。
区块,类似(但不如)字母表,表示Unicode字符映射表中一定范围内的代码点。例如Tibetan区块表是从U+0F00-U+0FFF的256个代码点,其中的字符在Java中可以用\p{InTibetan}
来匹配。
- 区块有很多种,包括对应大多数书写系统(拉丁语、希伯来等)的区块和特殊的字符组型(货币、箭头、文本框等)
- 区块可能包含未赋值的代码点
- 区块通常包含不相干的字符
- 属于某个字母表的字符可能同时包含于多个区块
位置匹配
以下元字符匹配的是一个位置,而不是具体的文本
起始与结束
^
行的起始$
行的结束\<
\>
用于匹配单词分界位置(在egrep中)\b
在其他流派中应用更普遍的是这个,用作单词分界,但并不区分起始和结束。例如\bcat\b
就只会匹配在”The cat wants to catch the mouse”中的第一个”cat”而不会匹配”catch”中的”cat”。
零长度断言(Zero-Width Assertion)
最开始看到一个词叫 零宽断言,第一反应就是什么鬼,后来看到有翻译成 零长度断言,再结合它的含义想想就很容易理解了。零长度,也就是不占字符,只匹配一个位置。
(?=···)
Positive Lookahead,从左往右看,子表达式能匹配 右侧 文本。- 例如
(?=\d)
,它表示如果当前位置的右边是一个数字,则匹配成功。
- 例如
(?!···)
Negative Lookahead,从左往右看,子表达式不能匹配 右侧 文本(?<=···)
Positive Lookbehind,从右往左看,子表达式能匹配 左侧 文本。- 例如
(?<=\d)
,它表示如果当前位置的左边是一个数字,则匹配成功。
- 例如
(?<!···)
Negative Lookbehind,从右往左看,子表达式不能匹配 左侧 文本。
注:lookahead和lookbind统称为lookaround。
举个栗子,假如我们要将”Janes”替换成”Jane’s”,有如下几种写法(以Perl的语法为例):
/\bJanes\b/Jane's/g
最简单直接搞笑的方法,正则表达式占用整个”Janes”/\b(Jane)(s)\b/$1'$2/g
单纯的增加复杂度的方法,无任何好处,占用整个”Janes”/(?<=\bJane)(?=s\b)/'/g
使用lookaround匹配出”‘“需要插入的位置,表达式不“占用”任何文本/(?=s\b)(?<=\bJane)/'/g
与上一个表达式完全相同,只是颠倒了lookaround的顺序,但由于它并没有占用任何字符,所以变换顺序没有影响。
此时,再来看本篇开头给出的栗子中的在数值中添加逗号的正则表达式s/(?<=\d)(?=(?:\d\d\d)+(?!\d))/,/g
,应该可以看懂了吧~
分组/捕获、条件判断和控制
分组/捕获
(···)
在上文的介绍中,我们已经多次使用到了括号。这里对括号的作用做个简单的概括,主要有一下三点:- 限制多选结构(多选结构可以包括很多字符,但不能超越括号的界限)
- 分组(如将若干字符组合为一个单元,受量词的作用)
- 捕获文本(反向引用)
虽然括号用的非常普遍,但有的情况我们只想用括号分个组,并不希望捕获文本,那么就可以使用非捕获型括号避免不必要的功能。
(?:···)
非捕获型括号,只用于分组,不可捕获。- 可避免不必要的捕获操作,提高了匹配效率
- 总的来说,根据情况选择合适的括号能够使程序更清晰,看代码的人不会被括号的具体细节困扰。(虽然说
(?:···)
这种表示法稍微增加了一点阅读难度,但从表达式的意义上来说是会更清晰)
反向引用
反向引用是正则表达式的特性之一,它容许我们匹配与表达式先前部分匹配的同样的文本。
举个例子,‘([a-z])([0-9])\1\2’,在这个表达式中,\1
代表[a-z]
匹配的内容,\2
代表[0-9]
匹配的内容。
1 | $ grep -E -i '\<([a-z]+) +\1\>' test.txt |
上面这行代码就可以在忽略大小写的模式下匹配出指定文件中的重复单词。不过由于egrep把每行文字作为一个独立部分看待,如果重复单词的第一个单词在行末,第二个在下一行行首,则这个表达式无法找到。因此我们还需要更强大的工具。
多选分支
|
多选分支,“或”,通过|
把各个子表达式组合起来获得的总表达式表示匹配任意子表达式。例如 ‘gray|grey’,‘gr(a|e)y’ 都可以表示匹配gray或者grey。- 相比于字符组只能匹配目标文本中的单个字符,多选分支的每个子结构都可能是完整的正则表达式,都可以匹配任意长度的文本。
量词
?
出现零次或一次。例如匹配color和colour可以用 ‘colou?r’ 匹配。+
出现一次或多次。*
出现零次或任意多次。{min, max}
容许指定的表达式重复的次数在min和max之间
贪婪与懒惰
之前提到的量词*+?
默认都采取贪婪模式,即尽可能多的进行匹配。另一种情况则是尽可能少的匹配,称之为懒惰模式。懒惰模式的量词如下表示:
??
出现零次或一次,但尽可能少重复+?
出现一次货多次,但尽可能少重复*?
出现任意次,但尽可能少重复{min, max}?
出现min到max次,但尽可能少重复
上文所涉及的只是正则表达式中部分常用元字符相关的基础内容,欲知更多,且看下回分解。
参考资料
- 精通正则表达式 Mastering Regular Expressions (3rd Edition).Jeffrey E.F. Friedl