Java正则表达式进阶教程之构造方法

原文发于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
制表符 (‘\u0009′)
\n
新行(换行) 符 (‘\u000A’)
\r
回车符 (‘\u000D’)
\f
换页符 (‘\u000C’)
\a
报警(bell)符 (‘\u0007′)
\e
转义符 (‘\u001B’)
\v
垂直制表符 (‘\u000B’)
控制字符: \cchar

Java可以使用\cchar匹配控制字符。其中char的值为64 ^ 控制字符,比如对于退格符(‘\b‘),其ASCII码为8,则char64 ^ 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)都没有谈到补集的概念,而是作为基本的字符类,但我觉得成为补集操作更加便于理解。
并集
可以在字符类中以字符类的方式来进行并集操作,比如对于[123456],可以表示为[123[456]]、[[123][456]、[[1][2][3][4][5][6]]、[[1[2]][3][4[5]6]]。并集操作时隐式的,没有特别的操作符,只需要按次序排在一起就OK了。
交集
交集操作可以保留两个字符类的共同部分,它要求两个字符类之间添加交集运算符 &&。例如[[1-5]&&[3-9]]等价于[3-5]。通过环视功能可以模拟交集运算,比如 (?=[1-5])[3-9][3-9](?<=[1-5])都等价于[3-5]。
差集
Java本身不支持差集元算,但是通过 交集+补集的形式来实现。比如[[0-9] && [^3-5]]等价于[0-26-9]。同样,也可以通过环视功能来模拟差集,比如 (?![3-5])[0-9][0-9](?<![3-5])都等价于[0-26-9]。

Java在解析字符类时,会按照如下的次序依次执行:

  1. 字面值转义 \x
  2. 分组 [...]
  3. 范围 a-z
  4. 并集 [a-e][i-u]
  5. 交集 [a-z&&[aeiou]]
  6. 补集 [^...]
点号:.

点号可以用来匹配除了行结束符之外的任意字符。但是,如果启用了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

部分Unicode区块列表( 查看WIKI)
属性说明
\p{InBASIC_LATIN}Basic Latin
\p{InCJK_COMPATIBILITY}中日韩兼容文字
更多请查看JavaDoc
基本的POSIX字符属性表( 查看标准定义)
属性说明
\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])
Java补充的字符属性表(定义于Character类)
属性说明
\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()
Unicode字符属性表(查看 标准定义)
属性说明
\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}+

占有优先与固化分组类似,一旦匹配某些内容,将不会交还这些内容,除非整个子表达式被弃用。往往可以使用占有量词来优化正则表达式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值