正则表达式,又称规则表达式、常规表示法(Regular Expression,简写为regex、regexp或RE),是计算机科学的一个概念。正则表达式是对字符串操作的一种逻辑公式,用事先定义好的一些特定字符及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑。通常用于查找、替换符合特征的字符串,或者用来验证某个字符串是否符合指定的特征。
正则表达式最初的想法源于1940年,神经学家沃伦·麦卡洛克(Warren McCulloch)和数学家沃尔特·皮茨(Walter Pitts)在对人类神经系统如何工作的早期研究中,研究出的一种数学描述方式。1951年,数学家斯蒂芬·克莱尼(Stephen Cole Kleene)在发表的《神经网事件的表示法》论文中首次提出了正则表达式的概念。1968年,unix操作系统之父肯尼斯·蓝·汤普森(Kenneth Lane Thompson)将正则表达式应用到了UNIX的QED编辑器的搜索算法中。到现在为止,正则表达式已经成为文本编辑器和搜索工具的一个重要部分,并被应用到各种编程语言中。
正则表达式主要有POSIX标准和PCRE流派。不同的流派支持的元字符和这些元字符代表的意义存在着细微的差异,元字符的类型有转义符、字符组、分组、匹配量词、锚点和零宽断言,以及多选结构和嵌入条件等。正则表达式可应用于各种编程语言和文本处理工具中,在数据科学、文本分析、网络爬虫、字符串搜索和替换等领域都有广泛的应用。
发展历史
起源
正则表达式最初的想法源于1940年,神经生理学家沃伦·麦卡洛克(Warren McCulloch)与数学家沃尔特·皮茨(Walter Pitts)研究出了一种用数学化的方式来描述神经网络的模型,他们将神经系统中的神经元描述成小而简单的自动控制元。1951年,美国数学家斯蒂芬·克莱尼(Stephen Cole Kleene)在麦卡洛克和皮茨早期工作的基础上,发表了一篇标题为《神经网事件的表示法》的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为“正则集的代数”的表达式,因此采用“正则表达式”这个术语。
发展
1968年,unix操作系统之父肯尼斯·蓝·汤普森(Kenneth Lane Thompson)将正则表达式应用到了UNIX的QED编辑器的搜索算法中,随后汤普森又将正则表达式引入了UNIX下的文本编辑器ed,这是正则表达式第一次在非技术领域大规模使用。1971年,汤普森申请的一项基于正则表达式的快速文本匹配机制的专利获得批准,它是最早的软件专利之一。
ed编辑器有条命令——显示正在编辑的文件中能够匹配特定正则表达式的行,该命令“g/Regular Expression/p”,读作“Global Regular Expression Print”(应用正则表达式的全局输出)。这个功能非常实用,最终成为独立的工具GREP(之后又产生了egrep——扩展的grep)。自此,正则表达式被广泛地应用到各种unix操作系统或类UNIX操作系统中。
1986年,POSIX诞生,它是Portable Operating System 接口(可移植操作系统接口)的缩写,由电气与电子工程师学会(Institute of Electrical and 电子学 Engineers,简称IEEE)制定。POSIX是一系列标准,确保操作系统之间的移植性,该标准的某些部分关乎正则表达式和使用他们的传统工具。POSIX把正则表达式常见的流派分为两大类:Basic Regular Expressions (BREs)和Extended Regular Expressions(EREs)。同年,亨利·斯宾塞(Henry Spencer)发布了用c语言写的正则表达式包,这个包可以置入其他程序中。
1987年12月,拉里·沃尔(Larry Wall)发布了Perl Version 1。Perl Version 1提供了传统上只有专用工具sed和Awk才提供的正则表达式操作符,这在通用脚本语言中是首创。1988年6月,Perl 2发布。Larry完全放弃了原有的正则表达式代码,而采用了斯宾塞的正则表达式包的增强版。此外,添加了/i量词,能够进行不区分大小写的匹配。Perl 3和Perl 4分别于1989年和1991年发布。1994年10月,Perl 5发布。就正则表达式来说,Perl 5进行了更多内部优化,添加了少量元字符、非捕获的括号、忽略优先(lazy)的量词、顺序环视功能以及/x量词。2015年12月25日,Perl 6正式发布,后改名为Raku。
1997年,菲利普·黑泽尔(Philip Hazel)开发了PCRE库。PCRE是一个软件正则表达式匹配库,兼容Perl5 正则表达式的语法定义,现该库主要有两个版本:PCRE和PCRE2。PCRE是目前得到广泛应用的正则表达式匹配解决方案,被集成在编程语言 (PHP)、网络浏览器、电子邮件系统、网络安全系统等多种应用场景。
现状
2009年,谷歌提供了基于软件的正则表达式匹配库——RE2,由曾在贝尔实验室任职的Russ Cox推出。正则表达式字符串匹配算法有单字符串匹配KMP算法、单字符串匹配BM算法、多字符串匹配AC算法、SHIFT-OR算法等。
21世纪,正则表达式在计算机编程和文本处理中变得越来越流行,很多基本的硬件都兼容正则表达式引擎的实现,可应用于各种编程语言和文本处理工具中,例如C/C++、Java、TCL科技、Python、Perl和PHP等,在数据科学、文本分析、网络爬虫、字符串搜索和替换等领域都有广泛的应用。
基本语法
概述
完整的正则表达式是由普通字符和元字符组成的文本模式。普通字符包括大写和小写字母、所有的数字,以及没有特殊定义的标点和符号。元字符则是在正则表达式中具有特殊含义的一些符号,例如字符组的开方括号[、闭方括号],锚点^、$等。正则表达式元字符的类型有转义符、字符组、分组、匹配量词、锚点和零宽断言,以及多选结构和嵌入条件等。匹配模式共有4种,分别是不区分大小写模式、单行模式、多行模式和注释模式。
此外,元字符是有特殊含义的字符,如果要匹配“元字符”自身则必须转义,也就是在元字符之前添加反斜线\,即转义元字符。比如元字符点号.,可以匹配除换行符以外的任何字符,如果要准确匹配字符串中的点号.,正则表达式中就必须写\.。
相关标准
自Ken Thompson将正则表达式引入qed编辑器之后,越来越多的unix操作系统或者类UNIX操作系统开始使用正则表达式,例如grep、egrep、lex、awk和sed等。不同的开发语言,例如C/C++、Java、TCL科技、Python、Perl和PHP等也都包含了各自的正则表达式包。
不同的工具和不同的开发语言在自身的发展过程中对正则表达式支持的元字符和这些元字符的意义的规定存在着较多的差异,导致形成众多正则表达式的流派。不同流派之间的差异给正则表达式的发展和使用造成了一定程序的混乱,不同流派的整合和相关标准的制定成为一个必然的趋势。
Perl和PCRE
1987年12月,拉里·沃尔发布了Perl Version 1。Perl 是一种通用编程语言,最初是为文本操作而开发的,现在用于广泛的任务,包括系统管理、We开发、网络编程、GUI开发等。它的主要特点是易于使用,支持过程和面向对象编程,具有强大的内置文本处理支持。
PCRE是由菲利普·黑泽尔(Philip Hazel)在1997年发布的一套兼容Perl正则表达式的库。PCRE的正则引擎质量很高,继承了Perl的正则表达式的语法和语义。开发者可以把PCRE整合到自己的工具和语言中,为用户提供丰富且极具表现力的各种正则功能。许多软件都使用了PCRE,例如PHP、Apache2和Nmap等。
PCRE有两个主要的版本,当前版本是PCRE2,于2015年发布,最新版本号是10.39。而目前使用得最广泛的仍然是1997年发布的PCRE,当前版本号是8.45。PCRE支持大量语法,可总结成几种不同的类别,包括字符类、有界重复、分支结构、反向引用等。PCRE2提供了与PCRE类似的语法支持,但做了少量调整,例如增加了一些Python、.NET和Oniguruma语法。PCRE语法从严格意义上来说是正则表达式的一个流派,而不是正则表达式的一个标准,但是由于PCRE使用广泛和受欢迎程度高,开发者也常常把兼容PCRE语法称为符合PCRE标准。
POSIX标准
正则表达式POSIX标准把正则表达式分成两大类:基本正则表达式(Basic Regular Expression,BRE)和扩展正则表达式(Extended Regular Expression,ERE),遵循POSIX标准的程序必须支持其中任意一种。
在传统的unix常用工具中,grep、vi、sed等属于BRE流派。当使用像()、{}这样的元字符时,元字符前面必须要加转义符。此外,BRE流派也不支持+和?量词,以及…|…多选结构和反向引用\1、\2等。但是,今天纯粹的BRE已经很少见了,GNU对BRE做了扩展,使得BRE也能支持+、?量词,以及…|…多选结构,不过这些元字符前面都必须要加转义符变成\+、\?、\|。所以,GNU的GREP等工具严格地说应该属于GNU BRE。
在传统的unix常用工具中,egrep、grep-E、awk等属于ERE流派。ERE名为扩展,但其并不是BRE的扩展,而是自成一体。ERE中元字符使用时前面无须加转义符,并且支持+和?量词,支持…|…多选结构。值得注意的是,POSIX ERE标准中并没有明确规定支持反向引用。GNU同样对ERE做了扩展,使得ERE能够支持反向引用的功能。所以,GNU的egrep等工具严格地说属于GNU ERE。
在POSIX标准中,支持[AZ]和[^a-z]这样的字符组。此外,POSIX标准还支持一种特别的字符组表示,类似于[[:alnum:]]和[[:alpha:]],这是POSIX标准方括号表达式的一种特殊功能。POSIX方括号表达式与PCRE字符组[...]和[^...]最主要的区别在于,POSIX方括号表达式内部\不是用来转义的,所以在POSIX中[\d]匹配的是\和d两个字符。
POSIX字符组就是在POSIX方括号表达式内使用几种特殊元字符。值得注意的是,POSIX字符组表示的是当前语言环境下对应的字符,因此POSIX字符组详细的列表会根据当前语言环境的变化而变化。此外,这种特殊的元字符只有在方括号表达式内部才是有效的,所以使用完整的POSIX字符组时必须写成[[:alnum:]]。
元字符
不同的流派支持的元字符和这些元字符代表的意义存在着细微的差异,这里参考了Perl兼容正则表达式(Perl Compatible Regular Expression,PCRE)的语法。正则表达式的元字符之间也有优先权顺序。在匹配过程中,按照正则表达式中从左到右、不同优先权先高后低来匹配相应的元字符。下面列举了优先权从高到低的正则表达式元字符的类型:转义符、字符组、分组、匹配量词、锚点和零宽断言,以及多选结构和嵌入条件等。
转义符
转义符用来清晰简便地表示一些特定的字符,包括一些不可打印字符。
字符组
字符组(Character Class)是正则表达式最基本的结构之一,在正则表达式中的某个位置表示一组指定的字符。常见的字符组表示方法有点号、普通字符组和字符组简记法。
分组
分组主要包括捕获型分组和非捕获型分组。
匹配量词
匹配量词能够用来限制前面子表达式的匹配次数。匹配量词又可以分为标准匹配量词、忽略优先量词和占有优先量词。
锚点和零宽断言
正则表达式中的锚点和零宽断言都不会匹配实际的字符,而是寻找和定位字符在文本中的位置,可以认为都是定位符。
多选结构和嵌入条件
多选结构常常和嵌入条件一起使用。例如上海市地区固定电话号码一般写成021-12345678或者(021)12345678,可以用正则表达式(\()?021(?(\1)\)|-)\d{8}来匹配这两种写法。其中(?(\1)\)|-)部分表示如果前面(\()捕获到了匹配,那么继续匹配一个右括号),否则匹配一个横线-。
模式修饰符
正则表达式可以通过模式修饰符(?modifier)来设置匹配的模式,常见的模式modifier的值有i、s、m。
量词
标准匹配量词
标准匹配量词(+、?、+,以及{m,n})都是优先匹配的。
例如邮政编码201203、100858,其正则表达式为\d\d\d\d\d\d。使用量词简化,可得\d{6}。
量词还可以表示不确定的长度,其通用形式是{m,n}。其中m和n是两个数字(量词中的逗号之后不能有空格),它限定之前的元素能够出现的次数,m是下限,n是上限(均为闭区间)。比如\d{4,6},表示这个数字字符串的长度最短是4个字符(“单个数字字符”至少出现4次),最长是6个字符。如果不确定长度的上限,也可以省略,只指定下限,写成\d{m,},比如\d{4,},表示“数字字符串的长度必须在4个字符以上”。量词限定的出现次数一般都有明确下限,如果没有,则默认为0。
{m,n}是通用形式的量词,正则表达式还有3个常用量词,分别是+、?、*。它们的功能与{m,n}是相同的。
忽略优先量词
忽略优先量词(lazy quantifier或reluctant quantifier,也有人将其翻译为懒惰量词),如果不确定是否要匹配,忽略优先量词会选择“不匹配”的状态,再尝试表达式中之后的元素,如果尝试失败,再回溯,选择之前保存的“匹配”的状态。
标准匹配量词与忽略优先量词逐一对应,只是在对应的标准匹配量词之后添加?,两者限定的元素能出现的次数也一样,遇到不能匹配的情况同样需要回溯;唯一的区别在于,忽略优先量词会优先选择“忽略”,而标准匹配量词会优先选择“匹配”。
占有优先量词
占有优先量词是在标准匹配量词之后接+字符,即*+、++、?+、{n}+、{n,}+和{n,m}+。占有优先量词类似于标准匹配量词,但是匹配的结果不会“交还”或者说回溯。
举例来说,当正则表达式a.*b和a.*+b同时去匹配字符串acccb时,a.*b会匹配成功,而a.*+b会匹配失败。原因是不管是.*还是.*+,它们都是匹配优先的,并且“贪婪”地匹配到了字符串acccb中的cccb,但是.*+不会回溯已匹配到的结果,所以正则表达式a.*+b中的b会发现这时候字符串中已经没有b可以匹配,从而整个正则表达式匹配失败。占有优先量词能够控制匹配和不能匹配的内容,在某些场合下使用占有优先量词能够提高匹配效率。
括号
分组
分组,将相关的元素归拢到一起,构成单个元素。如果用量词限定出现次数的元素不是字符或者字符组,而是连续的几个字符甚至子表达式,就应该用括号将它们“编为一组”。比如,要求字符串ab重复出现一次以上,正则表达式为(ab)+,此时(ab)成为一个整体,由量词+来限定;如果不用括号而直接写ab+,受+限定的就只有b。
多选结构
多选结构,规定可能出现的多个子表达式。多选结构的形式是(…|…),在括号内以竖线|分隔开多个子表达式,这些子表达式也叫多选分支(option);在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就成功;如果所有子表达式都不能匹配,则整个多选结构匹配失败。
此外,多选结构的一般表示法是(option1|option2)(其中option1和option2是两个作为多选分支的正则表达式),多选结构中一般会同时使用括号()和竖线|;但是如果没有括号(),只出现竖线|,仍然是多选结构。
引用分组
引用分组,将子表达式匹配的文本存储起来,供之后引用。
使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过group(num)之类的方法“引用”分组在匹配时捕获的内容。其中,num表示对应括号的编号,括号分组的编号规则是从左向右计数,从1开始。因为“捕获”了文本,所以这种功能叫作捕获分组(capturing group)。对应的,这种括号叫作捕获型括号。诸如2010-12-22、2011-01-03这类表示日期的字符串,从中提取出年、月、日之类的信息,就可借助捕获分组来实现。
反向引用(back-reference),允许在正则表达式内部引用之前的捕获分组匹配的文本,其形式是\num,其中num表示所引用分组的编号。根据反向引用,可以用来查找连续重叠字母。其表达式是([a-z])\1,其中的[a-z]匹配第一个字母,再用括号将匹配分组,然后用\1来反向引用。
在Python中,命名分组的记法为(?P
非捕获分组
非捕获分组(non-capturing group),用来标识那些不需要引用的分组。在开括号后紧跟一个问号和冒号(?:…),这样的括号叫作非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组。
断言
并不真正匹配文本,而只负责判断在某个位置左/右侧的文本是否符合要求,这种结构被称为断言(assertion)。常见的断言有三类:单词边界、行起始/结束位置、环视。
单词边界
单词边界(word boundary),记为\b。它匹配的是“单词边界”位置,而不是字符。即\b能够匹配这样的位置:一边是单词字符,另一边不是单词字符。其准确解释为:一端必须出现\w能匹配的字符,另一端不出现\w能匹配的字符。在JavaScript、PHP、Python2、Ruby中,\w只能匹配[0-9a-zA-Z_]。所以在这些语言中,\b\w+\b能用来匹配几乎所有的英文单词。
单词边界并不区分左右,在“单词边界”上,单词字符只能出现在一侧;单词字符要求“另一边不是单词字符”,即一边必须出现单词字符,另一边可以出现非单词字符,也可能没有任何字符。所以,如果字符串只包含单词word,用\bword\b应该是可以匹配的,虽然w之前和d之后都没有任何字符。
行起始/结束位置
单词边界匹配的是某个位置而不是文本,在正则表达式中,这类匹配位置的元素叫作锚点(anchor),它用来“定位”到某个位置。除了\b,常用的锚点还有^和$。通常来说,它们分别匹配字符串的开始位置和结束位置,用来判断“整个字符串能否由表达式匹配”。
一般情况下,^匹配整个字符串的起始位置。例如,正则表达式^Some可以准确验证字符串“是否以Some开头。因为^会把整个表达式的匹配“定位”在字符串的开始位置。这样,即便表达式的其他部分可以在字符串中其他位置找到匹配,整个表达式也无法匹配成功。在某些情况下,^也可以匹配字符串内部的“行起始位置”。
$通常匹配的是整个字符串的结尾位置。如果最后是行终止符则匹配行终止符之前的位置,否则匹配最后一个字符之后的位置。如果指定了多行模式,$会匹配每个行终止符之前的位置。最后一行的情况有点特殊:如果最后一行没有行终止符,则匹配字符串的结尾位置;否则,匹配行终止符之前的位置。与$类似的还有两个特殊标记\Z和\z,它们不受多行模式的影响,在任何情况下都匹配整个字符串的结束位置。\Z和\z的主要差别在于:\Z等价于默认模式(非多行模式)下的$;\z则不管行终止符,只匹配整个字符串的结束位置。
环视
环视(look-around)用来“停在原地,四处张望”。环视类似单词边界,在它旁边的文本需要满足某种条件,而且本身不匹配任何字符。环视一共分为4种:肯定顺序环视(positive-lookahead)、否定顺序环视(negative-lookahead)、肯定逆序环视(positive-lookbehind)、否定逆序环视(negative-lookbehind)。在当前位置,如果是朝右判断,则是顺序环视;如果是朝左判断,则是逆序环视;如果要求子表达式能匹配的字符串必须出现,则为肯定环视;如果要求子表达式能匹配的字符串不能出现,则为否定环视。
比如正则表达式<(?!/),其中的(?!/)是一个环视结构,(?!…)是这个结构的标识,/才是真正的表达式,整个结构的意思是“当前位置之后(右侧),不允许出现/能匹配的文本”。如果<(?!/)匹配成功,正则表达式真正匹配完成的只有<,而不包括<之后的那个字符,这样,就能准确表示“匹配<,同时这个<之后不能是/”。
再如正则表达式(?,其中的(?,同时>之前不能是/”。
匹配模式
匹配模式(match mode),是正则表达式一个常用功能,指的是匹配时遵循的规则。设置特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有4种:不区分大小写模式、单行模式、多行模式、注释模式。
模式指定方式
通常,有两种办法指定匹配模式:以模式修饰符指定,或者以预定义的常量作为特殊参数传入来指定。模式修饰符即模式名称对应的单个字符,使用时将其填入特定结构(?modifier)中(其中的modifier为模式修饰符),嵌在正则表达式的开头,常见的模式modifier的值有i、s、m。JavaScript是例外,JavaScript不支持模式修饰符(?modifier)的记法。另一种指定模式的方式是使用预定义的常量作为参数,传入正则函数。
不区分大小写模式
不区分大小写的匹配模式对应的模式修饰符是i(case Insensitive)。例如,对the指定此模式,完整的正则表达式就是(?i)the,就可以匹配the、The、THE等各种大小写形式的the。
单行模式
元字符点号.匹配除了换行符(\n)之外的任意一个字符。但是当需要匹配“任何字符”,比如在处理HTML源代码时,经常会遇到跨越多行的脚本代码,所以正则表达式提供了单行模式。在这种模式下,所有文本似乎只在一行里,换行符是这一行中的“普通字符”,所以可以由点号.匹配。即在单行模式下,点号.可以匹配包括换行符在内的任何字符。
单行模式对应的模式修饰符是s(Single line),所以如果用模式修饰符,可以在表达式的开头用(?s)指定,因此上述HTML的正则表达式可以写为(?s)