正则表达式是一种查找以及字符串替换操作。正则表达式在文本编辑器中广泛使用,比如正则表达式被用于:
与文本编辑器相似,几乎所有的高级编程语言都支持正则表达式。在这样的语境下,“文本”也就是一个字符串,可以执行的操作都是类似的。一些编程语言(比如Perl,JavaScript)会检查正则表达式的语法。
正则表达式是什么?
正则表达式只是一个字符串。没有长度限制,但是,这样的正则表达式长度往往较短。如下所示是一些正则表达式的例子:
I had a \S+ day today
[A-Za-z0-9\-_]{3,16}
\d\d\d\d-\d\d-\d\d
v(\d+)(\.\d+)*
TotalMessages="(.*?)"
<[^<>]>
在实现中,正则表达式还有其他的特点。本文将重点讨论正则表达式的核心语法,在几乎所有的正则表达式中都可以见到这些规则。
特别提示:正则表达式与文件通配语法无关,比如 *.xml
正则表达式中包含了一系列的字符,这些字符只能匹配它们本身。有一些被称为“元字符”的特殊字符,可以匹配一些特殊规则。
如下所示的例子中,我用红色标出了元字符。
I had a \S+ day today
[A-Za-z0-9\-_]{3,16}
\d\d\d\d-\d\d-\d\d
v(\d+)(\.\d+)*
TotalMessages="(.*?)"
<[^<>]*>
大部分的字符,包括所有的字母和数字字符,是普通字符。也就意味着,它们只能匹配它们自己,如下所示的正则表达式:
cat
意味着,只能匹配一个字符串,以“c”开头,然后是字符“a”,紧跟着是字符“t”的字符串。
到目前为止,正则表达式的功能类似于
String.indexOf()
函数strpos()
函数我们第一个要讲解的元字符是“.”。这个符号意味着可以匹配任意一个字符。如下所示的正则表达式:
c.t
意味着匹配“以c开头,之后是任意一个字符,紧跟着是字母t”的字符串。
在一段文本中,这样的正则表达式可以用来找出cat
, cot
, czt这样的字符串,甚至可以找出c.t这样的组合,但是不能找到ct或者是coot这样的字符串。
使用反斜杠“\”可以忽略元字符,使得元字符的功能与普通字符一样。所以,正则表达式
c\.t
表示“找到字母c,然后是一个句号(“.”),紧跟着字母t”
反斜杠本身也是一个元字符,这意味着反斜杠本身也可以通过相似的方法变回到普通字符的用途。因此,正则表达式
c\\t
表示匹配“以字符c开头,然后是一个反斜杠,紧跟着是字母t”的字符串。
注意!在正则表达式的实现中,.是不能用于匹配换行符的。”换行符“的表示方法在不同实现中也不同。实际编程时,请参考相关文档。在本文中,我认为.是可以匹配任意字符的。实现环境通常会提供一个Flag标志位,来控制这一点。
字符类是一组在方括号内的字符,表示可以匹配其中的任何一个字符。
包含忽略字符的例子
\[\]
\ab]表示匹配的字符为”[“或者”]”或者”a”,或者”b”\[\]
]表示匹配的字符为”\”或者 “[”或者”]”在字符类中,字符的重复和出现顺序并不重要。[dabaaabcc]与[abc]是相同的
重要提示:字符类中和字符类外的规则有时不同,一些字符在字符类中是元字符,在字符类外是普通字符。一些字符正好相反。还有一些字符在字符类中和字符类外都是元字符,这要视情况而定!
比如,.表示匹配任意一个字符,而[.]表示匹配一个全角句号。这不是一回事!
在字符集中,你可以通过使用短横线来表示匹配字母或数字的范围。
使用目前我们已经讲解的正则表达式相关知识,在字典中匹配找到含有最多连续元音的单词,同时找到含有最多连续辅音的单词。
[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]
这样的正则表达式,可以匹配连续含有六个元音的单词,比如 euouae
和 euouaes
。
同样的,恐怖的正则表达式
[bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]
可以找到连续含有十个辅音的单词sulphhydryls
.
下文中,我们会讲解,怎样有效缩短这样的正则表达式长度。
在字符类之外,短横线没有特殊含义。正则表达式a-z,表示匹配字符串“以a开头,然后是一个短横线,以z结尾”。
范围和单独的字符可能在一个字符类中同时出现:
使用已经介绍过的正则表达式知识,匹配YYYY-MM-DD格式的日期。
[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
.
同样的,下文中,我们会介绍怎样有效减少这样的正则表达式长度。
虽然你可以尝试在正则表达式中使用一些非字母或数字作为范围的最后一个符号,比如abc[!-/]def,但是这并不是在每种实现中都合法。即使这样的语法是合法的,这样的语义也是模糊的。最好不要这样使用。
同时,你必须谨慎选择范围的边界值。即使[A-z]在你使用的实现中,是合法的,也可能会产生无法预料的运行结果。(注意,在z到a之间,是有字符存在的)
注意:范围的字符值代表的是字符而已,并不能代表数值范围,比如[1-31]表示匹配一个数字,是1或者2或者3,而不是匹配一个数值在1到31之间的数。
你可以在字符类的起始位放一个反义符。
在字典中,找到一个不满足“在e之前有i,但是没有c”的例子。
cie和[^c]ei都要可以找到很多这样的例子,比如ancient,science,viel,weigh
\d这个正则表达式与[0-9]作用相同,都是匹配任何一个数字。(要匹配\d,应该使用正则表达式\\d)
\w与[0-9A-Za-z]相同,都表示匹配一个数字或字母字符
\s意味着匹配一个空字符(空格,制表符,回车或者换行)
另外
这些是你必须掌握的字符。你可能已经注意到了,一个全角句号“.”也是一个字符类,可以匹配任意一个字符。
很多正则表达式的实现中,提供了更多的字符类,或者是标志位在ASCII码的基础上,扩展现有的字符类。
特别提示:统一字符集中包含除了0至9之外的更多数字字符,同样的,也包含更多的空字符和字母字符。实际使用正则表达式时,请仔细查看相关文档。
简化正则表达式 [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
.
\d\d\d\d-\d\d-\d\d
.
在字符或字符集之后,你可以使用{ }大括号来表示重复
简化下面的正则表达式
z.......z
\d\d\d\d-\d\d-\d\d
[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]
[bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]
z.{7}z
\d{4}-\d{2}-\d{2}
[aeiou]{6}
[bcdfghjklmnpqrstvwxyz]{10}
注意:重复字符是没有记忆性的,比如[abc]{2}表示先匹配”a或者b或者c”,再匹配”a或者b或者c”,与匹配”aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc“一样。[abc]{2}并不能表示匹配”aa或者bb或者cc“
重复次数是可以指定范围的
注意这样的正则表达式会优先匹配最长字符串,比如输入 I had an aaaaawful day
会匹配单词aaaaawful中的aaaaa,而不会匹配其中的aaa。
重复次数是可以有范围的,但是有时候这样的方法也不能找到最佳答案。如果你的输入文本是I had an aaawful daaaaay那么在第一次匹配时,只能找到aaawful,只有再次执行匹配时才能找到daaaaay中的aaaaa.
重复次数的范围可以是开区间
使用正则表达式找到双引号。要求输入字符串可能包含任意个字符。
调整你的正则表达式使得在一对双引号中间不再包含其他的双引号。
".{0,}"
, 然后 "[^"]{0,}"
.
?与{0,1}相同,比如,colou?r表示匹配colour或者color
*与{0,}相同。比如,.*表示匹配任意内容
+与{1,}相同。比如,\w+表示匹配一个词。其中”一个词”表示由一个或一个以上的字符组成的字符串,比如_var或者AccountName1.
这些是你必须知道的常用转义字符,除此之外还有:
简化下列的正则表达式:
".{0,}"
and "[^"]{0,}"
x?x?x?
y*y*
z+z+z+z+
".*"
and "[^"]*"
x{0,3}
y*
z{4,
}写出正则表达式,寻找由非字母字符分隔的两个单词。如果是三个呢?六个呢?
\w+\W+\w+
, \w+\W+\w+\W+\w+
, \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+
.
下文中,我们将简化这个正则表达式。
正则表达式 “.*” 表示匹配双引号,之后是任意内容,之后再匹配一个双引号。注意,其中匹配任意内容也可以是双引号。通常情况下,这并不是很有用。通过在句尾加上一个问号,可以使得字符串重复不再匹配最长字符。
你可以使用|来分隔可以匹配的不同选择:
简化下列正则表达式:
s|t|u|v|w
aa|ab|ba|bb
[abc]|[^abc]
[^ab]|[^bc]
[ab][ab][ab]?[ab]?
答案
[s-w]
[ab]{2}
.
[^b]
[ab]{2,4}
使用正则表达式匹配1到31之间的整数,[1-31]不是正确答案!
这样的正则表达式不唯一. [1-9]|[12][0-9]|3[01]
是其中之一。
你可以使用括号表示分组:
在《时间机器中》找到一对括号中的内容,然后通过修改正则表达式,找到不含括号的内容。
.∗
. 然后是, [()]∗
.
分组可以包括空字符串:
你也可以在分组的基础上使用重复:
简化正则表达式 \w+\W+\w+\W+\w+
以及 \w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+
.
\w+(\W+\w+){2}
, \w+(\W+\w+){5}
.
在单词和非单词之间有单词分隔符。记住,一个单词\w是[0-9A-Za-z_],而非单词字符是\W(大写),表示[^0-9A-Za-z_].
在文本的开头和结尾通常也有单词分隔符。
在输入文本it’s a cat中,实际有八个单词分隔符。如果我们在cat之后在上一个空格,那就有九个单词分隔符。.
单词分隔符本身并不是字符。它们的宽度为0。下列正则表达式的作用不同
(\bcat)\b
(\bcat\b)
\b(cat)\b
\b(cat\b)
在词典中找到最长的单词。
在尝试之后发现,\b.{45,}\b可以在字典中找到最长单词
一篇文本中可以有一行或多行,行与行之间由换行符分隔,比如:
注意,所有的文本都是以一行结束的,而不是以换行符结束。但是,任意一行都可能为空,包括最后一行。
行的起始位置,是在换行符和下一行首字符之间的空间。考虑到单词分隔符,文本的起始位置也可以当做是首行位置。
最后一行是最后一行的尾字符和换行符之间的空间。考虑到单词分隔符,文本的结束也可以认为是行的结束。
那么新的格式表示如下:
基于上述概念:
^.*&
表示匹配全文内容,因为行的开始符号也是一个字符,"."会匹配这个符号。找到单独的一行,可以使用
^.*?$与字符分隔符一样,换行符也不是字符。它们宽度为0.如下所示的正则表达式作用不同:
(^cat)$
(^cat$)
^(cat)$
^(cat$)
使用正则表达式在《时间机器》中找到最长的一行。
使用正则表达式^.{73,}$可以匹配长度为73的一行
在很多的正则表达式实现中,将^和$作为文本的开始符号和结束符号。
还有一些实现中,用\A和\z作为文本的开始和结束符号。
从这里开始,正则表达式真正体现出了它的强大。
你已经知道了使用括号可以匹配一组符号。使用括号也可以捕获子串。假设正则表达式是一个小型计算机程序,那么捕获子串就是它输出的一部分。
正则表达式(\w*)ility表示匹配以ility结尾的词。第一个被捕获的部分是由\w*控制的。比如,输入的文本内容中有单词accessibility,那么首先被捕获的部分是accessib。如果输入的文本中有单独的ility,则首先被捕获的是一个空字符串。
你可能会有很多的捕获字符串,它们可能靠得很近。捕获组从左向右编号。也就是只需要对左括号计数。
假设有这样的正则表达式:(\w+) had a ((\w+) \w+)
输入的内容是:I had a nice day
在一些正则表达式的实现中,你可以从零开始编号,编号零表示匹配整句话:I had a nice day
.
在其他的实现中,如果没有制定捕获组,那么捕获组1会自动地填入捕获组0的信息。
是的,这也意味着会有很多的括号。有一些正则表达式的实现中,提供了“非捕获组”的语法,但是这样的语法并不是标准语法,因此我们不会介绍。
从一个成功的匹配中返回的捕获组个数,与使用原来的正则表达式获得的捕获组个数相同。记住这一点,你可以解释一些奇怪的现象。.
正则表达式((cat)|dog)表示匹配cat或者dog。这里有两个捕获组,如果输入文本是dog,那么捕获组1是dog,捕获组2为空。
正则表达式a(\w)*表示匹配一个以a开头的单词。这里只有一个捕获组
假如你使用了一个正则表达式去匹配字符串,你可以描述另外一个字符串来替换其中的匹配字符。用来替换的字符串称为替换表达式。它的功能类似于
将《时间机器》中所有的元音字母替换为r。
使用正则表达式[aeiou]以及[AEIOU],对应的替换字符串分别为r,R.
但是,你可以在替换表达式中引用捕获组。这是在替换表达式中,你可以唯一操作的地方。这也是非常有效的,因为这样你就不用重构你找到的字符串。
假设你正在尝试将美国风格的日期表示MM/DD/YY替换为ISO 8601日期表示YYYY-MM-DD
20\3-\1-\2
.2005-03-04
.在替换表达式中,你可以多次使用捕获组
在某些实现中,采用美元符号$代替\
使用正则表达式和替换表达式,将23h59这样的时间戳转化为23:59.
正则表达式finds the timestamps, 替换表达式\1:\2
在一个正则表达式中,你也可以引用捕获组。这称作:反向引用
比如,[abc]{2}表示匹配aa或者ab或者ac或者ba或者bb或者bc或者ca或者cb或者cc.但是{[abc]}\1表示只匹配aa或者bb或者cc.
在字典中,找到包含两次重复子串的最长单词,比如papa
, coco
\b(.{6,})\1\b
匹配 chiquichiqui
.
如果我们不在乎单词的完整性,我们可以忽略单词的分解,使用正则表达式 (.{7,})\1
匹配
countercountermeasure
以及 countercountermeasures
.
特别提醒:
在一些编程语言,比如Java中,对于包含正则表达式的字符串没有特殊标记。字符串有着自己的过滤规则,这是优先于正则表达式规则的,这是频繁使用反斜杠的原因。
比如在Java中
"[^"]*"
变为String re = “\”[^\”]*\””\[\]
] 变为String re = “[\\\\\
String re = "\\s";
和String re = "[ \t\r\n]";
是等价的. 注意它们实际执行调用时的层次不同。在其他的编程语言中,正则表达式是由特殊标明的,比如使用/。下面是JavaScript的例子:
var regExp = /\d/;
.var regExp = /[\\\[\]
]/;
var regExp = /\s/;
和 var regExp = /[ \t\r\n]/;
是等价的var regExp = /https?:\/\//;
.我希望现在你能明白,我为什么让你特别注意反斜杠。
当你动态创建一个正则表达式的时候请特别小心。如果你使用的字符串不够完善的花,可能会有意想不到的匹配结果。这可能导致语法错误,更糟糕的是,你的正则表达式语法正确,但是结果无法预料。
错误的Java代码:
String sep = System.getProperty(“file.separator”); String[] directories = filePath.split(sep);
Bug:String.split() 认为sep是一个正则表达式。但是,在Windows中,Sep是表示匹配一个反斜杠,也就是与正则表达式”\\”相同。这个正则表达式是正确的,但是会返回一个异常:PatternSyntaxException
.
任何好的编程语言都会提供一种良好的机制来跳过字符串中所有的元字符。在Java中,你可以这样实现:
String sep = System.getProperty(“file.separator”);
String[] directories = filePath.split(Pattern.quote(sep));
将正则表达式字符串加入反复运行的程序中,是一种开销很大的操作。如果你可以在循环中避免使用正则表达式,你可以大大提高效率。
在一个网站上,我输入了我的卡号比如 1234 5678 8765 4321
网站拒绝接收。因为它使用了正则表达式\d{16}。
正则表达式应该考虑到用户输入的空格和短横线。
实际上,为什么不先过滤掉所有的非数字字符,然后再进行有效性验证呢?这样做,可以先使用\D以及空的替换表达式。
在不先过滤掉所有的非数字字符的情况下,使用正则表达式验证卡号的正确性。
\D*(\d\D*){16}
is one of several variations which would accomplish this.
不要使用正则表达式来验证姓名。实际上,即使可以,也不要企图验证姓名。
程序员对名字的错误看法:
不要使用正则表达式验证邮箱地址的正确性。
首先,这样的验证很难是精确的。电子邮件地址是可以用正则表达式验证的,但是表达式会非常的长并且复杂。
短的正则表达式会导致错误。(你知道吗?电子邮箱地址中会有一些注释)
第二,即使一个电子邮件地址可以成功匹配正则表达式,也不代表这个邮箱实际存在。邮箱的唯一验证方法,是发送验证邮件。
在严格的应用场景中,不要使用正则表达式来解析HTML或者XML。解析HTML或者XML:
找到一个已经有的解析库来完成这个工作
总结:
a
b
c
d
1
2
3
4
etc..
[abc]
[a-z]
\d
\w
\s
.
代表任何字符\d
表示
“数字”\w
表示”字母”, [0-9A-Za-z_]
\s
表示 “空格, 制表符,回车或换行符”[^abc]
\D
\W
\S
{4}
{3,16}
{1,}
?
*
+
?
表示 “零次或一次”*
表示 “大于零次”+
表示 “一次或一次以上”(Septem|Octo|Novem|Decem)ber
\b
^
$
\A
\z
\1
\2
\3
etc. (在匹配表达式和替换表达式中都可用).
\
[
]
{
}
?
*
+
|
(
)
^
$
[
]
\
-
^
\