【Java字符序列】Pattern

简介

Pattern,正则表达式的编译表示,操作字符序列的利器。

整个Pattern是一个树形结构(对应于表达式中的‘|’),一般为链表结构,树(链表)的基本元素是Node结点,Node有各种各样的子结点,以满足不同的匹配模式。

样例1

以一个最简单的样例,走进源码。

1     public static void example() {
2         String regex = "EXAMPLE";
3         String text = "HERE IS A SIMPLE EXAMPLE";
4         Pattern pattern = Pattern.compile(regex, Pattern.LITERAL);
5         Matcher matcher = pattern.matcher(text);
6         matcher.find();
7     }

这个样例实现了查找字串的功能。

 

Pattern.compile(String regex)

1     public static Pattern compile(String regex) {
2         return new Pattern(regex, 0);
3     }

这个方法通过调用构造方法返回一个Pattern对象。

 

构造方法

 1     private Pattern(String p, int f) {
 2         pattern = p;
 3         flags = f;
 4 
 5         if ((flags & UNICODE_CHARACTER_CLASS) != 0)
 6             flags |= UNICODE_CASE;
 7 
 8         capturingGroupCount = 1;
 9         localCount = 0;
10 
11         if (pattern.length() > 0) {
12             compile();
13         } else {
14             root = new Start(lastAccept);
15             matchRoot = lastAccept;
16         }
17     }

构造方法又调用compile()方法。

 

compile()

 1     private void compile() {
 2         if (has(CANON_EQ) && !has(LITERAL)) {
 3             normalize(); // 标准化
 4         } else {
 5             normalizedPattern = pattern;
 6         }
 7         patternLength = normalizedPattern.length();
 8 
 9         temp = new int[patternLength + 2]; // 将pattern字符的代码点(codePoint)存在int数组中,多出2个槽,标识结束
10 
11         hasSupplementary = false;
12         int c, count = 0;
13         for (int x = 0; x < patternLength; x += Character.charCount(c)) {
14             c = normalizedPattern.codePointAt(x);
15             if (isSupplementary(c)) { // 确定指定的代码点是否为辅助字符或未配对的代理
16                 hasSupplementary = true;
17             }
18             temp[count++] = c; // 存到数组中
19         }
20 
21         patternLength = count; // 现在是代码点的个数
22 
23         if (!has(LITERAL))
24             RemoveQEQuoting(); // 处理\Q...\E的情况
25 
26         buffer = new int[32]; // 分配临时对象
27         groupNodes = new GroupHead[10]; //
28         namedGroups = null;
29 
30         if (has(LITERAL)) { // 纯文本,示例会走这个分支
31             matchRoot = newSlice(temp, patternLength, hasSupplementary); // Slice结点
32             matchRoot.next = lastAccept;
33         } else {
34             matchRoot = expr(lastAccept); // 递归解析表达式
35             if (patternLength != cursor) { // 处理异常情况
36                 if (peek() == ')') {
37                     throw error("Unmatched closing ')'");
38                 } else {
39                     throw error("Unexpected internal error");
40                 }
41             }
42         }
43 
44         if (matchRoot instanceof Slice) { // 如果是文本模式,则返回BnM结点(Boyer Moore算法,处理子字符串的高效算法)
45             root = BnM.optimize(matchRoot);
46             if (root == matchRoot) {
47                 root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot); // Start和LastNode(lastAccept)是首尾两个结点,通用处理
48             }
49         } else if (matchRoot instanceof Begin || matchRoot instanceof First) { // Begin和End也是结点类型,大概是处理多行模式,不展开讨论
50             root = matchRoot;
51         } else {
52             root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
53         }
54         // 清理工作
55         temp = null;
56         buffer = null;
57         groupNodes = null;
58         patternLength = 0;
59         compiled = true;
60     }
  1. 首先标准化表达式
  2. 将字符代码点暂存int数组中,所谓代码点指的是字符集里每个字符的编号,从0开始,常见的字符集ASCII和Unicode
  3. 返回相应类型的结点
  4. root和matchRoot的关系,root表示可以从给定文本的任意位置开始查找,matchRoot表示全字符匹配(从头到尾)

先看正则表达式是文本的分支,即样例中所示。

 

newSlice(int[] buf, int count, boolean hasSupplementary)

 1     private Node newSlice(int[] buf, int count, boolean hasSupplementary) {
 2         int[] tmp = new int[count];
 3         if (has(CASE_INSENSITIVE)) {
 4             if (has(UNICODE_CASE)) {
 5                 for (int i = 0; i < count; i++) {
 6                     tmp[i] = Character.toLowerCase(Character.toUpperCase(buf[i]));
 7                 }
 8                 return hasSupplementary ? new SliceUS(tmp) : new SliceU(tmp);
 9             }
10             for (int i = 0; i < count; i++) {
11                 tmp[i] = ASCII.toLower(buf[i]);
12             }
13             return hasSupplementary ? new SliceIS(tmp) : new SliceI(tmp);
14         }
15         for (int i = 0; i < count; i++) {
16             tmp[i] = buf[i];
17         }
18         return hasSupplementary ? new SliceS(tmp) : new Slice(tmp);
19     }

该方法主要处理了一些情况,比如是否关心大小写等,直接看最后一句,根据hasSupplementary的值决定初始化SliceS还是Slice,在此只关心Slice的情况。

 

数据结构Slice

 1     static final class Slice extends SliceNode {
 2         Slice(int[] buf) {
 3             super(buf);
 4         }
 5 
 6         boolean match(Matcher matcher, int i, CharSequence seq) {
 7             int[] buf = buffer;
 8             int len = buf.length;
 9             for (int j = 0; j < len; j++) { // 从第一个字符开始比较,如果长度不等,或遇到不等的字符,返回false,否则调用next结点的match方法
10                 if ((i + j) >= matcher.to) {
11                     matcher.hitEnd = true;
12                     return false;
13                 }
14                 if (buf[j] != seq.charAt(i + j))
15                     return false;
16             }
17             return next.match(matcher, i + len, seq);
18         }
19     }

该类继承了SliceNode,主要实现了match方法,该方法查看给定文本是否与给定表达式相等,从头开始一个字符一个字符地比较。

 

SliceNode

 1     static class SliceNode extends Node {
 2         int[] buffer;
 3         SliceNode(int[] buf) {
 4             buffer = buf;
 5         }
 6         boolean study(TreeInfo info) {
 7             info.minLength += buffer.length;
 8             info.maxLength += buffer.length;
 9             return next.study(info);
10         }
11     }

所有Slice结点的基类,实现了Node结点,主要的study方法,累加TreeInfo的最小长度和最大长度。

 

Node

 1     static class Node extends Object {
 2         Node next;
 3 
 4         Node() {
 5             next = Pattern.accept;
 6         }
 7 
 8         boolean match(Matcher matcher, int i, CharSequence seq) {
 9             matcher.last = i;
10             matcher.groups[0] = matcher.first; // 默认是一组(组[0-1])
11             matcher.groups[1] = matcher.last;
12             return true;
13         }
14 
15         boolean study(TreeInfo info) { // 零长度断言
16             if (next != null) {
17                 return next.study(info);
18             } else {
19                 return info.deterministic;
20             }
21         }
22     }

顶级结点,match方法总是返回true,子类应重写此方法,

group, 调用链如下:getSubSequence(groups[group * 2], groups[group * 2 + 1]) ---> CharSequence#subSequence(int start, int end).

每2个相邻的元素表示一个组的首尾索引。

 

再回到compile方法,下一步调用BnM.optimize(matchRoot).

 

BnM

继承Node结点

1     static class BnM extends Node {}

 

属性

1         int[] buffer; // 表达式数组(里面元素是代码点)
2         int[] lastOcc; // 坏字符,表达式里的每个字符按顺序(从表达式数组索引0开始)存到lastOcc数组中,存的位置是表达式元素的值对128取模,因为它的长度是128,存的值是patternLength - 移动步长
3         int[] optoSft; // 好后缀,长度等于表达式数组的长度,里面的元素也表示patternLength - 移动步长

 

构造方法

1         BnM(int[] src, int[] lastOcc, int[] optoSft, Node next) {
2             this.buffer = src;
3             this.lastOcc = lastOcc;
4             this.optoSft = optoSft;
5             this.next = next;
6         }

 

optimize(Node node)

 1         static Node optimize(Node node) {
 2             if (!(node instanceof Slice)) {
 3                 return node;
 4             }
 5 
 6             int[] src = ((Slice) node).buffer;
 7             int patternLength = src.length;
 8             if (patternLength < 4) {
 9                 return node;
10             }
11             int i, j, k; // k无用
12             int[] lastOcc = new int[128];
13             int[] optoSft = new int[patternLength];
14             for (i = 0; i < patternLength; i++) { // 构造坏字符数组
15                 lastOcc[src[i] & 0x7F] = i + 1; // 如果不同的字符存在了同一个索引上,则上一个字符沿用后一个字符的【被减步数】,比原来的大了,所以总的步长小了,便不会错过,而坏字符数组的规模则控制在了前128位,拿时间换空间是值得的,毕竟涵盖了整个ASCII字符集
16             }
17             NEXT: for (i = patternLength; i > 0; i--) { // 构造好后缀数组
18                 for (j = patternLength - 1; j >= i; j--) { // 从后往前,处理所有子字符串的情况,出现的子字符串同时也在头部出现才算有效
19                     if (src[j] == src[j - i]) {
20                         optoSft[j - 1] = i;
21                     } else {
22                         continue NEXT;
23                     }
24                 }
25                 while (j > 0) { // 填充剩余的槽位
26                     optoSft[--j] = i;
27                 }
28             }
29             optoSft[patternLength - 1] = 1;
30             if (node instanceof SliceS)
31                 return new BnMS(src, lastOcc, optoSft, node.next);
32             return new BnM(src, lastOcc, optoSft, node.next);
33         }

预处理,构造出坏字符数组和好后缀数组。

 

 1         boolean match(Matcher matcher, int i, CharSequence seq) {
 2             int[] src = buffer;
 3             int patternLength = src.length;
 4             int last = matcher.to - patternLength;
 5 
 6             NEXT: while (i <= last) {
 7                 for (int j = patternLength - 1; j >= 0; j--) { // 从后往前比较字符
 8                     int ch = seq.charAt(i + j);
 9                     if (ch != src[j]) {
10                         i += Math.max(j + 1 - lastOcc[ch & 0x7F], optoSft[j]); // 每次移动步长,取坏字符和好后缀中较大者
11                         continue NEXT;
12                     }
13                 }
14                 matcher.first = i;
15                 boolean ret = next.match(matcher, i + patternLength, seq);
16                 if (ret) {
17                     matcher.first = i;
18                     matcher.groups[0] = matcher.first; // 默认一组(两个索引确定一个片段,所以只需2个元素)
19                     matcher.groups[1] = matcher.last;
20                     return true;
21                 }
22                 i++;
23             }
24             matcher.hitEnd = true;
25             return false;
26         }

根据Boyer Moore算法比较子字符串。

study

1         boolean study(TreeInfo info) {
2             info.minLength += buffer.length;
3             info.maxValid = false;
4             return next.study(info);
5         }

 

Boyer Moore算法

可参考这个

该算法最主要的特征是,从右往左匹配,这样每次可以移动不止一个字符,有两个依据,坏字符和好后缀,取较大值。

 

坏字符

从表达式最右边的字符开始与文本中同索引字符比较,若相同则继续往左,直至比较结束,即匹配;或遇到不等的字符,即称该不等字符(文本中的字符)为坏字符,根据表达式中是否包含坏字符和坏字符的位置来确定移动步长,公式如下:

后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置

如果"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。

 

好后缀

从右往左比较过程中,相等的部分字符序列称为好后缀,最长好后缀的子序列也是好后缀,同时在表达式头部出现的好后缀才有效。公式如下:

后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置

"好后缀"的位置以最后一个字符为准。

 

分析

其实,不管是坏字符还是好后缀,它的目的是移动最大步长,以实现快速匹配字符串的,还得不影响正确性。

 

坏字符很好理解,如果表达式中不包含坏字符,这个时候移动的步长是表达式的长度,也是能移动的最大长度;假如这种情况下,移动的长度小于表达式的长度,那么上次的坏字符总能再次出现,结果还是不匹配,所以直接移动到坏字符的后面,即表达式长度。

若是表达式中包含坏字符呢,肯定是的表达式中的那个字符和坏字符对齐才行,若是不对齐,与别的字符比较,还是不等,那如果表达式中包含不只一个呢,为了不往回(左)移动,应该使得表达式中靠后的字符与坏字符对齐,这样如果不匹配的话,可以接着右移,避免回溯。

 

好后缀也好理解,如果头部不包含好后缀,那么完全可以移动表达式的长度,若是包含,只需将好后缀部分对齐即可。

 

Node链

matches()

matchRoot -> Slice -> LastNode -> Node

Slice和Node结点,前面已经介绍过了。Slice结点,从第一个字符开始比较,如果长度不等,或遇到不等的字符,返回false,否则调用next结点的match方法,这里的next结点是LastNode.

Node结点的match方法总会返回true.

LastNode

 1     static class LastNode extends Node {
 2         boolean match(Matcher matcher, int i, CharSequence seq) {
 3             if (matcher.acceptMode == Matcher.ENDANCHOR && i != matcher.to) // 当acceptMode是ENDANCHOR时,此时是全匹配,所以需要检查i是否是最后一个字符的下标
 4                 return false;
 5             matcher.last = i;
 6             matcher.groups[0] = matcher.first;
 7             matcher.groups[1] = matcher.last;
 8             return true;
 9         }
10     }

 

此结点是通用结点,用来最后检测结果的,注意accetMode参数,用以区分是全匹配还是部分匹配。

 

find()

root -> BnM -> LastNode -> Node

由BnM结点可知,匹配可从任意有效位置开始,其实就是查找子字符串,且acceptMode不是ENDANCHOR,所以在LastNode中,无需检查i是否指向最后一个字符。

以上结点均已在上文中给出。

 

样例2

1     public static void example() {
2         String regex = "\\d+";
3         String text = "0123456789";
4         Pattern pattern = Pattern.compile(regex);
5         Matcher matcher = pattern.matcher(text);
6         matcher.find();
7     }

 

这个样例是匹配数字。

跟踪其调用过程,跟样例1差不多,最后是到compile方法里面,调用expr(Node end) 方法。

 

expr(Node end)

 

posted @ 2018-07-23 23:58  林城画序  阅读(16363)  评论(0编辑  收藏  举报