【Regex】正则表达式

一直以来,从 JavaScriptPHPPythonGolang,然后还有linux系统中,无处不见正则表达式的身影,可是一致困扰在POSIXPCRE的概念中,分不清这两个是个啥,今天就来翻翻正则表达式的老底,了解了解正则表达式的前世今生。

Regular ExpressionRegular一般被译为正则、正规、常规。此处的Regular即是规则的意思,Regular Expression即描述某种规则的表达式之意。

正则表达式(英语:Regular Expression,在代码中常简写为regexregexpRE),是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些匹配某个模式的文本。

许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sedgrep)普及开的。正则表达式通常缩写成regex,单数有regexpregex,复数有regexpsregexesregexen

历史

正则表示式这一概念最早可以追溯到20世纪40年代的两个神经物理学家Warren McCullochWalter Pitts,他们将神经系统中的神经元描述成小而简单的自动控制元。

紧接着,在50年代,数学家1950年代,数学家Stephen Kleene利用称之为正则集合的数学符号来描述此模型,并且建议使用一个简单的概念来表示,于是regular expressions就正式登上历史舞台了。

1968年,Ken Thompson发表了Regular Expression Search Algorithm, 紧接着大神Thompson根据这个论文实现了Unix上编辑器ed的前身qeded所支持的正则表示式并不比qed的高级,但是ed是第一个在非技术圈广泛传播的工具,ed有一个命令可以展示文本中符合给定正则表达式的行,这个命令是g/Regular Expression/p,在英文中读作**Global Regular Expression Print**,由于这个命令非常实用,所以后来有了grepegrep这两个命令。

相比egrepgrep只支持很少的元符号,是支持的(但不能用于分组中),但是+|?是不支持的;而且,分组时需要加上反斜线转义,像\( ...\)这样才行,由于grep的缺陷性日渐明显,AT&TAlfred Aho实在受不了了,于是egrep诞生了,这里的e表示extended,加强版的意思,支持了+|?这三个元符号,并且可以在分组中使用*,分组可以直接写成(...),同时用\1,\2...来引用分组。

grepegrep发展的同时,awklexsed等程序也开始发展起来,而且每个程序所支持的正则表达式都或多或少的和其他的不一样,这应该算是正则表达式发展的混乱期,因为这些程序在不断的发展过程中,有时新增加的功能因为bug原因,在后期的版本中取消了该功能,例如,如果让grep支持元符号+的话,那么grep就不能表示字符+了,而且grep的老用户会对这很反感。

这种门派自居的时代混乱不堪,总得有人来统一吧。到了1986年,这个人终于来了,他就是**POSIX(Portable Operating System Interface)标准**,POSIX制定了不同操作系统之间都需要遵守的一套规则。当然了,正则表达式也包括其中,终于来个管事的,POSIX规范分为基本正则表达式BRE(Basic Regular Expressions)和扩展正则表达式ERE(Extended Regular Express,ERE)两个流派,所有的POSIX程序可以选择支持其中的一种,具体规范详见下表:

从上图可以看出,有三个空白栏,那么是不是就意味这无法使用该功能了呢?答案是否定的,因为我们现在使用的linux发行版,都是集成GNU套件的,GNUGnu’s Not Unix的缩写,GNU在实现了POSIX标准的同时,做了一定的扩展,所以上面空白栏中的功能也能使用。下面一一讲解:

  • BRE如何使用+?呢?需要用\+\?
  • BRE如何使用|呢?需要用\|
  • ERE如何使用\1、\2…\9这样的反引用?和BRE一样,就是\1、\2…\9

通过上面总结,可以发现:GNU中的EREBRE的功能相同,只是语法不同(BRE需要用\进行转义,才能表示特殊含义)。例如a{1,2},在ERE表示的是aaa,在BRE中表示的是a{1,2}这个字符串。为了能够在Linux下熟练使用文本处理工具,我们必须知道这些命令支持那种正则表达式。现对常见的命令总结如下:

  • 使用BRE语法的命令有:grep、ed、sed、vim
  • 使用ERE语法的命令有:egrep、awk、emacs

当然,这也不是绝对的,比如 sed 通过-r选项就可以使用ERE了,大家到时自己man一下就可以了。还值得一提的是POSIX还定义了一些shorthand,具体如下:

  • [:alnum:]
  • [:alpha:]
  • [:cntrl:]
  • [:digit:]
  • [:graph:]
  • [:lower:]
  • [:print:]
  • [:punct:]
  • [:space:]
  • [:upper:]
  • [:xdigit:]

在使用这些shorthand时有一个约束:必须在[]中使用,也就是说如果像匹配0-9的数字,需要这么写[[:alnum:]],取反就是[^[:alnum:]]shorhandBREEBE中的用法相同。

如果你对sedawk比较熟悉,你会发现我们平常在变成语言中用的\d\w在这些命令中不能用,原因很简单,因为POSIX规范根本没有定义这些shorthand,这些是由下面将要说的PCRE中定义的。

除了POSIX标准外,还有一个Perl分支,也就是我们现在熟知的PCRE(Perl兼容正则表达式,Perl Compatible Regular Expressions),源自于Henry Spencer1986119日发布的regex,随着Perl语言的发展,Perl语言中的正则表达式功能越来越强悍,为了把Perl语言中正则的功能移植到其他语言中,PCRE就诞生了。现在的编程语言中的正则表达式,大部分都属于PCRE这个分支

Perl语言第一版是由Larry Wall发布于198712月,Perl在发布之初,就因其强大的功能而一票走红,Perl的定位目标就是天天要使用的工具。

Perl比较显诸特征之一是与sedawk兼容,这造就了Perl成为第一个通用性脚本语言。

随着Perl的不断发展,其支持的正则表达式的功能也越来越强大。其中影响较大的是于199410月发布的Perl 5,其增加了很多特性,比如non-capturing parentheseslazy quantifierslook-ahead、元符号\G等等。

正好这时也是 WWW 兴起的时候,而Perl就是为了文本处理而发明的,所以Perl基本上成了web开发的首选语言。Perl语言应用是如此广泛,以至于其他语言开始移植 Perl,最终Perl compatible(兼容)的PCRE诞生了,这其中包括了Tcl, Python, Microsoft’s .NETRubyPHPC/C++Java等等。

前面说了shorthandPOSIXPCRE是不同的,PCRE中我们常用的有如下这些:

  • \w 表示[a-zA-Z]
  • \W 表示[^a-zA-Z]
  • \s 表示[ \t\r\n\f]
  • \S 表示[^ \t\r\n\f]
  • \d 表示[1-9]
  • \D 表示[^1-9]
  • \< 表示一个单词的起始
  • \> 表示一个单词的结尾

PCRE

  1. \, 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape,有^$()*+?.[\{|共计12个)、或一个向后引用(backreferences)、或一个八进制转义符。例如,n匹配字符n\n匹配一个换行符。序列\\匹配\\(则匹配(

  2. ^, 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配\n\r之后的位置。

  3. $, 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配\n\r之前的位置。

  4. *, 匹配前面的子表达式零次或多次。例如,zo*能匹配zzo以及zoo*等价于{0,}

  5. +, 匹配前面的子表达式一次或多次。例如,zo+能匹配zo以及zoo,但不能匹配z+等价于{1,}

  6. ?, 匹配前面的子表达式零次或一次。例如,do(es)?可以匹配dodoes中的do?等价于{0,1}

  7. {n}, n是一个非负整数。匹配确定的n次。例如,o{2}不能匹配Bob中的o,但是能匹配food中的两个o

  8. {n,}, n是一个非负整数。至少匹配n次。例如,o{2,}不能匹配Bob中的o,但能匹配foooood中的所有oo{1,}等价于o+o{0,}则等价于o*

  9. {m,n}, mn均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,o{1,3}将匹配fooooood中的前三个oo{0,1}等价于o?请注意在逗号和两个数之间不能有空格

  10. ?, 非贪心量化(Non-greedy quantifiers:当该字符紧跟在任何一个其他重复修饰符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串ooooo+?将匹配单个o,而o+将匹配所有o

  11. ., 匹配除\r \n之外的任何单个字符。要匹配包括\r \n在内的任何字符,请使用像(.|\r|\n)的模式。

  12. (pattern), 匹配pattern并获取这一匹配的子字符串。该子字符串用于向后引用。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性,要匹配圆括号字符,请使用\(\)

  13. (?:pattern), 匹配pattern但不获取匹配的子字符串,也就是说这是一个非获取匹配,不存储匹配的子字符串用于向后引用。这在使用或字符(|)来组合一个模式的各个部分是很有用。例如industr(?:y|ies)就是一个比industry|industries更简略的表达式。

  14. (?=pattern), 正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,Windows(?=95|98|NT|2000)能匹配Windows2000中的Windows,但不能匹配Windows3.1中的Windows。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

  15. (?!pattern), 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如Windows(?!95|98|NT|2000)能匹配Windows3.1中的Windows,但不能匹配Windows2000中的Windows。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

  16. (?<=pattern), 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,(?<=95|98|NT|2000)Windows能匹配2000Windows中的Windows,但不能匹配3.1Windows中的Windows

  17. (?<!pattern), 反向否定预查,与正向否定预查类似,只是方向相反。例如(?<!95|98|NT|2000)Windows能匹配3.1Windows中的Windows,但不能匹配2000Windows中的Windows

  18. x|y, 匹配xy。例如,z|food能匹配zfood(?:z|f)ood则匹配zoodfood

  19. [xyz], 字符集合(character class)。匹配所包含的任意一个字符。例如,[abc]可以匹配plain中的a。特殊字符仅有反斜线\保持特殊含义,用于转义字符。其它特殊字符如*+、各种括号等均作为普通字符。^如果出现在首位则表示不在字符集合;如果出现在字符串中间就仅作为普通字符。连字符 - 如果出现在字符串中间表示字符范围描述;如果如果出现在首位(或末尾)则仅作为普通字符。右方括号应转义出现,也可以作为首位字符出现。

  20. [^xyz], 排除型字符集合(negated character classes)。匹配未列出的任意字符。例如,[^abc]可以匹配plain中的plin

  21. [a-z], 字符范围。匹配指定范围内的任意字符。例如,[a-z]可以匹配az范围内的任意小写字母字符。

  22. [^a-z], 排除型的字符范围。匹配任何不在指定范围内的任意字符。例如,[^a-z]可以匹配任何不在az范围内的任意字符。

  23. \b, 匹配一个单词边界,也就是指单词和空格间的位置。例如,er\b可以匹配never中的er,但不能匹配verb中的er

  24. \B, 匹配非单词边界。er\B能匹配verb中的er,但不能匹配never中的er

  25. \cx, 匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Za-z之一。否则,将c视为一个原义的c字符。

  26. \d, 匹配一个数字字符。等价于[0-9]。注意Unicode正则表达式会匹配全角数字字符。

  27. \D, 匹配一个非数字字符。等价于[^0-9]

  28. \f, 匹配一个换页符。等价于\x0c\cL

  29. \n, 匹配一个换行符。等价于\x0a\cJ

  30. \r, 匹配一个回车符。等价于\x0d\cM

  31. \s, 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。注意Unicode正则表达式会匹配全角空格符。

  32. \S, 匹配任何非空白字符。等价于[^ \f\n\r\t\v]

  33. \t, 匹配一个制表符。等价于\x09\cI

  34. \v, 匹配一个垂直制表符。等价于\x0b\cK

  35. \w, 匹配包括下划线的任何单词字符。等价于[A-Za-z0-9_]。注意Unicode正则表达式会匹配中文字符。

  36. \W, 匹配任何非单词字符。等价于[^A-Za-z0-9_]

  37. \ck, 匹配控制转义字符。k代表一个字符。等价于Ctrl-k。用于ECMA语法。

  38. \xnn, 十六进制转义字符序列。匹配两个十六进制数字nn表示的字符。例如,\x41匹配A\x041则等价于\x04&1。正则表达式中可以使用ASCII编码。

  39. \num, 向后引用(back-reference)一个子字符串(substring),该子字符串与正则表达式的第num个用括号围起来的捕捉群(capture group)子表达式(subexpression)匹配。其中num是从1开始的十进制正整数,其上限可能是93199甚至无限。例如:(.)\1匹配两个连续的相同字符。

  40. \n, 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。

  41. \nm, 3位八进制数字,标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若nm均为八进制数字(0-7),则\nm将匹配八进制转义值nm

  42. \nml, 如果n为八进制数字(0-3),且ml均为八进制数字(0-7),则匹配八进制转义值nml

  43. \un, Unicode转义字符序列。其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。

POSIX

优先权

优先权 符号
最高 \
( )、(?: )、(?= )、[ ]
*、+、?、{n}、{n,}、{m,n}
^、$、中介字符
次最低 串接,即相邻字符连接在一起
最低 |

示例

  • 匹配至少同时包含大小写字母,数字以及符号中其中两个的密码字符串:^(?![A-Z]+$)(?![a-z]+$)(?!\d+$)(?!\W+$)\S{8,16}$

    123131sdadad
    #%sdad@#$dsd

参考阅读