相关阅读

  1. Java基础:String类
  2. Java字符串格式化
  3. Java基础:正则表达式

1. 正则表达式概述

在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。

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

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

正则表达式是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,这个“规则字符串”用来表达对字符串的一种过滤逻辑

1.1 给定一个正则表达式和另一个字符串,我们可以达到如下的目的

  • 给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”)
  • 可以通过正则表达式,从字符串中获取我们想要的特定部分

1.2 正则表达式的特点

  • 灵活性、逻辑性和功能性非常的强;
  • 可以迅速地用极简单的方式达到字符串的复杂控制。
  • 对于刚接触的人来说,比较晦涩难懂。

由于正则表达式主要应用对象是文本,因此它在各种文本编辑器场合都有应用,小到著名编辑器EditPlus,大到Microsoft Word、Visual Studio等大型编辑器,都可以使用正则表达式来处理文本内容

2. 正则表达式基础知识

2.1 元字符

正则表达式主要依赖于元字符。

元字符不代表他们本身的字面意思, 他们都有特殊的含义。 一些元字符写在方括号中的时候有一些特殊的意思。 以下是一些元字符的介绍:

元字符 描述
. 句号匹配任意单个字符除了换行符
[ ] 字符种类. 匹配方括号内的任意字符
[^ ] 否定的字符种类. 匹配除了方括号里的任意字符
* 匹配>=0个重复的在*号之前的字符
+ 匹配>=1个重复的+号前的字符
? 标记?之前的字符为可选
{n,m} 匹配num个中括号之前的字符 (n <= num <= m)
(xyz) 字符集, 匹配与 xyz 完全相等的字符串
\ 或运算符,匹配符号前或后的字符
\ 转义字符,用于匹配一些保留的字符 `[ ] ( ) { } . * + ? ^ $ \ `
^ 从开始行开始匹配
$ 从末端开始匹配

2.2 常见符号

2.2.1 字符

符号 说明
X 字符X
\\ 反斜线
\t 制表符 (‘\u0009’)
\n 回车
\r 换行
\f 换页符 (‘\u000C’)
\a 报警 (bell) 符 (‘\u0007’)

2.2.2 字符类

符号 说明
[abc] a、b或c
[^abc] 任何字符,除了a、b或c
[a-zA-Z] a到z,或A到Z
[0-9] 0到9的字符
[a-d[m-p]] a到 d或 m 到 p:[a-dm-p](并集)
[a-z&&[def]] d、e或 f(交集)
[a-z&&[^bc]] a到 z,除了 b和 c:[ad-z](减去)
[a-z&&[^m-p]] a到 z,而非 m到 p:[a-lq-z](减去)
\u4E00-\u9FA5 中文字符

2.2.3 预定义字符

符号 说明
. 任何字符
\d 数字:[0-9]
\D 非数字: [^0-9]
\s 空白字符:[ \t\n\x0B\f\r]
\S 非空白字符:[^\s]
\w 单词字符:[a-zA-Z_0-9]
\W 非单词字符:[^\w]

2.3 中括号表达式

范围匹配,一种特殊的匹配单个字符的方法。

  • 即[ ]表达式,用来匹配单个字符,只不过这一个字符的所在范围有中括号内的表达式来确定
  • 枚举:[abc],表示匹配a、b、c的任意一个
  • 范围:[a-f],表示匹配a-f之间的任意一个字符,包括边界,其中右边界一定要≥左边界,否则引擎编译错误
  • 并:[a-cm-p],就表示a-c的范围和m-p的范围求并,其实前面的abc之类的枚举也是一种并运算
  • 交:[a-z&&b-d],就表示a-z的范围和b-d的范围求交,结果等于[b-d]
  • 补:[^abc]表示非a、b、c的任意一个字符,[^a-f]表示非a-f的任一字符;^也是一个脱字符,必须是中括号表达式的第一个字符,否则不起任何作用。补的是^后面紧跟的整个表达式。
  • 嵌套:[a-m&&[def]],a-m和[def]都表示范围,因此可以做运算,结果等于[d-f]
  • 有了上面这些基本运算,就可以构造一些很复杂的运算了:[a-z&&[^bc]] == [ad-z],但是注意不要用^构造复杂表达式,以下一些表达式将不起作用!
    • ^后面紧跟一个[ ]嵌套:^[a-d],非法!不起任何作用
    • ^后面是一个运算(交并补):^a-cf-h、^a-z&&c-h、^^a-c,都是非法的!!不起任何作用!!
    • ^后面只能跟单纯的枚举和范围运算!!例如[^azh]、[^a-h]等;

2.4 边界匹配器

符号 说明
^ 行开头
$ 行结尾
\b 单词边界
\B 非单词边界
\A 输入的开头
\G 上一个匹配的结尾
\Z 输入的结尾,仅用于最后的结束符(如果有的话)
\z 输入的结尾

2.4.1 贪婪与懒惰数量词

  • 贪婪数量词
符号 说明
X? 0次或1次
X* 0次以上
X+ 1次以上
X{n} 恰好n次
X{n,} 至少n次
X{n,m} n-m次

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。在贪婪数量词后面加上?就是懒惰匹配。这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b匹配最短的,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(第一到第三个字符)和ab(第四到第五个字符)。

为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins

  • 懒惰限定符
代码/语法 说明
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

2.4.2 分组和捕获

分组可以分为两种形式,捕获组和非捕获组。

1、捕获组

捕获组可以通过从左到右计算其开括号来编号。例如,在表达式 ((A)(B(C)))中,存在四个这样的组:

  • 分组0:((A)(B(C)))
  • 分组1:(A)
  • 分组2:(B(C))
  • 分组3:(C)

正则表达式中每个”()”内的部分算作一个捕获组,每个捕获组都有一个编号,从1,2…,编号0代表整个匹配到的内容,组零始终代表整个表达式。

之所以这样命名捕获组是因为在匹配中,保存了与这些组匹配的输入序列的每个子序列。捕获的子序列稍后可以通过 Back 引用(反向引用) 在表达式中使用,也可以在匹配操作完成后从匹配器检索。

Back引用(\n)是说在后面的表达式中我们可以使用组的编号来引用前面的表达式所捕获到的文本序列。注意:反向引用,引用的是前面捕获组中的文本而不是正则,也就是说反向引用处匹配的文本应和前面捕获组中的文本相同,这一点很重要。

例如 ([" ']).*\1 其中使用了分组,\1就是对引号这个分组的引用,它匹配包含在两个引号或者两个单引号中的所有字符串,如,”abc” 或 “‘ “ 或’ “ ‘ ,但是请注意,它并不会对”a’或者 ‘a”匹配。原因上面已经说明,Back引用只是引用文本而不是表达式。

捕获组的作用就是为了可以在正则表达式内部或者外部(Java方法)引用它。在另一个字符串引用捕获组的内容的方法(“$”),在替换中常用\$匹配组的内容。\$n用来匹配第n个()里的内容。

利用Matcher中的group(int group)获取捕获组内容。

2、捕获组命名

如果捕获组的数量非常多,那都用数字进行编号并引用将会非常混乱,并且难以记忆每个捕获组的内容及意义,因此对捕获组命名显得尤为重要;Java 7开始提供了对捕获组命名的语法,并且可以通过捕获组的名称对捕获组反向引用(内外都行)。

命名捕获组的语法格式:(?<自定义名>expr),例如:(?<year>\d{4})-(?<date>\d{2}-(?<day>\d{2})),有三个命名捕获组year、date和day,从左到右编号分别为1、2、3(编号同样是有效的)

命名捕获组的反向引用:

  • 正则表达式内引用:\k<捕获组名称>,例如:(?<year>\d{4})-\k<year>可以匹配1999-1999
  • 外部引用:Matcher对象的start、end、group的String name参数指定要查询的捕获组的名称。

普通捕获组和命名捕获组的混合编号:

  • 普通捕获组是相对命名捕获组的,即没有显式命名的捕获组;
  • 当所有捕获组都是命名捕获组时那么编号规则和原来相同,即按照左括号(的出现顺序来编号;
  • 当普通捕获组和命名捕获组同时出现时,编号规则为:先不忽略命名捕获组,只对普通捕获组按照左括号顺序编号,然后再对命名捕获组从左往右累计编号,例如:先忽略命名命名捕获组<date>,先对普通捕获组编号\d{4}是1,\d\d是2,然后再接着累加地对命名捕获组编号,因此<date>是3;

3、非捕获组

非捕获组,只需要将捕获组中”()”变为”(?:)”即可。

以 (?) 开头的组是纯的非捕获组,它不捕获文本 ,也不针对组合计进行计数。就是说,如果小括号中以?号开头,那么这个分组就不会捕获文本,当然也不会有组的编号,因此也不存在Back引用。

我们通过捕获组就能够得到我们想要匹配的内容了,那为什么还要有非捕获组呢?原因是捕获组捕获的内容是被存储在内存中,可供以后使用,比如反向引用就是引用的内存中存储的捕获组中捕获的内容。而非捕获组则不会捕获文本,也不会将它匹配到的内容单独分组来放到内存中。所以,使用非捕获组较使用捕获组更节省内存。在实际情况中我们要酌情选用。

2.5 前后关联约束(前后预查)

前置约束和后置约束都属于非捕获簇(用于匹配不在匹配列表中的格式)。 前置约束用于判断所匹配的格式是否在另一个确定的格式之后。

例如, 我们想要获得所有跟在 $ 符号后的数字, 我们可以使用正向向后约束 (?<=\$)[0-9\.]*。这个表达式匹配 $ 开头, 之后跟着 0,1,2,3,4,5,6,7,8,9,. 这些字符可以出现大于等于 0 次。

前后关联约束如下:

符号 描述
?= 前置约束-存在
?! 前置约束-排除
?<= 后置约束-存在
?<! 后置约束-排除

?=exp前置约束(存在)

?=exp 前置约束(存在), 表示第一部分表达式必须跟在 ?=exp 定义的表达式之后.

返回结果只瞒住第一部分表达式. 定义一个前置约束(存在)要使用 (). 在括号内部使用一个问号和等号: (?=exp).

前置约束的内容写在括号中的等号后面. 例如, 表达式 [T|t]he(?=\sfat) 匹配 Thethe, 在括号中我们又定义了前置约束(存在) (?=\sfat) ,即 Thethe 后面紧跟着 (空格)fat.

  1. "[T|t]he(?=\sfat)" => The fat cat sat on the mat.

?!exp前置约束-排除

前置约束-排除 ?! 用于筛选所有匹配结果, 筛选条件为 其后不跟随着定义的格式 前置约束-排除 定义和 前置约束(存在) 一样, 区别就是 = 替换成 ! 也就是 (?!exp).

表达式 [T|t]he(?!\sfat) 匹配 Thethe, 且其后不跟着 (空格)fat.

  1. "[T|t]he(?!\sfat)" => The fat cat sat on the mat.

?<=exp后置约束-存在

后置约束-存在 记作(?<=exp)用于筛选所有匹配结果, 筛选条件为 其前跟随着定义的格式. 例如, 表达式 (?<=[T|t]he\s)(fat|mat) 匹配 fatmat, 且其前跟着 Thethe.

  1. "(?<=[T|t]he\s)(fat|mat)" => The fat cat sat on the mat.

?<!exp后置约束-排除

后置约束-排除 记作 (?<!exp)用于筛选所有匹配结果, 筛选条件为 其前不跟着定义的格式. 例如, 表达式 (?<!(T|t)he\s)(cat)匹配 cat, 且其前不跟着 Thethe.

  1. "(?<![T|t]he\s)(cat)" => The cat sat on cat.

2.5 标志

标志也叫修饰语, 因为它可以用来修改表达式的搜索结果. 这些标志可以任意的组合使用, 它也是整个正则表达式的一部分.

标志 描述
i 忽略大小写.
g 全局搜索.
m 多行的: 锚点元字符 ^ $ 工作范围在每行的起始.

忽略大小写 (Case Insensitive)

修饰语 i 用于忽略大小写. 例如, 表达式 /The/gi 表示在全局搜索 The, 在后面的 i 将其条件修改为忽略大小写, 则变成搜索 theThe, g 表示全局搜索.

  1. "The" => The fat cat sat on the mat.
  2. "/The/gi" => The fat cat sat on the mat.

全局搜索 (Global search)

修饰符 g 常用语执行一个全局搜索匹配, 即(不仅仅返回第一个匹配的, 而是返回全部). 例如, 表达式 /.(at)/g 表示搜索 任意字符(除了换行) + at, 并返回全部结果.

  1. "/.(at)/" => The fat cat sat on the mat.
  2. "/.(at)/g" => The fat cat sat on the mat.

多行修饰符 (Multiline)

多行修饰符 m 常用语执行一个多行匹配.

像之前介绍的 (^,$) 用于检查格式是否是在待检测字符串的开头或结尾. 但我们如果想要它在每行的开头和结尾生效, 我们需要用到多行修饰符 m.

例如, 表达式 /at(.)?$/gm 表示在待检测字符串每行的末尾搜索 at后跟一个或多个 . 的字符串, 并返回全部结果.

  1. "/.at(.)?$/" => The fat
  2. cat sat
  3. on the mat.
  4. "/.at(.)?$/gm" => The fat
  5. cat sat
  6. on the mat.

3. 更多正则表达式教程

learn-regex:一个学习正则的开源项目,由浅入深,很容易入手,而且教程有中文版。

正则表达式30分钟入门教程

Java正则表达式的应用