这个正则表达式替换如何反转字符串?
Posted
技术标签:
【中文标题】这个正则表达式替换如何反转字符串?【英文标题】:How does this regex replacement reverse a string? 【发布时间】:2011-04-11 06:11:08 【问题描述】:这是一系列教育正则表达式文章的第四部分。它展示了嵌套引用(参见:How does this regex find triangular numbers?)与断言(参见:How can we match a^n b^n with Java regex?)中“计数”的组合如何用于反转字符串。以编程方式生成的模式使用元模式抽象(参见:How does this Java regex detect palindromes?)。在该系列中,这些技术首次用于替换而不是整个字符串匹配。
提供了完整的 Java 和 C# 实现。包括励志名言。
使用正则表达式反转字符串似乎从来都不是一个好主意,如果它完全可能的话,甚至不是立即显而易见的,如果是的话,人们可能会如何尝试这样做。
虽然仍然不是一个好主意,但至少现在我们知道这是可能的,因为这是一种方法:
C# (also on ideone.com)
using System;
using System.Text.RegularExpressions;
public class TwoDollarReversal
public static void Main()
string REVERSE =
@"(?sx) . grab$2"
.Replace("grab$2",
ForEachDotBehind(
AssertSuffix(@"((.) \1?)")
)
);
Console.WriteLine(
Regex.Replace(
@"nietsniE treblA --
hguone llew ti dnatsrednu t'nod uoy ,ylpmis ti nialpxe t'nac uoy fI",
REVERSE, "$2"
)
);
// If you can't explain it simply, you don't understand it well enough
// -- Albert Einstein
// performs an assertion for each dot behind current position
static string ForEachDotBehind(string assertion)
return "(?<=(?:.assertion)*)".Replace("assertion", assertion);
// asserts that the suffix of the string matches a given pattern
static string AssertSuffix(string pattern)
return "(?=.*$(?<=pattern))".Replace("pattern", pattern);
Java (also on ideone.com)
class TwoDollarReversal
public static void main(String[] args)
String REVERSE =
"(?sx) . grab$2"
.replace("grab$2",
forEachDotBehind(
assertSuffix("((.) \\1?)")
)
);
System.out.println(
"taerG eht rednaxelA --\nyrt lliw ohw mih ot elbissopmi gnihton si erehT"
.replaceAll(REVERSE, "$2")
);
// There is nothing impossible to him who will try
// -- Alexander the Great"
static String forEachDotBehind(String assertion)
return "(?<=^(?:.assertion)*?)".replace("assertion", assertion);
static String assertSuffix(String pattern)
return "(?<=(?=^.*?pattern$).*)".replace("pattern", pattern);
C# 和 Java 版本似乎使用相同的整体算法,仅在抽象的实现细节上略有不同。
显然这不是反转字符串的最佳、最直接、最有效的方法。也就是说,为了了解正则表达式;如何概念化模式;引擎如何工作以匹配它们;如何将各个部分组合在一起以构建我们想要的东西;如何以可读和可维护的方式这样做;只是为了学习新事物的纯粹乐趣,我们能解释一下这是如何工作的吗?
附录:备忘单!
这是对所使用的基本正则表达式结构的简要说明:
(?sx)
是嵌入标志 modifiers。 s
启用“单行”模式,允许 dot 匹配 ANY 字符(包括换行符)。 x
启用 free-spacing 模式,其中未转义的空格将被忽略(#
可用于 cmets)。
^
和 $
是行首和行尾anchors。
?
作为重复说明符表示 optional(即零或一)。作为重复量词,例如.*?
表示*
(即零个或多个)重复是reluctant/non-greedy。
(…)
用于grouping。 (?:…)
是非捕获组。一个捕获组保存它匹配的字符串;它允许后退/前进/嵌套引用(例如\1
)、替换替换(例如$2
)等。
(?=…)
是积极的lookahead;它看起来向右断言给定模式的匹配。 (?<=…)
是一个积极的lookbehind;它向左看。
语言参考/其他资源
MSDN - Regular Expression Language Elements --System.Text.RegularExpressions
Java Tutorials/Essential classes/Regular expressions -- java.util.regex.Pattern
【问题讨论】:
关于meta系列的讨论:meta.stackexchange.com/questions/62695/… 嗯,非常漂亮(而且系列概念总体上很有趣)。我认为即使对于那些不熟悉正则表达式的人来说,解释也相当清楚,尽管我希望害怕它们的人在看到“巫毒魔法”后不要逃跑,而不是留下来阅读和学习,呵呵。 @Tim:从现在开始,我打算开始写中级的东西,没有什么“高级”的东西。我会继续使用“有趣”的例子来让学习变得更有趣。 我喜欢这个,即使没有警告 ;) +1 *。该死的你可变长度的lookbehinds!你可以和他们一起玩得很开心。太糟糕了,我们在 Perl/PCRE 中没有这些。关于正则表达式的一系列问题/答案。 :-) 哦,对于那些感兴趣的人(比如我),完整的 C# 表达式是:(?sx) . (?<=(?:.(?=.*$(?<=((.) \1?))))*)
【参考方案1】:
概述
在高层次上,该模式匹配任何一个字符 .
,但另外执行一个 grab$2
操作,该操作捕获匹配到组 2 中的字符的反转“配对”。这种捕获是通过构建完成的输入字符串的后缀,其长度匹配到当前位置的前缀长度。为此,我们将assertSuffix
应用于将后缀增加一个字符的模式,重复一次forEachDotBehind
。第 1 组捕获此后缀。该后缀的第一个字符(在第 2 组中捕获)是匹配字符的反转“配对”。
因此,将每个匹配的字符替换为其“伴侣”具有反转字符串的效果。
工作原理:一个更简单的例子
为了更好地理解正则表达式模式的工作原理,让我们首先将其应用于更简单的输入。此外,对于我们的替换模式,我们将“转储”出所有捕获的字符串,以便更好地了解正在发生的事情。这是 Java 版本:
System.out.println(
"123456789"
.replaceAll(REVERSE, "[$0; $1; $2]\n")
);
以上打印(as seen on ideone.com):
[1; 9; 9]
[2; 89; 8]
[3; 789; 7]
[4; 6789; 6]
[5; 56789; 5]
[6; 456789; 4]
[7; 3456789; 3]
[8; 23456789; 2]
[9; 123456789; 1]
因此,例如[3; 789; 7]
表示点匹配3
(在第0组中捕获),对应的后缀是789
(第1组),其第一个字符是7
(第2组)。注意7
是3
的“伙伴”。
current position after
the dot matched 3
↓ ________
1 2 [3] 4 5 6 (7) 8 9
\______/ \______/
3 dots corresponding
behind suffix of length 3
请注意,角色的“伴侣”可能在其右侧或左侧。一个角色甚至可能是它自己的“伴侣”。
后缀是如何构建的:嵌套引用
负责匹配和构建增长后缀的模式如下:
((.) \1?)
|\_/ |
| 2 | "suffix := (.) + suffix
|_______| or just (.) if there's no suffix"
1
请注意,在组 1 的定义中是对其自身的引用(使用\1
),尽管它是可选的(使用?
)。可选部分提供了“基本情况”,一种组在不引用自身的情况下进行匹配的方式。这是必需的,因为当组尚未捕获任何内容时,尝试匹配组引用总是会失败。
一旦第 1 组捕获了某些内容,我们的设置中就不会执行可选部分,因为我们上次捕获的后缀这次仍然存在,我们总是可以在此后缀的开头添加另一个字符 @ 987654341@。这个前置字符被捕获到第 2 组中。
因此,此模式尝试将后缀增加一个点。因此,重复一次forEachDotBehind
将产生一个后缀,其长度正好是到我们当前位置的前缀长度。
assertSuffix
和 forEachDotBehind
如何工作:元模式抽象
请注意,到目前为止,我们已将 assertSuffix
和 forEachDotBehind
视为黑盒。事实上,把这个讨论留到最后是一种深思熟虑的行为:名称和简短的文档暗示了他们所做的什么,这些信息足以让我们编写和阅读我们的REVERSE
模式!
经过仔细检查,我们发现这些抽象的 Java 和 C# 实现略有不同。这是由于两个正则表达式引擎之间的差异造成的。
.NET 正则表达式引擎允许在后视中使用完整的正则表达式,因此这些元模式在这种风格下看起来更加自然。
AssertSuffix(pattern) := (?=.*$(?<=pattern))
,即我们使用前瞻来一直到字符串的末尾,然后使用嵌套后瞻来匹配模式与后缀。
ForEachDotBehind(assertion) := (?<=(?:.assertion)*)
,即我们只是在后视中匹配 .*
,将断言与非捕获组内的点一起标记。
由于 Java 不正式支持无限长度的后视(但在某些情况下它仍然可以工作),它的对应物有点尴尬:
assertSuffix(pattern) := (?<=(?=^.*?pattern$).*)
,即我们使用lookbehind一直到字符串的开始,然后使用嵌套lookahead来匹配整个字符串,前置后缀模式用.*?
不情愿地匹配一些不相关的前缀。
forEachDotBehind(assertion) := (?<=^(?:.assertion)*?)
,即我们使用带有不情愿重复的锚定lookbehind,即^.*?
(同样将断言与非捕获组内的点一起标记)。
应该注意,虽然这些元模式的 C# 实现在 Java 中不起作用,Java 实现在 C# 中可以工作 (see on ideone.com)。因此,实际上没有必要为 C# 和 Java 提供不同的实现,但 C# 实现特意利用了更强大的 .NET 正则表达式引擎后向支持来更自然地表达模式。
因此,我们展示了使用元模式抽象的好处:
我们可以独立开发、检查、测试、优化等这些元模式实现,或许可以利用风味特定的功能来获得额外的性能和/或可读性。 一旦这些构建块开发并经过良好测试,我们就可以简单地将它们用作更大模式的一部分,这使我们能够在更高层次上表达想法,以获得更易读、更可维护、更便携的解决方案。 元模式促进重用,而程序化生成意味着更少的重复虽然这一概念的特殊表现形式相当原始,但也可以更进一步,开发更强大的编程模式生成框架,其中包含经过良好测试和优化的元模式库。
另见
Martin Fowler - Composed Regex .NET regular expressions - Balancing group definition - 元模式的一个很好的例子!结束的想法
需要重申的是,使用正则表达式反转字符串在实践中不是一个好主意。它比必要的复杂得多,性能也很差。
也就是说,这篇文章表明它实际上可以完成,并且当使用元模式抽象在更高级别表示时,该解决方案实际上是非常可读的。作为解决方案的关键组成部分,嵌套引用再次展示在另一个引人入胜的示例中。
不太明显,也许这篇文章还显示了解决起初可能看起来很困难(甚至“不可能”)的问题所需的决心。也许它也显示了对一个主题的更深入理解所带来的思想清晰,这是大量研究和努力的结果。
毫无疑问,正则表达式可能是一个令人生畏的主题,而且它肯定不是为了解决您的所有问题而设计的。然而,这不是可恨的无知的借口,如果你愿意学习的话,这是一门令人惊讶的深刻知识。
【讨论】:
以上是关于这个正则表达式替换如何反转字符串?的主要内容,如果未能解决你的问题,请参考以下文章