这个正则表达式替换如何反转字符串?C#(也在 ideone.com 上) Java(也在 ideone.com 上)

2022-09-02 22:29:35

这是一系列教育正则表达式文章中的第四部分。它显示了嵌套引用(参见:这个正则表达式如何找到三角形数字?)的组合在断言中“计数”(参见:我们如何将a^n b^n与Java正则表达式匹配?)可以用来反转字符串。以编程方式生成的模式使用元模式抽象(请参阅:此 Java 正则表达式如何检测回文?)。在本系列中,这些技术首次用于替换,而不是整个字符串匹配。

提供了完整的工作 Java 和 C# 实现。包括鼓舞人心的名言。

使用正则表达式反转字符串似乎从来都不是一个好主意,如果有可能,甚至也不会立即变得明显,如果是这样,人们可能会如何尝试这样做。

虽然这仍然不是一个好主意,但至少现在我们知道这是可能的,因为这里有一种方法可以做到这一点:

C#也在 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也在 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)是嵌入式标志修饰符。 启用“单行”模式,允许匹配任何字符(包括换行符)。 启用自由间距模式,其中忽略未转义的空格(可用于注释)。sx#
  • ^并且是线的起点和终点锚点$
  • ?作为重复说明符表示可选(即零或一)。作为重复量词,例如 它表示(即零个或多个)重复是不情愿/不贪婪的。.*?*
  • (…)用于分组。 是非捕获组。捕获组保存它匹配的字符串;它允许后退/前进/嵌套引用(例如),替换替换(例如)等。(?:…)\1$2
  • (?=…)是一个积极的展望;它看起来有权断言给定模式的匹配。 是一个积极的眼光;它向左看。(?<=…)

语言参考/其他资源


答案 1

概述

在较高级别,该模式匹配任何一个字符,但另外执行一个动作,该动作捕获匹配到组2中的角色的反转“mate”。此捕获是通过生成输入字符串的后缀来完成的,该后缀的长度与前缀的长度与当前位置的长度相匹配。我们通过应用将后缀增长一个字符的模式来做到这一点,重复一次 。组 1 捕获此后缀。该后缀的第一个字符(在第 2 组中捕获)是匹配字符的反转“mate”。.grab$2assertSuffixforEachDotBehind

因此,将每个匹配的字符替换为其“mate”具有反转字符串的效果。


工作原理:一个更简单的例子

为了更好地理解正则表达式模式的工作原理,让我们首先将其应用于更简单的输入。此外,对于我们的替换模式,我们将“转储”出所有捕获的字符串,以便更好地了解发生了什么。下面是一个 Java 版本:

System.out.println(
    "123456789"
        .replaceAll(REVERSE, "[$0; $1; $2]\n")
);

以上印刷品(如 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]

因此,例如 表示点匹配(在组 0 中捕获),相应的后缀是(组 1),其第一个字符是 (组 2)。请注意,这是 的“伴侣”。[3; 789; 7]3789773

                   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 捕获了某些内容,则在我们的设置中永远不会执行可选部分,因为我们上次捕获的后缀这次仍然存在,并且我们始终可以在此后缀的开头附加另一个字符。此前置字符被捕获到第 2 组中。(.)

因此,此模式尝试将后缀增大一个点。因此,重复此操作一次将产生一个后缀,其长度恰好是前缀的长度,直到我们当前的位置。forEachDotBehind


如何和工作:元模式抽象assertSuffixforEachDotBehind

请注意,到目前为止,我们已将 和 视为黑匣子。事实上,把这个讨论留到最后是一个故意的行为:名称和简短的文档暗示了他们做了什么,这足以让我们编写和阅读我们的模式!assertSuffixforEachDotBehindREVERSE

经过仔细检查,我们发现这些抽象的 Java 和 C# 实现略有不同。这是由于两个正则表达式引擎之间的差异。

.NET 正则表达式引擎允许在外观中提供完整的正则表达式,因此这些元模式在这种风格中看起来更加自然。

  • AssertSuffix(pattern) := (?=.*$(?<=pattern)),即我们使用一个 lookahead 一直到字符串的末尾,然后使用嵌套的 lookinhind 将模式与后缀进行匹配。
  • ForEachDotBehind(assertion) := (?<=(?:.assertion)*),即我们只是在查看后缀中匹配,将断言与非捕获组中的点一起标记。.*

由于Java并不正式支持无限长度的lookbehind(但在某些情况下它仍然可以工作),所以它的对应物有点尴尬:

  • assertSuffix(pattern) := (?<=(?=^.*?pattern$).*),即我们使用一个 lookbehind 一直到字符串的开头,然后使用嵌套的 lookahead 来匹配整个字符串,在后缀模式之前附加一些不情愿的前缀。.*?
  • forEachDotBehind(assertion) := (?<=^(?:.assertion)*?),即我们使用锚定的外观,不情愿地重复,即 (同样,在非捕获组中标记断言以及点)。^.*?

应该注意的是,虽然这些元模式的 C# 实现在 Java 中不起作用,但 Java 实现在 C# 中确实有效请参阅 ideone.com)。因此,实际上不需要为 C# 和 Java 提供不同的实现,但 C# 实现特意利用了更强大的 .NET 正则表达式引擎 lookbehind 支持来更自然地表达模式。

因此,我们已经展示了使用元模式抽象的好处:

  • 我们可以独立开发、检查、测试、优化等这些元模式实现,也许可以利用特定于风格的功能来获得额外的性能和/或可读性。
  • 一旦这些构建块被开发和良好测试,我们就可以简单地将它们用作更大模式的一部分,这使我们能够在更高层次上表达想法,以获得更具可读性,更易于维护,更便携的解决方案。
  • 元模式促进重用,编程生成意味着更少的重复

虽然这个概念的这种特殊表现形式相当原始,但也可以进一步发展这一点,开发一个更健壮的编程模式生成框架,其中包含一个经过充分测试和优化的元模式库。

另请参见


结束语

需要重申的是,在实践中,使用正则表达式反转字符串不是一个好主意。它比必要的复杂得多,并且性能非常差。

也就是说,本文表明它实际上可以完成,并且当使用元模式抽象在更高层次上表达时,解决方案实际上是相当可读的。作为解决方案的关键组件,嵌套引用再次展示在另一个引人入胜的示例中。

不那么具体地说,也许这篇文章也显示了解决一个起初可能看起来困难(甚至“不可能”)的问题所需的决心。也许它也显示了对主题的更深入理解所带来的思维清晰度,这是大量研究和辛勤工作的结果。

毫无疑问,正则表达式可能是一个令人生畏的主题,当然它并不是为了解决你所有的问题而设计的。然而,这并不是可恨的无知的借口,如果你愿意学习,这是一个令人惊讶的深厚知识井。


答案 2