原文发于http://blog.thihy.info/post/119,转载请注明出处。
本文是在学习正则表达式过程中整理的,虽然冠以“教程”,但实际上应该算是学习笔记。整篇文章需要对正则有一定的理解。。如果有啥写得不对的,或者写得不够清楚的,欢迎大家留言讨论。
概述
正则表达式(Regular Expression)是高效的、便捷的文本处理工具,能够快速查询符合某种规范的文本。
例如:[0-9]{3}
可以匹配3位数字,[a-z]{3}
则可以匹配3个小写字母。
目前正则表达式被众多工具所支持,比如egrep、sed、perl、ruby、Java、C#、python、Tcl等,不同的工具下,正在表达式的范式可能会有略微的差别,执行引擎也可能不同。目前,正则引擎主要有:DFA, 传统型NFA, POSIX NFA, DFA/NFA混合。本文主要介绍Java正则表达式,它的引擎属于传统型NFA。
Java正则支持Unicode,它在适当的时候,会使用java.lang.Character.codePointAt(CharSequence seq, int index)
获取Code Point
,而不是char。
构造方法
Java中的正则表达式的构造方法可以看Pattern的javadoc文档。
字符
对于可见字符,可以直接编写,这没啥难点。对于其它难以描述的字符,Java提供了一些表示方法。
字符缩略表示法
Java执行使用\x
来代表特殊的含义,有:
\\
\t
\n
\r
\f
\a
\e
\v
控制字符: \cchar
Java可以使用\cchar
匹配控制字符。其中char
的值为64 ^ 控制字符
,比如对于退格符(‘\b
‘),其ASCII码为8,则char
为64 ^ 8 = 72
,也即H
(H的ASCII码为72)。简单地,对于ASCII码小于64的控制字符,char
为控制字符的ASCII码加上64。
八进制表示:\0n
, \0nn
, \0mnn
Java中,八进制表示必须以\0开始(防止与反向引用混淆),这点可能与其他工具不同(某些工具有规则来区分反向引用和八进制)。\0后面的部分最多只能有3个数字,而n必须在区间[0,7]内,m必须在[0,3]内,所以最大表示\0377。
十六进制表示:\xhh
十六进制后面必须是两个十六进制字符,也即h必须是0~9,a-f或A-F。\x00和\xff都是合法的,但是\xa,\x0ab,\x1g都是不合法的。
Unicode转义:\uhhhh
Unicode转义的形式与十六进制类似,只是后面必须是四个六进制字符。
行结束符
行结束符是用来标记输入字符序列的行结尾,可能有一个或两个字符。在Java中,行结束符包括:
- 新行(换行)符 (‘\n’)
- 后面紧跟新行符的回车符 (“\r\n”)
- 单独的回车符 (‘\r’)
- 下一行字符 (‘\u0085′)
- 行分隔符 (‘\u2028′)
- 段落分隔符 (‘\u2029)
如果启用了UNIX_LINES模式,则新行符(即’\n’)是惟一识别的行结束符。
只有启用DOTALL标志,正则表达式中的点号(即’.')才会匹配行结束符。
默认情况下,正则表达式^和$会忽略行结束符,仅分别与整个输入序列的开头和结尾匹配。如果激活 MULTILINE 模式,则 ^ 在输入的开头和行结束符之后(输入的结尾)才发生匹配。处于 MULTILINE 模式中时,$ 仅在行结束符之前或输入序列的结尾处匹配。
字符类
字符类: [...]
字符类的形式是[...]
,其内部可以是若干字符,也可以是一个字符范围。比如[a]表示匹配a字符,[a-z]表示匹配所有小写的英文字母。字符范围中的字符可以是Unicode字符,并且不要求是同一类的,也即[a-}]
也是可以的,甚至是[a-星]
。
字符类集合运算
字符类可以进行补集、并集(隐式)、交集和差集的运算。所有的集合运算都必须在字符类内部实现。
^
开头,则表示是一个补集。比如[^a]表示匹配不是a的所有字符,这与[a^]是不同的,后者表示匹配a字符或^字符。既然是补集,那么需要明确全集是什么。由于Java是支持Unicode的,所以全集是所有的Unicode字符。也即[^a]可以匹配汉字字符。
注:很多书籍上(包括JavaDoc)都没有谈到补集的概念,而是作为基本的字符类,但我觉得成为补集操作更加便于理解。
&&
。例如[[1-5]&&[3-9]]等价于[3-5]。通过环视功能可以模拟交集运算,比如
(?=[1-5])[3-9]
和
[3-9](?<=[1-5])
都等价于[3-5]。
(?![3-5])[0-9]
和
[0-9](?<![3-5])
都等价于[0-26-9]。
Java在解析字符类时,会按照如下的次序依次执行:
- 字面值转义 \x
- 分组 [...]
- 范围 a-z
- 并集 [a-e][i-u]
- 交集 [a-z&&[aeiou]]
- 补集 [^...]
点号:.
点号可以用来匹配除了行结束符之外的任意字符。但是,如果启用了DOTALL标志,则可以匹配行结束符。
1
2
3
|
// (?s)会启用DOTALL标志
assertEquals(
true
,
"\n"
.matches(
"(?s)."
));
assertEquals(
true
,
"\r"
.matches(
"(?s)."
));
|
字符类简记法:
Java预定义了如下几种字符组,可以很方便地使用。
-
\d
数字:[0-9] -
\D
非数字:[^0-9] -
\s
空白字符:[ \t\n\x0B\f\r] (注意第一个字符是空格) -
\S
非空白字符:[^\s] -
\w
单词字符:[a-zA-Z_0-9] -
\W
非单词字符:[^\w]
Unicode属性和区块:\p{PropOrBlock
、\P{PropOrBlock
\p{PropOrBlock
表示匹配符合PropOrBlock的所有字符,大写的\P{PropOrBlock
则匹配不符合PropOrBlock的所有字符。
PropOrBlock包括字符属性(Char Property)和区块(Block)。区块必须以In
开头,字符属性可以以Is
开头(可选)。Unicode 区块的定义在java.lang.Character.UnicodeBlock
,具体可以查看Unicode标准。字符属性的定义在java.util.regex.Pattern.CharPropertyNames
。
\p{InBASIC_LATIN} | Basic Latin |
\p{InCJK_COMPATIBILITY} | 中日韩兼容文字 |
更多请查看JavaDoc |
\p{ASCII} | ASCII字符: 0×00~0x7F |
\p{Lower} | 小写字母([a-z]) |
\p{Upper} | 小写字母([A-Z]) |
\p{Punct} | ASCII标点符号 |
\p{Alpha} | ASCII字母([a-zA-Z]) |
\p{Digit} | 数字([0-9] |
\p{Alnum} | ASCII字母和数字: [a-zA-Z0-9]) |
\p{Graph} | ASCII可打印(可见)字符: [\p{Alnum}\p{Punct}] |
\p{Blank} | ASCII Blank字符(空格和Tab字符) |
\p{Cntrl} | ASCII控制字符([\x00-\x1F\x7F]) |
\p{Print} | 可打印字符(0×20~0x7E) |
\p{Space} | ASCII Space字符([ \t\n\x0B\f\r]) |
\p{XDigit} | ASCII十六进制字符([0-9a-fA-F]) |
\p{javaLowerCase} | 等效于 java.lang.Character.isLowerCase() |
\p{javaUpperCase} | 等效于 java.lang.Character.isUpperCase() |
\p{javaTitleCase} | 等效于 java.lang.Character.isTitleCase() |
\p{javaDigit} | 等效于 java.lang.Character.isDigit() |
\p{javaDefined} | 等效于 java.lang.Character.isDefined() |
\p{javaLetter} | 等效于 java.lang.Character.isLetter() |
\p{javaLetterOrDigit} | 等效于 java.lang.Character.isLetterOrDigit() |
\p{javaJavaIdentifierStart} | 等效于 java.lang.Character.isJavaIdentifierStart() |
\p{javaJavaIdentifierPart} | 等效于 java.lang.Character.isJavaIdentifierPart() |
\p{javaUnicodeIdentifierStart} | 等效于 java.lang.Character.isUnicodeIdentifierStart() |
\p{javaUnicodeIdentifierPart} | 等效于 java.lang.Character.isUnicodeIdentifierPart() |
\p{javaIdentifierIgnorable} | 等效于 java.lang.Character.isIdentifierIgnorable() |
\p{javaSpaceChar} | 等效于 java.lang.Character.isSpaceChar() |
\p{javaISOControl} | 等效于 java.lang.Character.isISOControl() |
\p{javaWhitespace} | 等效于 java.lang.Character.isWhitespace() |
\p{javaMirrored} | 等效于 java.lang.Character.isMirrored() |
\p{L} | Letter: 字母 |
\p{M} | Mark: 标记字符,不能单独出现,必须与其他基本字符同时出现(如重音符号)。 |
\p{N} | Number: 各种数字字符 |
\p{Z} | Separator: 分隔字符,但是本身不可见(比如空格) |
\p{P} | Punctuation: 标点符号 |
\p{S} | Symbol: 各种图形符号和字母符号 |
\p{C} | Other: 其他任何字符 |
\p{Ll} | Character.LOWERCASE_LETTER: 小写字母 |
\p{Lu} | Character.UPPERCASE_LETTER: 大写字母 |
\p{Lt} | Character.TITLECASE_LETTER: 出现在单词开头的字母 |
\p{Lm} | Character.MODIFIER_LETTER: 少数形似字母的,有特别用途的字符 |
\p{Lo} | Character.OTHER_LETTER: 没有大小写形式,也不属于修饰符的字母。 |
\p{LC} | L1,Lu,Lt |
\p{LD} | LC,Lm,Lo,Nd |
\p{L1} | Latin-1(0×00~0xff) |
\p{Mn} | Character.NON_SPACING_MARK |
\p{Mc} | Character.COMBINING_SPACING_MARK |
\p{Me} | Character.ENCLOSING_MARK |
\p{Nd} | Character.DECIMAL_DIGIT_NUMBER |
\p{Nl} | Character.LETTER_NUMBER |
\p{No} | Character.OTHER_NUMBER |
\p{Zs} | Character.SPACE_SEPARATOR |
\p{Zl} | Character.LINE_SEPARATOR |
\p{Zp} | Character.PARAGRAPH_SEPARATOR |
p{Cc} | Character.CONTROL |
p{Cf} | Character.FORMAT |
p{Co} | Character.PRIVATE_USE |
p{Cs} | Character.SURROGATE |
p{Pd} | Character.DASH_PUNCTUATION |
p{Ps} | Character.START_PUNCTUATION |
p{Pe} | Character.END_PUNCTUATION |
p{Pc} | Character.CONNECTOR_PUNCTUATION |
p{Po} | Character.OTHER_PUNCTUATION |
p{Sm} | Character.MATH_SYMBOL |
p{Sc} | Character.CURRENCY_SYMBOL |
p{Sk} | Character.MODIFIER_SYMBOL |
p{So} | Character.OTHER_SYMBOL |
p{Pi} | Character.INITIAL_QUOTE_PUNCTUATION |
p{Pf} | Character.FINAL_QUOTE_PUNCTUATION |
p{all} | 所有字符 |
锚点及其他“零长度断言”
锚点及其他“零长度断言”都是用来匹配一个位置,而不是具体的字符。
起始位置:^、\A
脱字符(^)和\A会匹配输入文本的起始位置。但是如果启用了MULTILINE模式,^
还可以匹配每个行结束符之后的位置。
// \A会匹配起始位置
assertEquals(
true
,
"abcde"
.matches(
"\\Aabcde"
));
// MULTILINE模式下,\A仍然只匹配起始位置,而不会匹配行结束符之后的位置!
assertEquals(
false
,
"\nabcde"
.matches(
"(?m)\n\\Aabcde"
));
// ^ 会匹配起始位置
assertEquals(
true
,
"abcde"
.matches(
"^abcde"
));
// MULTILINE模式下,^会匹配行结束符之后的位置!
assertEquals(
true
,
"\nabcde"
.matches(
"(?m)\n^abcde"
));
// 甚至这样
assertEquals(
true
,
"\na\nbcde"
.matches(
"(?m)\n^a\n^bcde"
));
|
结束位置:$、\Z和\z
$
比较复杂,它的含义依赖于MULTILINE模式是否启用。再次明确一下,行结束符会根据UNIX_LINES模式是否启用而变化。
- 未启用MULTILINE模式(默认)
$待匹配位置之后要么没有任意字符(即严格的结尾),要么只是行结束符,但需要要注意的是,在非UNIX_LINES模式下,它不能在\r\n之间。// 匹配严格的结尾
assertEquals(
true
,
"abcde"
.matches(
"abcde$"
));
// 匹配严格的结尾
assertEquals(
true
,
"abcde\r"
.matches(
"abcde\r$"
));
assertEquals(
true
,
"abcde\r\n"
.matches(
"abcde\r\n$"
));
// 匹配行结束符之前的位置
assertEquals(
true
,
"abcde\r"
.matches(
"abcde$\r"
));
assertEquals(
true
,
"abcde\n"
.matches(
"abcde$\n"
));
assertEquals(
true
,
"abcde\u0085"
.matches(
"abcde$\u0085"
));
assertEquals(
true
,
"abcde\u2028"
.matches(
"abcde$\u2028"
));
assertEquals(
true
,
"abcde\u2029"
.matches(
"abcde$\u2029"
));
assertEquals(
true
,
"abcde\r\n"
.matches(
"abcde$\r\n"
));
// 启用UNIX_LINES模式后,行结束符只有\n
assertEquals(
false
,
"abcde\r"
.matches(
"(?d)abcde$\r"
));
assertEquals(
true
,
"abcde\n"
.matches(
"(?d)abcde$\n"
));
assertEquals(
false
,
"abcde\u0085"
.matches(
"(?d)abcde$\u0085"
));
assertEquals(
false
,
"abcde\u2028"
.matches(
"(?d)abcde$\u2028"
));
assertEquals(
false
,
"abcde\u2029"
.matches(
"(?d)abcde$\u2029"
));
assertEquals(
false
,
"abcde\r\n"
.matches(
"(?d)abcde$\r\n"
));
// 启用UNIX_LINES模式后,$可以在\r\n之间
assertEquals(
true
,
"abcde\r\n"
.matches(
"(?d)abcde\r$\n"
));
// 还可以这样匹配
assertEquals(
true
,
"abcde\r\r\n"
.matches(
"abcde\r$\r\n"
));
assertEquals(
true
,
"abcde\n\r\n"
.matches(
"abcde\n$\r\n"
));
// 甚至是这样
assertEquals(
true
,
"abcde\r\n"
.matches(
"abcde$\r\n$"
));
assertEquals(
true
,
"abcde\r\n"
.matches(
"abcde$$\r\n$$"
));
// 但不能匹配
assertEquals(
false
,
"abcde\r\n"
.matches(
"abcde\r$\n"
));
// 不能匹配\r\n之间的位置
assertEquals(
false
,
"abcde\r\n\n"
.matches(
"abcde\r$\n\n"
));
// 不能匹配\r\n之间的位置
assertEquals(
false
,
"abcde\n\n"
.matches(
"abcde$\n$\n$"
));
// 第一个$不满足条件
- 启用MULTILINE模式
$能够匹配输入文本的严格末尾,或者行结束符之前的位置,需要注意的是,在非UNIX_LINES模式下,它同样不能在\r\n之间。// 非MULTILINE模式下的所有正则表达式,它都可以匹配成功。
// 但是从下面这个断言可以看出区别
assertEquals(
true
,
"abcde\n\n"
.matches(
"(?m)abcde$\n$\n$"
));
// 第一个$也满足条件
其实MULTILINE模式的启用只是允许$匹配文本中间的行(要不也不叫多行模式了)。
\Z等价于未启用MULTILINE模式的$。
// 与未启用MULTILINE模式的$一样
assertEquals(
true
,
"abcde"
.matches(
"(?m)abcde\\Z"
));
assertEquals(
true
,
"abcde\n"
.matches(
"(?m)abcde\\Z\n"
));
// 即使正则表达式启用了MULTILINE模式,也不能匹配成功
assertEquals(
false
,
"abcde\n\n"
.matches(
"(?m)abcde\\Z\n\\Z\n\\Z"
));
|
\z则匹配输入文本的严格末尾,也即要求待匹配位置后面不能有任何字符(包括行结束符)。
// 匹配末尾
assertEquals(
true
,
"abcde\n"
.matches(
"abcde\n\\z"
));
// 后面不能有任何字符,所以不能匹配
assertEquals(
false
,
"abcde\n"
.matches(
"abcde\\z\n"
));
|
上次匹配成功的结束位置:\G
在迭代匹配中,有时候需要从上次匹配成功的结束位置继续匹配,就跟循环执行\A一样。
在每次成功匹配之后,Java会保存此次匹配的结束位置(见Matcher.last)。下次匹配时,如果上次起始位置与上次结束位置一样,则强制前进一个字符,防止无穷循环。如果要求匹配\G,则会比较当前的位置是否与上次结束位置相同,相同则匹配成功,否则匹配不成功。
1
|
assertEquals(
"!a!b!c!d!e!"
,
"abcde"
.replaceAll(
"x?"
,
"!"
));
|
单词分界符:\b、\B
单词分界符\b用来匹配单词的边界,边界要求一边是单词字母,另一边不是单词字母。所谓单词字母,包括下划线(‘_’)、大小写字母、数字和非空格标记字符(Character.NON_SPACING_MARK),也即[\w\p{Mn}]。
\B则匹配不是单词边界的位置。
Java不区分左分界、右分界,而是笼统的边界。可以使用下面介绍的环视功能来区分左右边界,比如(?
顺序环视(?=…)、(?!…);逆序环视(?<=…)、(?<!…)
环视功能可以从当前位置向左或向右匹配执行的正则子表达式。向左查看称作逆序环视,向右查看称作顺序环视。
举例说明,对于字符串1234223432344234,(?<=2)234可以匹配到1234223432344234。
逆序环视要求长度是确定的,也即最大长度不能使无穷的。比如(?<=books?)是可以的,因为其最大长度是5,但(?<=\w+)是不行的。
注释和模式修饰符
模式修饰符:(?modifier),如(?i)和(?-i)
Java允许在正则表达式中使用模式修饰符来设定匹配模式,(?x)
开启x模式,(?-x)
关闭x模式。如果模式修饰符在括号内部,则其作用范围仅限于括号内部。
比如<B>(?i)text(?-i)</B>
,要求两边的TAG必须为大写,而启用的内容text则不关系大小写。<B>(?:(?i)text)</B>
也是相同的含义,因为(?i)只作用于括号内部。
// <B>(?i)text(?-i)</B>
assertEquals(
true
,
"<B>text</B>"
.matches(
"<B>(?i)text(?-i)</B>"
));
assertEquals(
true
,
"<B>TEXT</B>"
.matches(
"<B>(?i)text(?-i)</B>"
));
assertEquals(
false
,
"<b>text</B>"
.matches(
"<B>(?i)text(?-i)</B>"
));
// <B>(?:(?i)text)</B>
assertEquals(
true
,
"<B>text</B>"
.matches(
"<B>(?:(?i)text)</B>"
));
assertEquals(
true
,
"<B>TEXT</B>"
.matches(
"<B>(?:(?i)text)</B>"
));
assertEquals(
false
,
"<b>text</B>"
.matches(
"<B>(?:(?i)text)</B>"
));
|
i | 对应于Pattern.CASE_INSENSITIVE标志: 不区分大小写 |
d | 对应于Pattern.UNIX_LINES标志:只有\n 被视为行结束符,\r 等不再被视为行结束符 |
u | 对应于Pattern.UNICODE_CASE标志:当启用CASE_INSENSITIVE模式时,忽略大小写时会支持Unicode字符,而不仅仅是ASCII字符。具体地,此标志下,会使用Character.toUpperCase/toLowerCase来转换大小写。 |
x | 对应于Pattern.COMMENTS标志:忽略空白字符(即\s代表的字符),忽略#和行结束符之前的内容(同时也忽略行结束符)。 |
m | 对应于Pattern.MULTILINE标志:使得^和$可以匹配文本中间的行结束符之前和之后的位置。具体见^和$。 |
s | 对应于Pattern.DOTALL标志:点号. 可以匹配行结束符 |
局部模式修饰符:(?modifier:…),如(?i:…)
要限制模式修饰符的作用范围,除了将之放于括号内部,也可以简单地使用(?x:…)形式。比如<B>(?:(?i)text)</B>
可以简化成<B>(?i:text)</B>
。需要注意的是,虽然与括号形式类似,但它并不是捕获组,无法捕获内容。
文本范围: \Q…\E
\Q…\E会把其内部的字符当作普通文本来对待,而不是视为正则表达式。特别需要注意的是内部文本不能包含\E,可以使用\Q…\E\\E\Q…\E来代替。
1
2
3
4
|
// match: \s <--- \Q\s\E
assertEquals(
true
,
"\\s\\t"
.matches(
"\\Q\\s\\t\\E"
));
// match: \E <--- \Q\s\E\\E\Q\t\E
assertEquals(
true
,
"\\s\\E\\t"
.matches(
"\\Q\\s\\E\\\\E\\Q\\t\\E"
));
|
分组和捕获
捕获/分组括号:(…)和\1、\2、…
普通的没有特殊含义的括号通用用于分组和捕获,形式为(...)
。
对于捕获的分组,可以使用反向引用来获取子表达式匹配的文本。Java使用\1、\2、…形式的反向引用,后面的数字表示分组的编号。分组编号是按照左括号(
出现的次序来分配的。在Java中,分组编号的数目是没有限制的。
1
2
3
4
5
6
|
// 【样例1】捕获分组1:abc
assertEquals(
true
,
"abc1abc"
.matches(
"(\\w+)1\\1"
));
// 【样例2】捕获分组1:a,分组2:b,分组3:c
assertEquals(
true
,
"abc1cba"
.matches(
"(\\w)(\\w)(\\w)1\\3\\2\\1"
));
// 【样例3】一个很奇怪的例子,这与Java在匹配分组循环时回溯逻辑有关,具体原因不便讨论。
assertEquals(
true
,
"abc11"
.matches(
"(\\w)+1\\1"
));
|
仅分组不捕获的括号:(?:…)
(?:…)仅用于分组,但不能用来提取文本。
固化分组:(?>…)
固化意思是说,一旦括号内的子表达式匹配成功之后,匹配的内容就被固化,在接下来的匹配过程中是不变的,除非整个子表达式被弃用。
1
2
3
|
assertEquals(
true
,
"1abcde2"
.matches(
"1.*2"
));
// .*匹配到abcde2之后,将会被固化,但是之后的2无法匹配成功,需要强迫.*释放最后匹配的内容(即e),但是由于是固化分组,这个操作无法实现。
assertEquals(
false
,
"1abcde2"
.matches(
"1(?>.*)2"
));
|
多选分支:…|…|…
多选分支可以用来在同一个位置测试多个子表达式。Java会按照从左到右的次序来匹配。子表达式可以为空表达式,比如(?:abc|)等价于(?:abc)?。
量词
匹配优先量词:*、+、?、{num,num}
两次可以限制作用对象的匹配次数。*
表示匹配次数零次或多次,+
表示匹配次数一次或多次,?
表示匹配次数零次或一次,{cmin,cmax}表示匹配次数为cmin到cmax次(均包含边界)。
需要注意,X{0,0}的意思不是“不出现X”,而是不进行任何匹配,也即跟没有是一样的。如果要实现“不出现X”,请使用否定环视功能。
忽略优先量词:*?、+?、??、{num,num}?
默认情况下,量词是匹配优先的,也即匹配尽可能多的内容。而忽略优先则相反,会匹配尽可能少的内容。
占有优先量词:*+、++、?+、{num,num}+
占有优先与固化分组类似,一旦匹配某些内容,将不会交还这些内容,除非整个子表达式被弃用。往往可以使用占有量词来优化正则表达式。