正则匹配之提取表达式

1,250 阅读3分钟

背景

某天,黄世仁和我聊起针对快编过程中 A8 检查 报错太隐晦了

Error : not found Class dalvik.annotation.Signature from void com.sobot.chat.core.http.task.PriorityObject.(int, java.lang.Object)

业务同学基本是不理解这个报错的,基本都先会找到我或者他(其实大部分是找到我),黄世仁希望是通过正则表达式提取出报错的类,再通过遍历反查出对应模块,最终输出对应模块,让业务同学能够更快定位到模块且进行修复。

我对正则的认识停留在大学社团进入要求时的基础匹配,平时写个 .* 匹配个字符串问题也不大,提取就要求太高了,这。。。能做到吗

黄世仁:有手就行,这样那样一下就好了。

🙄,听你一席话还是一席话

拆解问题

先输出一批 A8 报错信息,看看报错信息的共性

Error : not found Class dalvik.annotation.Signature from void com.hpplay.sdk.source.t.MultiMirrorControl(boolean, java.util.List)
Error : not found Class com.google.gson.Throws from void androidx.media.MediaBrowserServiceCompat$ServiceCallbacks.onConnectFailed()
Error : not found Class com.hw.annotation.View from java.lang.Object com.sobot.chat.core.http.task.PriorityObject.obj
Error : not found Class androidx.annotation.SourceDebugExtension from com.game.widget.GameIconView$IconFont
Error : not found Class androidx.annotation.Signature from java.lang.Object androidx.paging.PageFetcherSnapshot.getInitialKey$paging_common()

分析报错信息大致具有如下规则:

Error not found Class 类名 + from + 空格 + (方法返回值类型 + 空格) + 包名 + 报错类名 + (变量/方法)

写了一长串看起来挺复杂的,关键点是在于 from 后面我们需要区别去看待,可能是包含方法返回值类型再紧跟一个空格,也可能没有,后面紧跟包名,类名. 最后的变量和方法是跟随方法返回值一同出现的。

知道组成规则,那怎么从其中提取出来呢,这得提到正则表达式的捕获分组啦(Capturing Group)

提取变量-捕获分组(Capturing Group)

我是从这个链接了解的:捕获分组, 正则表达式通过 () 进行捕获变量进而进行分组,分组后我们通过 Group 的数字去取就好。文中有给出一个 Test 帮助我们理解。

要求匹配以 file 开头,.pdf 结尾的字符串,提取出其中的文件名

待匹配的字符串:
file_record_transcript.pdf    ->     file_record_transcript
file_07241999.pdf             ->     file_07241999     
testfile_fake.pdf.tmp 不匹配,因其不是以 file 开头,.pdf 结尾

对应的 Pattern 为 ^(file.+)\.pdf$, 这里和平时模式串不同的是有了一个 (),括号里的 file.+ 匹配的即是文件名,模式串最后以 .pdf 结尾。因为有 () 的原因在 Java 中我们可以在 Pattern.matcher 之后通过 group(1) 获取到文件名。伪代码类似:

val matcher = pattern.matcher(s)
if (matcher.find()) {
	val fileName = matcher.group(1)
}

在一些情况, 我们括号 () 中的东西是可选出现的,其并不需要提取出来,会使用非捕获分组, 非捕获分组是以 (?:) 开头, 就比如我要解决问题中提到的 from 后面跟的一坨返回值。

第一个版本的正则

我复习了下之前的正则知识再结合上面的分组知识,开始尝试写起了正则表达式,首先我先搜了类似的正则表达式,以 all qulified class name regex 作为 keyword, 搜到了这么一个[结果](regex101.com/r/mTWpYK/1).

(?:([a-zA-Z_$][a-zA-Z\d_$]*(?:\.[a-zA-Z_$][a-zA-Z\d_$]*)*)\.)?([a-zA-Z_$][a-zA-Z\d_$]*)

这里他以 (?:([a-zA-Z_$][a-zA-Z\d_$]*(?:\.[a-zA-Z_$][a-zA-Z\d_$]*)*)\.)? 来匹配包名,但不希望提取出来他,包名也是可选的,其并不一定存在。基于这个搜出来的例子,我写出了第一版的正则:

匹配串:Error : not found Class dalvik.annotation.Signature from void com.sobot.chat.core.http.task.PriorityObject.<init>(int, java.lang.Object)
模式串:.*from (?:(?:[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*\.)?[a-zA-Z_$][\w$]* )?(?:[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_][\w]*)*\.)?([A-Z_$][\w]*)

嗯,正则表达式很长哈。我解释下为啥这么写。对于 from 前面的字符串可以忽略跳过的,用 .*from 匹配,from 后面跟的一个超长的非捕获分组

(?:(?:[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*\.)?[a-zA-Z_$][\w$]* )?

这是借鉴了网站上匹配类来匹配我这边一个可空的返回值,最后的 ? 表示前面的返回值是可空的。最后的

(?:[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_][\w]*)*\.)?([A-Z_$][\w]*)

通过(?:)? 来匹配包含错误类的包名,因为项目中的类名是大写或者 _ 开头, 通过这样使得贪心匹配到了最后一个包名的分隔符 . 就停止了,剩余的被捕获分组 ([A-Z_$][\w]*) 所捕获。获取时通过 group(1) 就能获取到对应类名。

image-20220911202731959.png

第二版的正则

正则写的很长,也不是很优雅。两周后大概只有上帝能知道他的含义了,自己去请教了组里大哥,大哥不一会就给出了解决方案,我看到时不由感叹,大哥就是大哥。

(?:.*from \S* ([^( ]*)\.)|(?:.*from ([^( ]*)\Z)

真滴优雅,用了 1/3 的表达式实现了一样的效果。这个模式串将问题拆分成两个部分,将包含返回值和没有返回值的情况分开对待。最终通过 | 的方式得到完整的解。

先看前半部分:

模式串:(?:.*from \S* ([^( ]*)\.)
匹配串:
Error : not found Class dalvik.annotation.Signature from void com.hpplay.sdk.source.t.MultiMirrorControl(boolean, java.util.List)
Error : not found Class com.hw.annotation.View from java.lang.Object com.sobot.chat.core.http.task.PriorityObject.obj

开头一个 ?: 表示 () 内的不进行捕获, .*from 匹配到 from 关键字及空格,后面的 \S 匹配非空格字符,通过限定后面必须接一个空格 限制了 \S 匹配的必然是返回值。又因为有返回值的情况下必然有变量/方法,他们的共性是类名后面必然有 \. , 通过在表达式 ([^( ]*)\.) 最后加上 \. 约束了贪婪匹配的范围,但光这样还不够,因为方法参数内部也可能包含 . , 所以需要通过剔除 ( 及空格排除方法参数的影响。

而后半部分:

模式串:(?:.*from ([^( ]*)\Z)
匹配串:Error : not found Class android.annotation.SourceDebugExtension from com.game.widget.GameIconView$IconFont

针对没有返回值,即只匹配类的情况,以 \Z 结尾约束模式串能够匹配字符串的结尾。

思考

这个问题解决几周后,自己在写文章的过程中,开始重新回顾这个问题始末,发现自己在解决这个问题时拆解的并不够清楚,当时的我并没理解有无返回值,其实是和变量,方法的存在相绑定的。以及对正则一些特殊的变量 \S \Z 并不熟悉,才耗费了许多时间解决这个问题。不过吧,自己独立解决再尝试对比大佬的方案,相比直接要方案会促使自己更多的思考方案之间的差异。

最后的最后,我如愿的将方案糊在了黄世仁脸上,🐶, 不是很简单嘛,有手就行,**