• 【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)

  • 相关阅读:
    [转]大型网站架构设计的体系演变
    [转]木桶理论已死,长板理论告诉你:优势才是王道!
    UHF RFID编码之TPP编码
    Git使用笔记
    使用Open Live Writer写博客
    频谱分析仪
    相位噪声
    峰值因子,峰均比,Reference Level
    SeeSharpTools.JXI.DSP.Spectrum 使用
    dyld: Library not loaded: /usr/lib/libstdc++.6.dylib
  • 原文地址:https://www.cnblogs.com/aniao/p/9351534.html
Copyright © 2020-2023  润新知