【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 }
- 首先标准化表达式
- 将字符代码点暂存int数组中,所谓代码点指的是字符集里每个字符的编号,从0开始,常见的字符集ASCII和Unicode
- 返回相应类型的结点
- 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)