在写编译的时候发现在词法分析环节可以使用正则表达式来简化分析过程,故先对正则表达式进行学习。由于平时写的最多的还是JS,所以先从JS的正则入手,然后再分析Java和JS中正则的异同。

JS正则表达式

模式和修饰符

在JavaScript中我们有两种创建正则表达式对象的语法:

较长一点的语法:

1
regexp = new RegExp("pattern", "flags");

较短一点的语法,使用斜线 "/"

1
2
regexp = /pattern/; // 没有修饰符
regexp = /pattern/gmi; // 带有修饰符 g、m 和 i

这两种语法之间的主要区别在于,使用斜线 /.../ 的模式不允许插入表达式(如带有 ${...} 的字符串模板)。它是完全静态的。

1
2
let tag = prompt("What tag do you want to find?", "h2");
let regexp = new RegExp(`<${tag}>`);

修饰符

在 JavaScript 中,有 6 个修饰符:

  • i

    使用此修饰符后,搜索时不区分大小写。

  • g

    使用此修饰符后,搜索时会寻找所有的匹配项 —— 没有它,则仅返回第一个匹配项。

  • m

    多行模式。

  • s

    启用 “dotall” 模式,允许点 . 匹配换行符 \n

  • u

    开启完整的 Unicode 支持。

  • y

    粘滞模式,在文本中的确切位置搜索。

搜索

str.match(regexp) 方法在字符串 str 中寻找 regexp 的所有匹配项。

如果有修饰符g,则返回一个由所有匹配项构成的数组;如果没有这样的修饰符,则会以数组形式返回第一个匹配项。

1
2
3
4
5
6
let str = "We will, we will rock you";

let result = str.match(/we/i); // 没有修饰符 g

alert( result[0] ); // We(第一个匹配项)
alert( result.length ); // 1

如果没有匹配组,则返回null

替换

str.replace(regexp, replacement) 方法使用 replacement 替换在字符串 str 中找到的 regexp 的匹配项(如果带有修饰符 g 则替换所有匹配项,否则只替换第一个)。

1
2
3
4
5
// 没有修饰符 g
alert( "We will, we will".replace(/we/i, "I") ); // I will, we will

// 带有修饰符 g
alert( "We will, we will".replace(/we/ig, "I") ); // I will, I will

对于第二个参数 replacement ,可以在其中使用特殊字符组合来对匹配项进行插入:

符号在替换字符串中的行为
$&插入整个匹配项
$`插入字符串中匹配项之前的字符串部分
$'插入字符串中匹配项之后的字符串部分
$n如果 n 是一个 1-2 位的数字,则插入第 n 个分组的内容
$<name>插入带有给定 name 的括号内的内容
$$$$插入字符 $
1
2
3
alert( "I love HTML".replace(/HTML/, "$& and JavaScript") ); // I love HTML and JavaScript
alert( "I love HTML".replace(/HTML/, "$` and JavaScript") ); // I love I love and JavaScript
alert( "I love HTML".replace(/HTML/, "$' and JavaScript") ); //I love I love and JavaScript

测试

regexp.test(str) 方法寻找至少一个匹配项,如果找到了,则返回 true,否则返回 false

1
2
3
4
let str = "I love JavaScript";
let regexp = /LOVE/i;

alert( regexp.test(str) ); // true

字符类

字符类(Character classes) 是一种特殊的符号,匹配特定集合中的任何符号。

最常用的是:

  • \d(“d” 来自 “digit”)

    数字:从 09 的字符。

  • \s(“s” 来自 “space”)

    空格符号:包括空格,制表符 \t,换行符 \n 和其他少数稀有字符,例如 \v\f\r

  • \w(“w” 来自 “word”)

“单字”字符:拉丁字母或数字或下划线 _。非拉丁字母不属于 \w

例如,\d\s\w 表示“数字”,后跟“空格字符”,后跟“单字字符”,例如 1 a

对于每个字符类,都有一个“反向类”,用相同的字母表示,但是大写的。

“反向”表示它与所有其他字符匹配,例如:

  • \D

    非数字:除 \d 以外的任何字符,例如字母。

  • \S

    非空格符号:除 \s 以外的任何字符,例如字母。

  • \W

    非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格。

+7(903)-123-45-67 这样的字符串中创建一个只包含数字的电话号码:

1
2
3
4
let str = "+7(903)-123-45-67";

alert( str.match(/\d/g).join('') ); // 79031234567
alert( str.replace(/\D/g, "") ); // 79031234567

匹配任意字符的.

. 是一种特殊字符类,它与“除换行符之外的任何字符”匹配。点不是缺少字符,我们必须有一个字符和点匹配。

1
2
3
4
5
let regexp = /CS.4/;

alert( "CSS4".match(regexp) ); // CSS4
alert( "CS-4".match(regexp) ); // CS-4
alert( "CS 4".match(regexp) ); // CS 4(空格也是一个字符)

之前提到过s的作用是启用 “dotall” 模式,允许点 . 匹配换行符 \n

1
alert( "A\nB".match(/A.B/s) ); // A\nB(匹配了!)

锚点

插入符号 ^ 和美元符号和replace()中的$作用不同。)

插入符号 ^ 匹配文本开头,而放在一起通常被用于测试一个字符串是否完全匹配一个模式。

1
2
3
4
let str1 = "Mary had a little lamb";
alert( /^Mary/.test(str1) ); // true
let str1 = "it's fleece was white as snow";
alert( /snow$/.test(str1) ); // true

多行模式

锚点的多行模式由修饰符m启用。

它只影响^$的行为。

1
2
3
4
5
6
let str = `1st place: Winnie
2nd place: Piglet
3rd place: Eeyore`;

console.log( str.match(/^\d/gm) ); // 1, 2, 3
console.log( str.match(/^\d/g) ); // 1
1
2
3
4
5
6
let str = `Winnie: 1
Piglet: 2
Eeyore: 3`;

console.log( str.match(/\d\n/g) ); // 1\n,2\n
console.log( str.match(/\d$/gm) ); // 1,2,3

词边界

词边界 \b 是一种检查,就像 ^$ 一样。

有三种不同的位置可作为词边界:

  • 在字符串开头,如果第一个字符是单词字符 \w
  • 在字符串中的两个字符之间,其中一个是单词字符 \w,另一个不是。
  • 在字符串末尾,如果最后一个字符是单词字符 \w
1
2
3
4
5
6
alert( "Hello, Java!".match(/\bHello\b/) ); // Hello
alert( "Hello, Java!".match(/\bJava\b/) ); // Java
alert( "Hello, Java!".match(/\bHell\b/) ); // null(无匹配项)
alert( "Hello, Java!".match(/\bJava!\b/) ); // null(无匹配项)
alert( "1 23 456 78".match(/\b\d\d\b/g) ); // 23,78
alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56

转义与特殊字符

  • 要在字面意义上搜索特殊字符 [ \ ^ $ . | ? * + ( ),我们需要在它们前面加上一个反斜杠 \(“转义它们”)。
  • 如果在 /.../ 内(但不在 new RegExp 内),我们还需要转义 /
  • 当将字符串传递给给 new RegExp 时,我们需要双反斜杠 \\,因为字符串引号会消耗一个反斜杠。

集合与范围

在方括号 […] 中的几个字符或者字符类表示“搜索给定字符中的任意一个”。

1
2
3
4
// 查找 [t 或 m],然后匹配 "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"
// 查找 "V",然后匹配 [o 或 i],之后匹配 "la"
alert( "Voila".match(/V[oi]la/) ); // null,无匹配项

范围

方括号也可以包含字符范围。

1
alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF

如果我们还想查找小写字母,则可以添加范围 a-f[0-9A-Fa-f]。或添加标志 i

我们也可以在 […] 中使用字符类。

例如,如果我们想查找单词字符 \w 或连字符 -,则集合可以写为 [\w-]

字符类其实是某些字符集合的缩写,例如:

  • \d —— 和 [0-9] 相同,
  • \w —— 和 [a-zA-Z0-9_] 相同,
  • \s —— 和 [\t\n\v\f\r ] 外加少量罕见的 Unicode 空格字符相同。

排除范围

除了普通的范围匹配,还有像这样 [^…] 的“排除”范围匹配。

通过在开头添加插入符号 ^ 来表示匹配所有 除了给定的字符 之外的任意字符。

例如:

  • [^aeyo] —— 匹配除了 'a''e''y''o' 之外的任何字符。
  • [^0-9] —— 匹配除了数字之外的任何字符,与 \D 作用相同。
  • [^\s] —— 匹配任何非空格字符,与 \S 作用相同。

量词

{n}

在字符、字符类或集合后附加一个{n},用来指出我们具体需要的数量。

\d{5} 表示 5 位数,与 \d\d\d\d\d 相同。

范围:{3,5},匹配 3-5 个

我们可以省略上限,那么正则表达式 \d{3,} 就会查找位数大于等于 3 的数字:

我们如果需要一个及以上的数字,就使用 \d{1,}

1
2
3
4
5
let str = "+7(903)-123-45-67";

let numbers = str.match(/\d{1,}/g);

alert(numbers); // 7,903,123,45,67

缩写

大多数常用量词都有缩写形式。(缩写都是针对前面的字符)

+

代表一个或多个,与{1,}相同。

例如,\d+ 用来查找所有数字:

1
2
3
let str = "+7(903)-123-45-67";

alert( str.match(/\d+/g) ); // 7,903,123,45,67

?

代表一个或零个,与{0,1}相同。

所以 colou?r 会找到 colorcolour

1
2
3
let str = "Should I write color or colour?";

alert( str.match(/colou?r/g) ); // color, colour

*

代表“零个及以上”,与 {0,} 相同。

例如,\d0* 查找一个数字后面跟着任意数量的零(可能有很多或没有)的数字:

1
alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1

示例

量词是构成复杂正则表达式的主要“模块”。

  • 小数:\d+\.\d+
  • HTML标签:/<[a-z]+>/i /<[a-z][a-z0-9]*>/i /<\/?[a-z][a-z0-9]\*>/i
  • “…”(任意三个字符):/\.{3,}/g
  • #开头的颜色值:/#[0-9a-f]{6}\b/gi

贪婪量词与惰性量词

量词功能十分强大,但也会产生一些让人困惑的问题。

比如说有一个文本,我们需要用书名号:«...» 来代替所有的引号 "..."

首先想到的写法是使用.+匹配"中的内容:

1
2
3
let regexp = /".+"/g;
let str = 'a "witch" and her "broom" is one';
alert( str.match(regexp) ); // "witch" and her "broom"

可以看到预期结果与想象中的不同。这是因为正则在查找时采用了贪婪算法。

  1. 该模式的第一个字符是一个引号 "

    正则表达式引擎尝试在源字符串 a "witch" and her "broom" is one 的位置 0 找到它,但那里有 a,所以匹配失败。

    然后继续前进:移至源字符串中的下一个位置,并尝试匹配模式中的第一个字符,再次失败,最终在第三个位置匹配到了引号:

    a ==”==witch” and her “broom” is one

  2. 找到引号后,引擎就尝试去匹配模式中的剩余字符。它尝试查看剩余的字符串是否符合 .+"

    在我们的用例中,模式中的下一个字符为 .(一个点)。它表示匹配除了换行符之外的任意字符,所以将会匹配下一个字符 'w'

    a ==”w==itch” and her “broom” is one

  3. 然后由于量词 .+,点会重复。正则表达式引擎一个接一个字符地进行匹配。

    ……什么时候会不匹配?点(.)能够匹配所有字符,所以只有在移至字符串末尾时才停止匹配:

    a ==”witch” and her “broom” is one==

  4. 现在引擎完成了对重复模式 .+ 的搜索,并且试图寻找模式中的下一个字符。是引号 "。但是有一个问题:对字符串的遍历已经结束,没有更多字符了!

    正则表达式引擎知道它为 .+ 匹配太多项了,所以开始 回溯

    换句话说,它去掉了量词匹配项的最后一个字符:

    a ==”witch” and her “broom” is on==e

    现在它假设 .+ 的匹配在字符串的倒数第一个字符前的位置结束,并尝试从该位置匹配模式的剩余部分。

    如果那里有引号,则搜索将结束,但最后一个字符是 'e',所以不匹配。

  5. ……所以引擎会将 .+ 的重复次数减少一个字符:

    引号 '"''n' 不匹配。

  6. 引擎不断进行回溯:它减少 '.' 的重复次数,直到模式的其余部分(在我们的用例中是 '"')匹配到结果:

​ a ==”witch” and her “broom”== is one 匹配完成。

所以,第一次匹配项是 "witch" and her "broom"。如果正则表达式具有修饰符 g,则搜索将从第一个匹配结束的地方继续。字符串 is one 的剩余部分不再有引号,因此没有更多匹配项。

惰性模式

我们可以通过在量词后面加一个?来启用惰性模式,这样搜索过程将转变为:

  1. 第一步是一样的:它在第三个字符的位置找到了模式的开头 '"': a ==”==witch” and her “broom” is one

  2. 下一步也是类似的:引擎为 '.' 找到了一个匹配项: a ==”w==itch” and her “broom” is one

  3. 接下来的搜索就有些不同了。因为我们对 +? 启用了惰性模式,引擎不会去尝试多匹配一个点的匹配字符,而会停止并立即尝试对剩余的模式 '"' 进行匹配: a ==”w==itch” and her “broom” is one

    如果这里有一个引号,搜索就会停止,但这里是一个 'i',所以没有匹配到引号。

  4. 接着,正则表达式引擎增加对点的重复搜索次数,并且再次尝试: a ==”wi==tch” and her “broom” is one

    又失败了。然后重复次数一次又一次的增加……

  5. ……直到找到了模式中的剩余部分的匹配项: a ==”witch”== and her “broom” is one

  6. 接下来的搜索从当前匹配的结尾开始,并产生了下一个匹配项: a ==”witch”== and her ==”broom”== is one

在这个例子中,我们看到了惰性模式的 +? 是怎样工作的。量词 *??? 的工作方式类似 —— 正则表达式引擎仅在模式的其余部分无法在给定位置匹配时增加重复次数。

另一个惰性模式的例子:

1
alert( "123 456".match(/\d+ \d+?/) ); // 123 4

替代方法

上面这个例子还有一个替代的正则表达式:/"[^"]+"/g

捕获组

模式的一部分可以使用()括起来,这被称为捕获组(capturing group),捕获组有两个影响:

  • 它允许将匹配的一部分作为结果数组中的单独项。
  • 如果我们将量词放在括号后,则它将括号视为一个整体。

示例

gogogo

不带括号,模式 go+ 表示 g 字符,其后 o 重复一次或多次。例如 goooogooooooooo

括号将字符组合,所以 (go)+ 匹配 gogogogogogo等。

域名

1
2
3
mail.com
users.mail.com
smith.users.mail.com

域名是由单词加点组成的,除了最后一个单词

1
let regexp = /(\w+\.)+\w+/g;

电子邮件

电子邮件的格式为name@domain。名称可以是任何单词,允许使用连字符和点。

1
let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;

这里[]中的.可以不用转义,但()中的.需要转义。

选择

方括号只允许字符或字符类。选择允许任何表达式。正则表达式 A|B|C 表示表达式 ABC 其一均可。

例如:

  • gr(a|e)y 等同于 gr[ae]y
  • gra|ey 表示 graey

要将选择应用于模式中一部分内容的选择,我们可以将其括在括号中:

  • I love HTML|CSS 匹配 I love HTMLCSS
  • I love (HTML|CSS) 匹配 I love HTMLI love CSS

示例

时间匹配的正则表达式:

1
let regexp = /([01]\d|2[0-3]):[0-5]\d/g;

前瞻断言与后瞻断言

有时我们需要为一个模式找到那些在另一个模式之后或之前的匹配项。

前瞻断言

语法为:x(?=y),它表示“仅在后面是 Y 时匹配 X”。

1
2
3
let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=€)/) ); // 30,数字 1 被忽略了,因为它后面没有 €

\d+(?=\s)(?=.*30) 查找后跟着空格 (?=\s)\d+,并且有 30 在它之后的某个地方 。

1
2
3
let str = "1 turkey costs 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

否定前瞻断言

语法是:X(?!Y),意思是“搜索 X,但前提是后面没有 Y”。

1
2
3
let str = "2 turkeys cost 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2(价格不匹配)

后瞻断言

前瞻断言允许添加一个“后面要跟着什么”的条件判断。

后瞻断言也类似,只不过它是在相反的方向上进行条件判断。也就是说,它只允许匹配前面有特定字符串的模式。

语法为如下:

  • 肯定的后瞻断言:(?<=Y)X,匹配 X,仅在前面是 Y 的情况下。
  • 否定的后瞻断言:(?<!Y)X,匹配 X,仅在前面不是 Y 的情况下。

使用 (?<=\$)\d+ —— 一个前面带 $ 的数值:

1
2
3
4
let str = "1 turkey costs $30";

// 美元符号被转义 \$
alert( str.match(/(?<=\$)\d+/) ); // 30(跳过了仅仅是数字的值)

我们可以使用否定的后瞻断言:(?<!\$)\d+

1
2
3
let str = "2 turkeys cost $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2(价格不匹配)

示例

选择非负整数

1
2
3
4
5
let regexp = /\b(?<!-)\d+\b/g;

let str = "0 12 -5 123 -18";

console.log( str.match(regexp) ); // 0, 12, 123

Java正则

对Java正则来说,主要包括以下三个类:

  • Pattern 类:

    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:

    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexMatches
{
public static void main( String[] args ){

// 按指定模式在字符串查找
String line = "This order was placed for QT3000! OK?";
// 找一个包含数字的字符串
String pattern = "(\\D*)(\\d+)(.*)";

// 创建 Pattern 对象
Pattern r = Pattern.compile(pattern);

// 现在创建 matcher 对象
Matcher m = r.matcher(line);
}
}

在Java中,\\表示我要插入一个具有特殊意义的正则表达式的反斜线,而在JS等很多其他语言中,\就能达到相同的效果。

下面是Matcher类的一些方法:

索引方法

索引方法提供了有用的索引值,精确表明输入字符串中在哪能找到匹配:

序号方法及说明
1public int start() 返回以前匹配的初始索引。
2public int start(int group) 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引
3public int end() 返回最后匹配字符之后的偏移量。
4public int end(int group) 返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。

查找方法

查找方法用来检查输入字符串并返回一个布尔值,表示是否找到该模式:

序号方法及说明
1public boolean lookingAt() 尝试将从区域开头开始的输入序列与该模式匹配。
2public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列。
3public boolean find(int start**)** 重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
4public boolean matches() 尝试将整个区域与模式匹配。

替换方法

替换方法是替换输入字符串里文本的方法:

序号方法及说明
1public Matcher appendReplacement(StringBuffer sb, String replacement) 实现非终端添加和替换步骤。
2public StringBuffer appendTail(StringBuffer sb) 实现终端添加和替换步骤。
3public String replaceAll(String replacement) 替换模式与给定替换字符串相匹配的输入序列的每个子序列。
4public String replaceFirst(String replacement) 替换模式与给定替换字符串匹配的输入序列的第一个子序列。
5public static String quoteReplacement(String s) 返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。