正则表达式之一入撸门深似海(一)

作者 Zhendong Ho 日期 2018-08-16
正则表达式之一入撸门深似海(一)

定义

正则表达式(Regular Expression)是强大、便捷、高效的文本处理工具。正则表达式本身,加上如同一门袖珍编程语言的通用模式表示法(general pattern notation),赋予使用者描述和分析文本的能力。配合上特定工具提供的额外支持,正则表达式能够添加、删除、分离、叠加、插入和修整各种类型的文本和数据。


文件名模式

我们都知道,report.txt是一个文件名。但在Unix或者DOC/Windows中,能够使用“*.txt”来选择多个文件。在这种类型的文件名(称为“文件群组” file globs,或者“通配符” wildcards)中有些字符具有特殊的意义。星号表示“任意文本”,问号表示“任意单个字符”。所以文件群组“*.txt”以能够匹配字符的“*”符号开头,以普通文字“.txt”结尾。所以,它的意思是:选择任意文本开头,以.txt结尾的所有文件。这称为文件名模式(filename pattern)。


正则表达式“语言”

大多数系统都提供了少量的附加特殊字符*(additional special characters),但是总的来说,这些文件名模式的表达能力还远远不够。近几年来,一种“通用的模式语言”(generalized pattern language)已经发展起来,它功能强大,描述能力也很强,可以用来解决各种问题。不同的程序以不同的方式来实现和使用这种语言,这种强大的模式语言和模式本身被称为“正则表达式”(regular expression)。

完整的正则表达式由两种字符构成。特殊字符(special characters,例如文件名例子中的*)称为“元字符”,其他为“文字”,或者是普通文本字符。正则表达式与文件名模式的区别就在于,正则表达式的元字符提供了更强大的描述能力。文件名模式只为有限的需求提供了有限的元字符,但正则表达式“语言”为高级应用提供了丰富而且描述力极强的元字符。


检索文本文件:Egrep

文本检索是正则表达式最简单的应用之一,许多文本编辑器和文字处理软件都提供了正则表达式检索的功能。最简单的就是egrep。在指定了正则表达式和需要检索的文件之后,egrep会尝试用正则表达式来匹配每个文件的每一行,并显示能够匹配的行。

行的起始和结束

  • 脱字符『^』代表一行的开始
  • 美元符号『$』代表一行的结束
  • 正则表达式『cat』寻找的是一行文本中任意位置的cat。『^cat』寻找行首的cat,『cat$』寻找行末的cat。
  • 对正则表达式正确的理解:『^cat』匹配的是以c作为一行的第一个字符,紧接一个a,紧接一个t的文本。

字符组

如果我们需要搜索的单词是“grey”,同时又不确定它是否写作“gray”,就可以使用正则表达式结构体[…]。它可以列出使用者在某处期望匹配的字符,通常被称作字符组。如gr[ae]y中的[ae]。

  • 『e』匹配字符e,『a』匹配字符a,而正则表达式『[ea]』能匹配a或者e。

所以,gr[ea]y的意思是:先找到g,跟着是一个r,然后是一个a或者e,最后一个是y。

注意:在字符组以外,普通字符都有“接下来”的意思。gr[ae]y,首先匹配g,接下来匹配r。而字符组内的情况完全相反,字符组的内容是在同一个位置能够匹配的若干字符,所以它的意思是“”。

  • 『[Ss]mith』,这个表达式仍然能够匹配内嵌在其他单词里头的smith或者是Smith,例如blacksmith。
  • 字符组元字符‘-’(连接符)表示一个范围:『H[1-6]』与『H[123456]』是完全一样的。
  • 『[0-9]』和『[a-z]』是常用的匹配数字和小写字母的简便方式。例如,『[0123456789abcdefABCDEF]』可以写作『[0-9a-fA-F]』(或者也可以写作『[A-Fa-f0-9]』,顺序无所谓)。

注意:只有在字符组内部,连字符才是元字符,否则它就只能匹配普通的连字符号。『[0-9A-Z_!.?]』里面,真正的 特殊字符就只有那两个连字符。

排除型字符组

用『[^…]』取代『[…]』,这个字符组就会匹配任何未列出的字符,例如,『[^1-6]』匹配出了1-6以外的任何字符。这个字符组中开头的『^』表示“排除”,所以这里列出的不是希望匹配的字符,而是不希望匹配的字符。

注意:在字符组外部,^表示一个行锚点,但是在字符组内部(而且必须是紧接在字符组的第一个方括号之后),它就是一个元字符。

  • 输入命令:egrep ‘q[^u]’ word.list,结果如下

    Iraqi、Iraqian、miqra、qasida、qintar、qoph、zaqqum%,其中,”Iraq”和”Qantas”不包含在结果中。

注意:排除型字符组表示“匹配一个未列出的字符”,而不是“不要匹配列出的字符”,所以结果中没有Iraq。

  • 解答:

    为什么『q[^u]』无法匹配’Qantas’或者’Iraq’?

    Qantas无法匹配的原因是,正则表达式要求小写q,而Qantas中的Q是大写的。

    而Iraq,正则表达式要求q之后紧跟一个u以外的字符。通常行尾都有一个换行字符,但是有的egrep把这些换行符都去

    掉,所以在行尾没有能够匹配u以外的字符。

注意:一个字符组,即使是排除型字符组,也需要匹配一个字符。

用点号匹配任意字符

元字符『.』是用来匹配任意字符的字符组的简便写法。表达式中使用一个“匹配任何字符”的占位符,可以使用点号。

例如,03/19/76、03-19-76或者03.19.76,可以用一个明确的字符组构建正则表达式,如『03[-./]19[-./]76[-./]』。也可以用点号『03.19.76』。

  • 『03[-./]19[-./]76[-./]』中,点号并不是元字符,因为它在字符组中。连字符也不是元字符,因为它们都紧接在“[”或者“[^”之后。如果连字符不在字符组的开头,如『[.-/]』就是用来表示范围,但这样是错误的。

注意:在字符组的里面和外面,元字符的定义和意义是不一样的。一个重要但常见的的问题是,写正则表达式时,需要在对欲检索文本的了解程度与检索精确性之间求得平衡。要想正确使用正则表达式,清楚了解目标文本是非常重要的。

多选结构

『|』是一个非常简捷的元字符,意思是“”。它能把不同的子表达式组合成一个总的表达式,而这个总的表达式又能够匹配任意的子表达式。『Bob』和『Robert』是两个表达式,但『Bob|Robert』能够同时匹配任意一个。在这样的组合中,子表达式称为“多选分支”。

  • 『gr[ea]y』可以写作『grey|gray』或者是『gr(e|a)y』。但是『gr[e|a]y』不符合要求,|是和a与e一样的普通字符。

注意:『gr[ea]y』与『gr(e|a)y』中,字符组和多选结构的区别是,一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身都可能是完整的正则表达式,都可以匹配任意长度的文本。

字符组基本可以算是一门独立的微型语言(例如,对于元字符,它们有自己的规定),而多选结构是“正则表达式语言主体”的一部分。在一个包含多选结构的表达式中使用脱字符和美元符号的时候也要小心。例如『^From|Subject|Date: 』和『^(From|Subject|Date): 』。

忽略大小写

该功能并不是正则表达式语言的一部分,却是许多工具软件提供的有用的相关特性。egrep的命令行参数“-i”表示进行忽略大小写的匹配。把-i写在正在则表达式之前:

  • egrep -i ‘^(From|Subject|Date): ‘ mailbox

单词分界符

如果egrep支持“元字符序列”『\<』和『\>』,就可以使用它们来匹配单词分界的位置。可以把它们想象为单词版本的『^』和『$』,分别用来匹配单词的开头和结束位置。

  • 表达式『\<cat\>』的意思是“匹配的单词的开头位置,然后是cat,然后是单词的结束位置”。就是匹配cat这个单词。
  • 『\<cat』表示匹配以cat开头的单词,『cat\>』表示匹配以cat结尾的单词。

注意:『<』和『>』本身不是元字符,只有当它们与斜线结合起来的时候,整个序列才具有特殊意义。但是,并不是所有版本的egrep都支持单词分界符,而且不一定能认得出“英语单词”。“单词的起始位置”只不过是一系列字母和数字符号开始的位置,而“结束位置”就是它们结尾的地方。

小结

元字符 名称 匹配对象
. 点号 单个任意字符
[…] 字符组 列出的任意字符
[^…] 排除型字符组 未列出的任意字符
- 连字符 表示一个范围,在字符组内部有效
^ 脱字符 行的起始位置
$ 美元符号 行的结束位置
\< 反斜杠-小于 单词的起始位置
\> 反斜杠-大于 单词的结束位置
| 竖线 匹配分隔两边的任意一个表达式
(…) 括号 限制竖线的作用范围

另外还有几点需要注意:

  • 在字符组内部,元字符的定义规则和意义是不一样的。
  • 不要混淆多选项和字符组。字符组只能匹配一个字符,多选项可以匹配任意长度的文本,每个多选项可能匹配的文本都是独立的。
  • 排除型字符组是表示所有未列出字符的字符组的简便方法。
  • -i参数规定在匹配时不区分大小写。

可选项元素

元字符『?』代表可选项。把它加在一个字符的后面,就表示此处容许出现这个字符。如『colou?r』可以用来匹配color和colour。『u?』是必然能够匹配成功的,有时它会匹配一个u,其他时候则不匹配任何字符。

  • 不是任何包含?的正则表达式都永远能匹配成功,如『colou?r』无法匹配semicolon。
  • 『(July|Jul) (fourth|4th|4)』可以写成『July? (fourth|4(th)?)』。

其他量词:重复出现

『+』和『*』的作用与问好类似。元字符『+』表示“之前紧邻的元素出现一次或多次”,而『*』表示“之前紧邻的元素出现任意多次,或者不出现”。

  • 『?』、『+』、『*』这三个元字符,统称为量词,因为他们限定了所作用元素的匹配次数。
  • 与『…?』一样,正则表达式中的『…*』永远不会匹配失败,区别只在于它们的结果。而『…+』在无法进行任何一次匹配时,就会报告匹配失败。
  • 匹配<HR SIZE=14>的HTML Tag。正则表达式为『<HR +SIZE *= *14 *>』,替换14『<HR +SIZE *= * [0-9]+ *>』。

“表示重复的元字符”含义小结

次数下限 次数上限 含义
? 1 可以不出现,也可以只出现一次(单词可选)
* 可以出现无数次,也可以不出现(任意次数均可)
+ 1 可以出现无数次,但至少要出现一次(至少一次)

规定重复次数的范围:区间

某些版本的egrep能够使用元字符来自定义重现次数的区间:『…{min,max}』。这称为区间量词。

  • 『…{3,12}』能够容许的重现次数在3到12之间。

括号和反响引用

反向引用是正则表达式的特征之一,它容许我们匹配与表达式先前部分匹配的同样的文本。

  • 『\\』中,第一个the替换为能够匹配任意单词的正则表达式『[A-Za-z]+』,然后在两端加上括号;把后一个the替换为特殊的元字符序列『\1』,就得到了『\<([A-Za-z]+) +\1\>』。
  • 在支持反向引用的工具软件中,括号能够“记忆”其中的子表达式匹配的文本,不论这些文本是什么,元字符序列『\1』 都能记住它们。
  • 在一个表达式中我们可以使用多个括号。再用『\1』、『\2』、『\3』等来表示第一、第二、第三组括号匹配的文本。如,『([a-z])([0-9])\1\2』,『\1』代表『[a-z]』匹配的内容,『\2』代表『[0-9]』匹配的内容。

神奇的转义

如果需要匹配的某个字符本身就是元字符,如ega.att.com,则可以利用反斜线加上点号的组合:ega\.att\.com。『\.』称为“转义的点号”或者“转义的句号”,这样的办法适用于所有的元字符,不过在字符组内无效。

  • 这样使用的反斜线称为“转义符”,它作用的元字符会失去特殊含义,成了普通字符。
  • 『\([a-zA-z]+\)』可以匹配(very)。

基础知识扩展

更多的例子

编写正则表达式时,按照预期获得成功的匹配要花去一半的功夫,另一半的功夫用来考虑如何忽略那些不符合要求的文本。

变量名

许多程序设计语言都有标识符的概念。标识符只包含字母、数字以及下划线,但不能以数字开头。我们可以用『[a-zA-Z_][a-zA-Z_0-9]』来匹配标识符。第一个字符组匹配可能出现的第一个字符,笫二个字符组(包括对应的『\』)匹配余下的字符。如果标识符的长度有限制,例如最长只能是32个字符,又能用区间量词{min,max},我们可以用『{0,31}』来替代最后的『*』。

引号内的字符串

匹配引号内的字符串最简单的办法是使用这个表达式:『”[^”]*”』。

两端的引号用来匹配字符串开头和结尾的引号。在这两个引号之间的文本可以包括双引号之外的任何字符。所以我们用『[^”]』来匹配除双引号之外的任何字符,用『*』来表示两个引号之间可以存在任意数目的非双引号字符。

关于引号字符串,更有用也更复杂的定义是,两端的双引号之间可以出现由反斜线转义的双引号。

美元符号(可能包含小数)

『\$[0-9]+(\.[0-9][0-9])?』是一种匹配美元金额的办法。

大致可以理解为:一个美元符号,然后是一组字符,最后可能还有另一组字符。这里的“字符”指的是数字(一组数字构成一个数值),“另一种字符”是由一个小数点和两位数字构成的。

HTTP/HTML URL

常见的HTTP/HTML URL是这样的:

http://hostname/path.html

  1. hostname(主机名,例如www.yahoo.com)的规则比较复杂。但是一般跟在"http://"后面的就有可能是主机名,所以正则表达式为『[-a-z0-9_.]+』。
  2. path部分的变化更多,于是『[-a-z0-9_:@&?=+,.!/~*%$]*』。注意连字符必须放在字符组的开头,保证它是一个普通字符,而不是用来表示范围。
  3. egrep -i ‘\\http://[-a-z0-9\_.:]+/[-a-z0-9_:@&?=+,.!/~\*%$]\*\\.html?\\‘ files
  4. 降低了对匹配的要求,’http://…./foo.html’也能匹配,正则表达式为:egrep -i ‘\<http://[^ ]*\.html?\>’ files。

表示时刻的文字

  • 例如“9:17 am”或者“12:30 pm”,匹配表示时刻的文字可能有不同的严格程度。

    『[0-9]?[0-9]:[0-9][0-9] (am|pm)』能够匹配9:17 am或者12:30 pm,但也能匹配无意义的时刻,如99:99 pm。

  1. 把小时数分为两种情况处理。『1[012]』匹配两位数,『[1-9]』匹配一位数。综合起来就是『(1[012]|[1-9])』。
  2. 分钟数就简单些。第一位数应该是『[0-5]』,第二位是『[0-9]』。综合起来是『(1[012]|[1-9]):[0-5][0-9] (am|pm)』。

正则表达式术语汇总

正则(regex)

“正则表达式”(regular expression)这个全名念起来有点麻烦,所以一般采用“正则”(regex)的说法。

匹配(matching)

一个正则表达式“匹配”一个字符串,其实是指这个正则表达式能在字符串中找到匹配文本。严格地说,正则表达式『a』不能匹配cat,但是能匹配cat中的a。

元字符(metacharacter)

一个字符是否元字符(或者是“元字符序列”,这两个概念是相等的),取决于应用的具体情况。例如,只有在字符组外部并且是在未转义的情况下,『*』才是一个元字符。“转义”的意思是,通常情况下在这个字符之前有一个反斜线。『\*』是对『*』的转义,而『\\*』则不是(第一个反斜线用来转义第二个反斜线)。

流派(flavor)

不同的工具使用不同的正则表达式完成不同的任务,每样工具支持的元字符和其他特性各有不同。“流派”这个词用来描述所有这些细微的实现规定。

  • “流派”和“工具”是两个不同的概念。两个人可以说同样的方言,两个完全不同的程序也可能属于同样的流派。同样,两个名字相同的程序所属的流派可能有细微的差别。有许多程序都叫egrep,它们所属的流派也五花八门。

子表达式(subexpression)

“子表达式”指的是整个正则表达式的一部分,通常是括号内的表达式,或者是由『|』分隔的多选分支。

  • 例如,在『^(Subject|Date): 』中,『Subject|Date』通常被视为一个子表达式。其中的『Subject』和『Date』也算得上是子表达式。而且,严格来说,『S』,『u』,『b』,『j』这些字符,都算子表达式。
  • 1-6这样的字符序列并不能算『H[1-6] *』的子表达式,因为‘1-6’所属的字符组是不可分割的单元。但是,『H』、『[1-6]』、『 *』都是『H[1-6] *』的子表达式。

字符(character)

“字符”在计算机领域是一个有特殊意义的单词。一个字节所代表的单词取决于计算机如何解释。单个字节的值不会变化,但这个所代表的字符却是由解释所用的编码来决定的。


总结

egrep的元字符总结

匹配单个字符的元字符

元字符 匹配对象
. 点号 匹配单个任意字符
[…] 字符组 匹配单个列出的字符
[^…] 排除型字符组 匹配单个未列出的字符
\char 转义字符 若char是元字符,或转义序列无特殊含义时,匹配char对应的普通字符

提供计数功能的元字符

元字符 匹配对象
? 问号 容许匹配一次,但非必须
* 星号 可以匹配任意多次,也可能不匹配
+ 加号 至少需要匹配一次,至多可能任意多次
{min,max} 区间量词 至少需要min次,至多容许max次

匹配位置的元字符

元字符 匹配对象
^ 脱字符 匹配一行的开头位置
$ 美元符 匹配一行的结束位置
\< 单词分界符 匹配单词的开始位置
\> 单词分界符 匹配单词的结束位置

其他元字符

元字符 匹配对象
| alternation 匹配任意分隔的表达式
(…) 括号 限定多选结构的范围,标注量词作用的元素,为反向引用“捕获”文本
\1,\2 反向引用 匹配之前的第一、第二组括号内的字表达式匹配的文本
  • 各个egrep程序是有差别的。它们支持的元字符,以及这些元字符的确切含义,通常都有差别。
  • 使用括号的3个理由是:限制多选结构、分组和捕获文本。
  • 字符组的特殊性在于,关于元字符的规定是完全独立于正则表达式语言“主体”的。
  • 多选结构和字符组是截然不同的,它们的功能完全不同,只是在有限的情况下,它们的表现相同。
  • 排除型字符组仍然需要匹配一个字符。
  • -i的参数很有用,它能进行忽略大小写的匹配。
  • 转义有三种情况:
    1. 『\』加上元字符,表示匹配元字符所使用的普通字符,例如『\*』匹配普通的星号。
    2. 『\』加上非元字符,组成一种由具体实现方式规定其意义的元字符序列,例如『\<』表示“单词的起始边界”。
    3. 『\』加上任意其他字符,默认情况就是匹配此字符,也就是说,反斜线被忽略了。
  • 由星号和问号限定的对象在“匹配成功”时可能并没有匹配任何字符。即使什么字符都匹配不到,它们仍然会报告“匹配成功”。