一文带你读懂:Google 和 JDK 的正则表达式引擎有何不同

daa672d1ce430b5c6c3457e29365bf9a.png

Together for a Shared future

开发经验

5374d16225aa084589de4f5a67f8a0e6.png

413dcb0912e00f32a8883a45ee0920ee.png

7df13aef08a11cc017104d61dec3d432.png

最近我在实际工作中,接手了兄弟部门开发的一个模块,然后有部分用户提了一个问题到我这里。

经过一顿排查,原因竟然是:开发人员选择了不同的正则表达式引擎,导致了用户使用上的体验差异。

正则表达式的基础,大家可以通过菜鸟教程(https://www.runoob.com/regexp/regexp-intro.html)复习一下概念和正则语法~~

c34f3b9f013544a684739935e040c001.png

问题凸显

b6b772e46eed83fcb4807f4fe9eea7a9.png

最近同事反馈某个正则表达式在相关网站上面,能够正常去匹配字符串,但是在我们的系统中却抛出异常信息,如下:

09fcf05d303559cc2ffdcc7166ac1e5c.png

42d903e14ca922f4ef7ad2f9b0269a46.png

不同引擎的使用差异

dc51ba639fdfca9aa492d6c64149d750.png

于是我这边进行问题定位,发现是底层使用了 Google 的 Re2j 的正则表达式引擎,代码段如下:

public class TestGoogleCompile {
  public static void main(String[] args) {
    isPathValidateOfGoogleRe2j("^(?!.*aaa).*(bbb)+(?!.*aaa.*)");
  }


  private static boolean isPathValidateOfGoogleRe2j(String config) {
    try {
      com.google.re2j.Pattern.compile(config);
      return true;
    } catch (Exception ex) {
      System.out.println(MessageFormat.format("isPathValidate error, config={0}, exception={1}",
          config, ex.getMessage()));
      return false;
    }
  }
}
isPathValidate error, config=^(?!.*aaa).*(bbb)+(?!.*aaa.*), 
exception=error parsing regexp: 
invalid or unsupported Perl syntax: `(?!`

然后使用 JDK 原生的 Regex 正则表达式引擎,代码段如下:

public class TestJdkRegex {
  public static void main(String[] args) {
    isPathValidateOfJdkRegex();
  }


  private static void isPathValidateOfJdkRegex(){
    String text = "aa.gradle";
    String pattern = "^(?!.*lib_tavcam).*(gradle)+(?!.*lib_tavcam.*)";
    Pattern p = Pattern.compile(pattern);
    Matcher m = p.matcher(text);
    // 调用匹配器对象的功能
    if (m.find()) {
      System.out.println(m.group());
    }
  }
}
aa.gradle

结论:

        相同的正则表达式,不同的表达式引擎,会出现不同的表现结果。两相对比,TestJdkRegex 的运行结果一切正常,而 TestGoogleCompile 复现了 bug。

a8bd3fe4e6164a3e991ca8d8e7acec65.png

Google 的 Re2j 正则表达式引擎

8f8c902c015d655ad44f9607232ec1cf.png

RE2/J 是 RE2 到纯 Java 的一个端口。

465cecc9fef2c80adf95fe73447a2475.png

maven 依赖

<!-- https://mvnrepository.com/artifact/com.google.re2j/re2j -->
<dependency>
    <groupId>com.google.re2j</groupId>
    <artifactId>re2j</artifactId>
    <version>1.0</version>
</dependency>

非确定性有限自动机

RE2 是一个正则表达式引擎,在输入的大小上以时间线性方式运行。

RE2 算法使用非确定性有限自动机在一次传递输入数据时同时探索所有匹配。所谓非确定性有限自动机(NFA)即:

  • 对于某一个状态,读入某一个输入的时候,可能会有多种转移规则;

  • 对于某一个状态,它可能会缺少对应某种输入的转移规则;

  • 下面就是一个 NFA:

5d2524763120efde0cbaf536cb35aeb4.png

通过观察上图可以发现,在状态 1 输入 b 的时候,可能跳转到状态 1,也可能跳转到状态 2;而状态 4 则对任何输入不会有转移。这样的机器就是 NFA(Nondeterministic finite automata)。

fc2a90e32d961c2120a31a218074b909.png

JDK 的 Regex 正则表达式引擎

a25ec6dfaf0cecf94d7bd18805628824.png

Java 的标准正则表达式包java.util.regex,以及许多其他广泛使用的正则表达式包,如 PCRE、Perl 和 Python,都使用回溯实现策略:当一个模式呈现两个备选方案(如a|b)时,引擎将首先尝试匹配子模式a,如果结果不匹配,它将重置输入流并尝试匹配b

应用层

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

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

  • Matcher 类:

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

  • PatternSyntaxException:

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

回溯实现策略

回溯法,又称试探法,是常用的,基本的优选搜索方法。常用于解决这一类问题:给定一定约束条件 F(该约束条件常用于后面的剪枝)下求问题的一个解或者所有解。

回溯法其实是暴力枚举的一种改进,因为其会聪明的 filter 掉不合适的分支,大大减少了无谓的枚举。若某问题的枚举都是可行解得话,也就是没有剪枝发生,那么回溯法和暴力枚举并无二异。

不足之处

如果这样的选择是深层嵌套的,则此策略需要对输入数据进行指数级的传递,然后才能检测输入是否匹配。如果输入量很大,就很容易构造出运行时间超过宇宙生命周期的模式。当接受来自不受信任的源(如 web 应用程序的用户)的正则表达式模式时,这会产生安全风险。

在最坏的情况下,java.util.regex匹配器可能永远运行,或者超过可用堆栈空间而失败;这在 RE2/J 中永远不会发生。

afefd90aaebc0926738ccb7a007351c3.png

Go 对正则表达式引擎的选择

5e6b56ef34d3e0ca0901309287f91075.png

768dd1dc29c7324b49aef8e0055b18bb.png

显然, Go 正则表达式引擎,本质也 NFA 的应用,遵守效率优先的原则。

9bb2501723913ce845814d268b31a63c.png

其他语言对正则表达式引擎的选择

a7fa4aed37bb4dfff77fc437e00d480e.png

fb933cd84e303ba63711aa68265b3443.png

2a3773137abdbb2309de1ea118ff88f5.png

问题原因:Lookaround

8de3b2d40909b859bfed1c08ab9141da.png

回到用户提到的问题,为什么google的表达式引擎,在解析执行时会抛异常呢?

1)Lookaround包括Lookahead和Lookbehind两种匹配模式

(Lookahead检测的是后缀,而Lookbehind检测的是前缀,它们有 Positive、Negative 两种匹配方式),而 google/re2 是不支持 lookaround 的。

2)部分功能使用了 google/re2 的实现,所以我们要将 Lookaround 的语法转换为非 Lookaround 使用;

而上面的案例,用户使用的 path = ^(?!.*lib_tavcam).*(gradle)+(?!.*lib_tavcam.*),是既有前瞻(lookahead),也有后视(lookbehind),所以判断为不合法。

f4795ba8ce598deb3dff017531e704ed.png

如何选择正则表达式引擎呢?

36ac1647507e27f996fb667e7920dc95.png

那么在我们日常开发过程中,在 JDK 与 Google 的引擎应该进行什么选择呢?下面给出一些建议:

在这个问题上,JDK 是能够正常识别 lookaround 的表达式,但是 google 选择效率优先,不支持 lookaround 的正则。

  • 如果说你的系统是内部系统,确认不会出现 SQL 注入类似的安全问题,使用 JDK 原生的正则表达式引擎无疑让你的正则表达式支持范围更强大;

  • 如果说你的系统是商业化系统,对安全问题是否看重,那么使用 Google 的 Re2j 引擎是不二选择。

  • 务必确保所有的模块都使用同一个技术栈,避免因为引擎选择不同,而导致的功能性兼容问题。

后端技术&架构精华

f94bce68e8a6d63c0657df3eaa5ecbff.png

《源码系列》

JDK之Object 类

JDK之BigDecimal 类

JDK之String 类

JDK之Lambda表达式

Spring源码:Event事件发布与监听

《经典书籍》

Java并发编程实战:第1章 多线程安全性与风险

Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制

Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭

《服务端技术栈》

《Docker 核心设计理念

《Kafka史上最强原理总结》

《HTTP的前世今生》

《算法系列》

读懂排序算法(一):冒泡&直接插入&选择比较

《读懂排序算法(二):希尔排序算法》

《读懂排序算法(三):堆排序算法》

《读懂排序算法(四):归并算法》

《读懂排序算法(五):快速排序算法》

《读懂排序算法(六):二分查找算法》

《项目管理》

《学点项目管理,对咱程序员很重要》

《项目管理实践篇(一):技术人如何做好风险把控》

《项目管理实践篇(二):总结项目经历》

《如何写好一篇汇报材料》

《在鹅厂工作一周年的经验分享》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值