简介

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

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

样例1

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

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

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

Pattern.compile(String regex)

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

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

构造方法

     private Pattern(String p, int f) {
pattern = p;
flags = f; if ((flags & UNICODE_CHARACTER_CLASS) != 0)
flags |= UNICODE_CASE; capturingGroupCount = 1;
localCount = 0; if (pattern.length() > 0) {
compile();
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}

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

compile()

     private void compile() {
if (has(CANON_EQ) && !has(LITERAL)) {
normalize(); // 标准化
} else {
normalizedPattern = pattern;
}
patternLength = normalizedPattern.length(); temp = new int[patternLength + 2]; // 将pattern字符的代码点(codePoint)存在int数组中,多出2个槽,标识结束 hasSupplementary = false;
int c, count = 0;
for (int x = 0; x < patternLength; x += Character.charCount(c)) {
c = normalizedPattern.codePointAt(x);
if (isSupplementary(c)) { // 确定指定的代码点是否为辅助字符或未配对的代理
hasSupplementary = true;
}
temp[count++] = c; // 存到数组中
} patternLength = count; // 现在是代码点的个数 if (!has(LITERAL))
RemoveQEQuoting(); // 处理\Q...\E的情况 buffer = new int[32]; // 分配临时对象
groupNodes = new GroupHead[10]; // 组
namedGroups = null; if (has(LITERAL)) { // 纯文本,示例会走这个分支
matchRoot = newSlice(temp, patternLength, hasSupplementary); // Slice结点
matchRoot.next = lastAccept;
} else {
matchRoot = expr(lastAccept); // 递归解析表达式
if (patternLength != cursor) { // 处理异常情况
if (peek() == ')') {
throw error("Unmatched closing ')'");
} else {
throw error("Unexpected internal error");
}
}
} if (matchRoot instanceof Slice) { // 如果是文本模式,则返回BnM结点(Boyer Moore算法,处理子字符串的高效算法)
root = BnM.optimize(matchRoot);
if (root == matchRoot) {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot); // Start和LastNode(lastAccept)是首尾两个结点,通用处理
}
} else if (matchRoot instanceof Begin || matchRoot instanceof First) { // Begin和End也是结点类型,大概是处理多行模式,不展开讨论
root = matchRoot;
} else {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
// 清理工作
temp = null;
buffer = null;
groupNodes = null;
patternLength = 0;
compiled = true;
}
  1. 首先标准化表达式
  2. 将字符代码点暂存int数组中,所谓代码点指的是字符集里每个字符的编号,从0开始,常见的字符集ASCII和Unicode
  3. 返回相应类型的结点
  4. root和matchRoot的关系,root表示可以从给定文本的任意位置开始查找,matchRoot表示全字符匹配(从头到尾)

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

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

     private Node newSlice(int[] buf, int count, boolean hasSupplementary) {
int[] tmp = new int[count];
if (has(CASE_INSENSITIVE)) {
if (has(UNICODE_CASE)) {
for (int i = 0; i < count; i++) {
tmp[i] = Character.toLowerCase(Character.toUpperCase(buf[i]));
}
return hasSupplementary ? new SliceUS(tmp) : new SliceU(tmp);
}
for (int i = 0; i < count; i++) {
tmp[i] = ASCII.toLower(buf[i]);
}
return hasSupplementary ? new SliceIS(tmp) : new SliceI(tmp);
}
for (int i = 0; i < count; i++) {
tmp[i] = buf[i];
}
return hasSupplementary ? new SliceS(tmp) : new Slice(tmp);
}

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

数据结构Slice

     static final class Slice extends SliceNode {
Slice(int[] buf) {
super(buf);
} boolean match(Matcher matcher, int i, CharSequence seq) {
int[] buf = buffer;
int len = buf.length;
for (int j = 0; j < len; j++) { // 从第一个字符开始比较,如果长度不等,或遇到不等的字符,返回false,否则调用next结点的match方法
if ((i + j) >= matcher.to) {
matcher.hitEnd = true;
return false;
}
if (buf[j] != seq.charAt(i + j))
return false;
}
return next.match(matcher, i + len, seq);
}
}

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

SliceNode

     static class SliceNode extends Node {
int[] buffer;
SliceNode(int[] buf) {
buffer = buf;
}
boolean study(TreeInfo info) {
info.minLength += buffer.length;
info.maxLength += buffer.length;
return next.study(info);
}
}

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

Node

     static class Node extends Object {
Node next; Node() {
next = Pattern.accept;
} boolean match(Matcher matcher, int i, CharSequence seq) {
matcher.last = i;
matcher.groups[0] = matcher.first; // 默认是一组(组[0-1])
matcher.groups[1] = matcher.last;
return true;
} boolean study(TreeInfo info) { // 零长度断言
if (next != null) {
return next.study(info);
} else {
return info.deterministic;
}
}
}

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

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

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

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

BnM

继承Node结点

     static class BnM extends Node {}

属性

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

构造方法

         BnM(int[] src, int[] lastOcc, int[] optoSft, Node next) {
this.buffer = src;
this.lastOcc = lastOcc;
this.optoSft = optoSft;
this.next = next;
}

optimize(Node node)

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

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

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

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

study

         boolean study(TreeInfo info) {
info.minLength += buffer.length;
info.maxValid = false;
return next.study(info);
}

Boyer Moore算法

可参考这个

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

坏字符

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

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

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

好后缀

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

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

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

分析

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

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

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

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

Node链

matches()

matchRoot -> Slice -> LastNode -> Node

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

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

LastNode

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

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

find()

root -> BnM -> LastNode -> Node

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

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

样例2

     public static void example() {
String regex = "\\d+";
String text = "0123456789";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
matcher.find();
}

这个样例是匹配数字。

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

expr(Node end)

【Java字符序列】Pattern的更多相关文章

  1. Java 之 可变字符序列:字符串缓冲区(StringBuilder 与 StringBuffer)

    一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: public class StringDemo { public ...

  2. Java实现 蓝桥杯VIP 算法提高 最长字符序列

    算法提高 最长字符序列 时间限制:1.0s 内存限制:256.0MB 最长字符序列 问题描述 设x(i), y(i), z(i)表示单个字符,则X={x(1)x(2)--x(m)},Y={y(1)y( ...

  3. Java 常用类——StringBuffer&StringBuilder【可变字符序列】

    一.字符串拼接问题 由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象. Demo: 1 public class StringDemo { 2 pub ...

  4. JAVA基础 XML生成与解析和String包装类下 .replace方法的使用以及char和字符序列的使用场景

    ptLink0.setText(arbu.getPtLink().replace("&","&")); // 如果像 '&','& ...

  5. java.util.regex.Pattern的应用

    java.util.regex.Pattern 正则表达式的一种已编译的实现. 正则表达式通常以字符串的形式出现,它首先必须被编译为Pattern类的一个实例.结果模型可以用来生成一个Matcher, ...

  6. JAVA正则表达式:Pattern类与Matcher类详解(转)

    java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...

  7. JAVA正则表达式:Pattern类与Matcher类详解

    java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包.它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表 ...

  8. Java 字符的验证

    package net.hlj.common.util; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @ ...

  9. Java 字符编码归纳总结

    String newStr = new String(oldStr.getBytes(), "UTF-8");       java中的String类是按照unicode进行编码的 ...

随机推荐

  1. wxpython 窗口排版- proportion/flag/border参数说明

    新学习wxpython,一直纠结于窗口控件的排版,经过几天的查资料.试验,总结如下. 1.需求实例 来个实例,窗口有3行控件 第一行是文本提示(大小不变,文字左对齐,控件居左). 第二行依次为文本提示 ...

  2. python---九九乘法表代码

    #_*_ coding:utf-8 _*_# author choco ''' #while循环num1=0while num1<9: num1+=1 num2=1 while num2< ...

  3. C++用法总结

    1.C++的绝对值符号 如果是整形的,就是abs() 如果是浮点型的,是fabs() 这两个函数都从属于库函数math.h #include <cmath> or #include< ...

  4. December 02nd 2016 Week 49th Friday

    People will fall for its appearance while driving passionately. 观者倾心,驭者动魄. An advertisement of Merce ...

  5. Windows10自动更新之后,无线有线都连不上网

    大概浪费了我至少6个小时. 一个是无线网卡,这个后来可以修复,其实也不是网卡的原因.最主要的原因是 Realtek PCIe GBE Family Controller  这个驱动.只找到一个win1 ...

  6. 第一篇,编译生成libcef_dll_wrapper

    因为工作原因需要在程序里面嵌入地图,在网上看了百度地图和高德地图都没有提供c++的接口,提供有web接口,那只好在程序里面嵌入web控件了,第一想到的是web browser控件,接着脑海里又想到IE ...

  7. 【实战项目】【FLEX】#900 实现拖控件功能

    一.功能说明:拖控件的功能(类似FLEX,VS 里面的拖控件). 提示:大家对事件的注册和派发的说法可能不一样.因为在FLEX中和在Java中,叫法有的区别.但是本质是一样的. 注册事件  == 设置 ...

  8. LA5713 秦始皇修路 (mst)

    题意: 秦朝有n个城市,需要修路让每个城市都互相连通,现在可以免费修一条路,秦始皇希望他除了这条免费修的路外所需修的路的总和B最短,同时这条免费的路连接的人口之和A尽可能大,求最大的A/B是多少,城市 ...

  9. 1562. [NOI2009]变换序列【二分图】

    Description Input Output Sample Input 5 1 1 2 2 1 Sample Output 1 2 4 0 3 HINT 30%的数据中N≤50: 60%的数据中N ...

  10. Python pip常用指令

    pip listpip list --outdatedpip list --outdated | grep Jinja2pip uninstall pycurlpip show pycurlpip i ...