答案是不用说,是的!你可以肯定写一个Java正则表达式匹配一个ñ b ñ。它使用肯定的前瞻性进行断言,并使用一个嵌套的引用进行“计数”。
该答案不是立即给出模式,而是将引导读者完成其推导过程。随着解决方案的逐步构建,给出了各种提示。在这方面,希望这个答案将不仅仅包含另一个整洁的正则表达式模式。希望读者还将学习如何“思考正则表达式”,以及如何将各种结构和谐地组合在一起,以便他们将来可以自己衍生出更多模式。
为简洁起见,用于开发解决方案的语言将是PHP。模式完成后的最终测试将用Java完成。
步骤1:先行断言
让我们从一个简单的问题开始:我们要a+
在字符串的开头进行匹配,但前提是要紧随其后b+
。我们可以^
用来锚定匹配项,并且由于我们只想匹配a+
不带的匹配项b+
,因此可以使用超前断言(?=…)
。
这是带有简单测试工具的模式:
function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
输出为(如ideone.com所示):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
这正是我们想要的输出:a+
仅当它在字符串的开头且紧随其后时,我们才匹配b+
。
课程:您可以在环顾四周中使用模式进行断言。
第2步:提前捕获(和自由间距模式)
现在让我们说,即使我们不希望将b+
其作为比赛的一部分,我们还是希望将其捕获到第1组中。而且,由于我们期望使用更复杂的模式,因此请使用x
修饰符进行自由间距设置,这样我们可以使我们的正则表达式更具可读性。
在我们之前的PHP代码段的基础上,我们现在具有以下模式:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
现在的输出为(如ideone.com所示):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
请注意,例如aaa|b
是join
每个组使用捕获的结果'|'
。在这种情况下,捕获组0(即模式匹配的对象)aaa
,并捕获组1 b
。
课程:您可以捕获一个环顾四周。您可以使用自由间距来增强可读性。
步骤3:将前瞻重构为“循环”
在介绍计数机制之前,我们需要对我们的模式进行一次修改。当前,前瞻位于+
重复“循环”之外。到目前为止,这还不错,因为我们只是想断言b+
我们后面有一个a+
,但是最终我们真正想做的是断言对于a
“循环”内我们匹配的每个循环,都有一个对应的对应项b
。
现在不用担心计数机制,只需按如下所示进行重构即可:
- 首先重构
a+
,以(?: a )+
(注意,(?:…)
是一个非捕获组)
- 然后将超前移动到该非捕获组中
- 请注意,我们现在必须先“跳过”,
a*
然后才能“看到” b+
,因此请相应地修改模式
因此,我们现在有以下内容:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
输出与之前相同(如ideone.com所示),因此在这方面没有任何变化。重要的是,现在我们在“循环”的每次迭代中都进行断言+
。在我们当前的模式下,这不是必需的,但是接下来,我们将使用自我参照为组1“计数”。
课程:您可以在非捕获组中捕获。环顾四周可以重复。
步骤4:这是我们开始计算的步骤
这是我们要做的事情:我们将重写第1组,以便:
- 在的第一次迭代结束
+
时,如果第一次a
匹配,则应捕获b
- 在第二次迭代的末尾,当另一个
a
匹配时,它应该捕获bb
- 在第三次迭代结束时,它应该捕获
bbb
- ...
- 在第n次迭代的最后,组1应该捕获b n
- 如果没有足够
b
的内容捕获到组1中,则断言只会失败
因此,现在(b+)
必须将第1组重写为(\1 b)
。也就是说,我们尝试将“添加” b
到上一次迭代中捕获的第1组。
这里存在一个小问题,因为该模式缺少“基本情况”,即在没有自引用的情况下可以匹配的情况。需要基本情况,因为组1开始“未初始化”;它尚未捕获任何内容(甚至不是空字符串),因此自引用尝试将始终失败。
有许多解决方法,但是现在让我们将自引用匹配设置为可选,即\1?
。这可能行得通,也可能行不通,但让我们看看它能做什么,如果有任何问题,那么我们将在那座桥上过桥。另外,在进行测试时,我们将添加更多测试用例。
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
现在的输出为(如ideone.com所示):
aaa 0
aaab 1 aaa|b # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b # yes!
aabb 1 aa|bb # YES!!
aaabbbbb 1 aaa|bbb # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....
哈!看来我们现在真的很接近解决方案!我们设法通过自我参考使第1组“计数”!但是等等...第二个和最后一个测试用例出了点问题!b
s 不够,以某种方式计入了错误!我们将在下一步研究为什么会发生这种情况。
课程:一种“初始化”自引用组的方法是使自引用匹配为可选。
步骤4½:了解问题所在
问题在于,由于我们将自引用匹配设置为可选,因此当计数器不足时,“计数器”可以“重置”为0 b
。让我们仔细检查一下模式的每次迭代(aaaaabbb
作为输入)会发生什么。
a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
_
a a a a a b b b
↑
# 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
# so it matched and captured just b
___
a a a a a b b b
↑
# 2nd iteration: Group 1 matched \1b and captured bb
_____
a a a a a b b b
↑
# 3rd iteration: Group 1 matched \1b and captured bbb
_
a a a a a b b b
↑
# 4th iteration: Group 1 could still match \1, but not \1b,
# (!!!) so it matched and captured just b
___
a a a a a b b b
↑
# 5th iteration: Group 1 matched \1b and captured bb
#
# No more a, + "loop" terminates
哈!在第4次迭代中,我们仍然可以匹配\1
,但是我们无法匹配\1b
!由于我们允许使用进行自参考匹配\1?
,因此引擎回溯并采用了“不用了,谢谢”选项,这使我们能够进行匹配和捕获b
!
请注意,但是,除了第一次迭代外,您始终可以仅匹配自引用\1
。当然,这很明显,因为这是我们在上一次迭代中捕获的,并且在我们的设置中,我们始终可以再次对其进行匹配(例如,如果bbb
上次捕获,则可以保证仍然存在bbb
,但可能还是可能不是bbbb
这次)。
课程:当心回溯。regex引擎将尽您所能进行尽可能多的回溯,直到给定的模式匹配为止。这可能会影响性能(即灾难性的回溯)和/或正确性。
第5步:自救!
现在,“修复”应该很明显:将可选重复与所有格修饰符结合在一起。也就是说,代替简单地?
使用?+
代替(记住,量化为所有格的重复不会回溯,即使这种“合作”可能导致整体模式匹配)。
用非常非正式的说法,这就是?+
,?
并??
说:
?+
- (可选)“不必在那里,”
- (可能)“但是,如果存在,则必须拿走它,不要放开它!”
?
??
在我们的设置中,\1
不会第一次出现,但是之后总会出现,我们一直想匹配它。因此,\1?+
将完全完成我们想要的。
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
现在的输出是(如ideone.com上所示):
aaa 0
aaab 1 a|b # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb # Hurrahh!!!
哎呀!问题解决了!!!现在,我们正按照我们想要的方式正确计数!
课程:了解贪婪,勉强和所有格重复的区别。可选拥有可能是强大的组合。
步骤6:画龙点睛
因此,我们现在拥有的是一个a
重复匹配的模式,对于每个匹配的模式,在组1中a
都有一个对应的b
捕获。+
当不再存在时a
,或者如果断言失败,因为没有对应的b
,则终止。一个a
。
要完成工作,我们只需要追加到我们的pattern即可\1 $
。现在,这是对第1组匹配项的后向引用,后面是行锚的末尾。锚确保b
字符串中没有多余的;换句话说,实际上我们有一个n b n。
这是最终模式,其中包含其他测试用例,其中包括一个10,000个字符长的用例:
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
'', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
str_repeat('a', 5000).str_repeat('b', 5000)
);
$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
它发现4场比赛:ab
,aabb
,aaabbb
,和一个5000 b 5000。在ideone.com上运行仅需0.06s。
步骤7:Java测试
因此该模式可在PHP中运行,但最终目标是编写一种可在Java中运行的模式。
public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"", // false
"ab", // true
"abb", // false
"aab", // false
"aabb", // true
"abab", // false
"abc", // false
repeat('a', 5000) + repeat('b', 4999), // false
repeat('a', 5000) + repeat('b', 5000), // true
repeat('a', 5000) + repeat('b', 5001), // false
};
for (String test : tests) {
System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN));
}
}
static String repeat(char ch, int n) {
return new String(new char[n]).replace('\0', ch);
}
该模式按预期工作(如ideone.com所示)。
现在我们得出结论...
需要说的a*
是,前瞻和“主+
循环”都允许回溯。鼓励读者确认为什么这不是正确性方面的问题,以及为什么同时拥有所有格也起作用(尽管也许以相同的方式混合强制性和非强制性所有格量词可能会导致误解)。
还应该说,虽然有一个匹配a n b n的正则表达式模式很简单,但实际上这并不总是“最佳”解决方案。更好的解决方案是简单地匹配^(a+)(b+)$
,然后比较托管编程语言中第1组和第2组捕获的字符串的长度。
在PHP中,可能看起来像这样(如ideone.com所示):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
本文的目的不是要说服读者相信正则表达式几乎可以做任何事情;它显然不能,甚至对于它可以做的事情,如果可以导致更简单的解决方案,则至少应考虑对托管语言的部分委派。
如顶部所述,尽管本文必须标记[regex]
为stackoverflow,但它可能不止于此。虽然肯定有学习断言,嵌套引用,所有格限定符等的价值,但是也许更大的一课是创造性的过程,通过该过程人们可以尝试解决问题,遇到困难时通常需要的决心和努力工作各种约束,从各个部分的系统组成,以建立有效的解决方案,等等。
奖金材料!PCRE递归模式!
由于我们确实提出了PHP,因此需要说PCRE支持递归模式和子例程。因此,以下模式适用于preg_match
(如在ideone.com上所示):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
当前,Java的正则表达式不支持递归模式。
更多的奖励材料!匹配一个n b n c n!
因此,我们已经看到了如何匹配非常规的n b n,但是仍然与上下文无关,但是我们还可以匹配甚至不是上下文无关的n b n c n吗?
答案是肯定的!鼓励读者尝试自己解决这个问题,但是下面提供了解决方案(ideone.com上的Java实现)。
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $