1. 前言
在研究正则表达式中,遇到了一个需求。通过本文来梳理
和记录
一下解决方案,并 分享
给大家。对于正则表达式而言,一个括号就对应一个分组。现在期望解析正则表达式,获取分组情况:
(((\d{1,4})年)(\d{1,2}))月(\d{1,2})日
比如,上面的正则分组情况如下:简单来说,就是提取所有的匹配括号
中内容。
第一组: ((\d{1,4})年)(\d{1,2})
第二组: (\d{1,4})年
第三组: \d{1,4}
第四组: \d{1,2}
第五组: \d{1,2}
2.基于符号分割字符串
首先,通过正则表达式匹配到所有的 (
和 )
的左右边界,如下图所示:
通过 split
方法根据上面的正则表达式进行分割,就可以得到如下的列表:
main() {
String src = r'(((\d{1,4})年)(\d{1,2}))月(\d{1,2})日';
RegExp exp = RegExp(r'(?<=\()|(?<=\))|(?=\()|(?=\))');
List<String> parts = src.split(exp);
}
所以现在我们需要做的是如何匹配括号的闭合,并提取出闭合括号中的内容。
3. 括号闭合匹配思路
对闭合性的校验,最常用的当属栈结构
。可能很多朋友只是听闻,并不知道具体的处理逻辑,这里通过图解示意一下:首先场景1为空栈,此时索引为 0 的 (
准备入栈;然后索引为 1 的 (
准备入栈,此时栈中有一个元素,而 (
未能与栈顶匹配,所以入栈,如下场景 3
。
索引为 2
的字符不是 (
或)
,所以不作处理;场景5中,索引为 3
的字符为 )
,此时栈顶是索引为 1
的 (
。两者是匹配的,故 出栈
,之后栈中只有一个元素,如场景6:
也就是说索引 1
和 索引 3
配对了,此时我们可以记录这两个索引值,以便后续处理。如下所示:
接下来继续匹配,索引为 4
的字符不是 (
或)
,所以不作处理;场景8中,索引为 5
的字符为 )
,此时栈顶是索引为 0
的 (
。两者是匹配的,故 出栈
,之后栈中只有没有元素,如场景9:
这样我们就得到了 0
和 5
索引是括号匹配的区间,也记录下来:
后面同理,根据 )
字符对比,通过 (
字符的出入栈情况,我们就可以获取到括号匹配的空间。
4.代码实现
首先,Dart 中的栈结构可以通过 Queue
实现,它是双链表的数据结构,可以支持两端操作,所以既可以当栈,又可以当队列来用。栈结构是先进后出,每次元素都在末尾添加,所以入栈是 addLast
方法,出栈是末尾元素先出战,即 removeLast
方法。
另外,抽象出 Position
概念,记录括号的起止点,其中包含 start
和 end
索引位:
class Position {
final int start;
final int end;
Position({required this.start,required this.end});
String part(List parts)=> parts.sublist(start+1,end).join();
@override
String toString() {
return 'Position{start: $start, end: $end}';
}
}
前面的示意图理解后,逻辑处理非常简单,也就下面的十几行:(
时入栈, )
是栈非空,栈顶出栈,并收集配对的节点即可。
main() {
String src = r'(((\d{1,4})年)(\d{1,2}))月(\d{1,2})日';
RegExp exp = RegExp(r'(?<=\()|(?<=\))|(?=\()|(?=\))');
List<String> parts = src.split(exp);
final Queue<int> stack = Queue();
List<Position> positionList = [];
for (int i = 0; i < parts.length; i++) {
String target = parts[i];
if (target == "(") {
stack.addLast(i);
}
if (target == ")") {
if (stack.isNotEmpty) {
int matchIndex = stack.removeLast();
// 收集配对的 节点
positionList.add(Position(
start: matchIndex,
end: i,
));
}
}
}
}
通过调试可以看出,收集的 positionList
正是图示中两个配对括号的索引位:
由于正则的分组规则,我们需要通过根据 (
的索引位排下顺序,如下所示:
positionList.sort((a,b)=>a.start-b.start);
print('===正则表达式:== ${src}=====');
print('===分组情况=====');
positionList.forEach((element) {
print(element.part(parts));
});
这样,给定正则表达式,我们就能分析出分组情况:
5.正则匹配的分组测试
我们可以通过一个小案例测试一下该正则的分组匹配情况:
main() {
String src = r'光绪七年辛巳年八月初三(1881年9月25日),出生于浙江绍兴城内东昌坊新台门周家。幼名阿张,长根,长庚,学名周樟寿。';
String reg = r'(((\d{1,4})年)(\d{1,2}))月(\d{1,2})日';
RegExp exp = RegExp(reg);
List<RegExpMatch> allMatches = exp.allMatches(src).toList();
for (var match in allMatches) {
print('match.groupCount:${match.groupCount}');
for (int i = 0; i <= match.groupCount; i++) {
print("groupIndex:$i====match:${match.group(i)}====");
}
}
}
打印结果如下,根据上面分析的结果,可以看到是正确的:
---->[打印结果]----
groupIndex:0====match:1881年9月25日====
groupIndex:1====match:1881年====
groupIndex:2====match:1881====
groupIndex:3====match:9====
groupIndex:4====match:25====
---->[分析出的分组结果]----
(\d{1,4})年
\d{1,4}
\d{1,2}
\d{1,2}
再用更复杂一些的括号实验一下,比如: (((\d{1,4})年)(\d{1,2})月)((\d{1,2})日)
,测试结果如下:
---->[正则匹配打印结果]----
groupIndex:0====match:1881年9月25日====
groupIndex:1====match:1881年9月====
groupIndex:2====match:1881年====
groupIndex:3====match:1881====
groupIndex:4====match:9====
groupIndex:5====match:25日====
groupIndex:6====match:25====
---->[分析出的分组结果]----
((\d{1,4})年)(\d{1,2})月
(\d{1,4})年
\d{1,4}
\d{1,2}
(\d{1,2})日
\d{1,2}
6.非捕获的处理
(?:)
可以设置非捕获,表示当前括号不需要作为一个组,我们需要对其进行处理,处理方式也非常简单,移除匹配的以 ?:
开头的记录即可:
positionList.sort((a,b)=>a.start-b.start);
positionList.removeWhere((element) => element.part(parts).startsWith("?:"));
positionList.forEach((element) {
print(element.part(parts));
});
下面通过: (((?:\d{1,4})年))(?:(\d{1,2}))月(\d{1,2})日
实验一下,测试结果如下:
---->[正则匹配打印结果]----
groupIndex:0====match:1881年9月25日====
groupIndex:1====match:1881年====
groupIndex:2====match:1881年====
groupIndex:3====match:9====
groupIndex:4====match:25====
---->[分析出的分组结果]----
((?:\d{1,4})年)
(?:\d{1,4})年
\d{1,2}
\d{1,2}
好了,本文到这里就结束了,这样通过一个正则表达式,就可以完美地解析出它的分组情况。《玩转正则表达式》
小册正在进行中,敬请期待 ~