有没有一种方法可以在正则表达式中定义自定义的缩写?

5

我有一个形式为的正则表达式

def parse(self, format_string):
    for m in re.finditer(
        r"""(?: \$ \( ( [^)]+ ) \) )   # the field access specifier
          | (
                (?:
                    \n | . (?= \$ \( ) # any one single character before the '$('
                )
              | (?:
                    \n | . (?! \$ \( ) # any one single character, except the one before the '$('
                )*
            )""",
        format_string,
        re.VERBOSE):
    ...

我想要用一些自定义的简写常量来替换所有重复序列(\$ \(),这个常量看起来像这样:
def parse(self, format_string):
    re.<something>('\BEGIN = \$\(')
    for m in re.finditer(
        r"""(?: \BEGIN ( [^)]+ ) \) )   # the field access specifier
          | (
                (?:
                    \n | . (?= \BEGIN ) # any one single character before the '$('
                )
              | (?:
                    \n | . (?! \BEGIN ) # any one single character, except the one before the '$('
                )*
            )""",
        format_string,
        re.VERBOSE):
    ...

有没有一种方法可以仅使用正则表达式(即不使用Python的字符串格式化将\BEGIN替换为\$\()来完成这个任务?
澄清:Python源代码仅用于上下文和说明。我正在寻找正则表达式解决方案,该解决方案可在某些RE方言(可能不是Python的方言)中使用,而不是特定于Python的解决方案。

好吧,这里的“可移植”并不是指生成的带有自定义简写的正则表达式可以在不同的编程语言中使用。 - Michael Pankov
1
许多正则表达式方言不支持冗长的正则表达式,或者断言。 - Pavel Anossov
1
在 PCRE 中是可能的,但我猜你想要一个 Python 的解决方案? - Martin Ender
2
@HamZa 我认为这只是一个例子,展示了原始帖子所想象的重用语法可能会是什么样子。 - Martin Ender
我同意。它可以使复杂的正则表达式更易读,但需要在字符串变量名称中表达您的意图。对于捕获组计数,一些正则表达式引擎(至少是 .net 和 Java)具有命名捕获组,从而避免了在更改表达式并保持捕获组编号同步时出现的所有混乱。 - FrankPl
显示剩余7条评论
1个回答

9
我认为在Python的正则表达式中这是不可能的。你需要使用递归(或者说模式重用),这只有PCRE支持。事实上,PCRE甚至在它的手册页面(搜索“定义子模式”)中提到了如何定义简写。
在PCRE中,你可以使用递归语法类似于反向引用的方式——除了模式被再次应用,而不是尝试从反向引用中获取相同的文字。例如:
/(\d\d)-(?1)-(?1)/

匹配类似日期的内容(其中(?1)将被替换为\d\d并再次进行评估)。这非常强大,因为如果您在引用的组内部使用此结构,则会得到递归 - 但是即使我们不需要那样做。以上内容也适用于命名组:

/(?<my>\d\d)-(?&my)-(?&my)/

现在我们已经非常接近了,但是定义也是模式的第一次使用,这样有点使表达式混乱。诀窍是首先在从不被评估的位置中使用该模式。手册建议使用一个条件,该条件取决于一个(不存在的)组DEFINE
/
(?(DEFINE)
  (?<my>\d\d)
)
(?&my)-(?&my)-(?&my)
/x

构造函数(?(group)true|false)应用于模式true,如果组group在之前使用过,则应用该模式,否则(可选的)模式false。由于没有DEFINE组,条件将始终为false,并且将跳过true模式。因此,我们可以放置各种定义,而不必担心它们会被应用并混淆我们的结果。这样,我们可以将它们放入模式中,而不真正使用它们。

另一种选择是负向先行断言,它永远不会到达表达式定义的点:

/
(?!
  (?!)     # fail - this makes the surrounding lookahead pass unconditionally
  # the engine never gets here; now we can write down our definitions
  (?<my>\d\d) 
)
(?&my)-(?&my)-(?&my)
/x

然而,只有在没有条件语句但有命名模式重用的情况下才需要使用这种形式(我不认为存在这样的味道)。另一种变体的优点在于使用DEFINE明确了组的用途,而前瞻方法有点含糊不清。因此,回到您最初的示例:
/
# Definitions
(?(DEFINE)
  (?<BEGIN>[$][(])
)
# And now your pattern
  (?: (?&BEGIN) ( [^)]+ ) \) ) # the field access specifier
|
  (
    (?: # any one single character before the '$('
      \n | . (?= (?&BEGIN) ) 
    )
  | 
    (?: # any one single character, except the one before the '$('
      \n | . (?! (?&BEGIN) ) 
    )*
  )
/x

这种方法有两个主要注意事项:
  1. 递归引用是原子的。也就是说,一旦引用匹配到了某个内容,它就不会再回溯了。对于某些情况,这意味着您必须在构建表达式时聪明一点,以便第一个匹配始终是您想要的匹配。
  2. 您不能在定义的模式内部使用捕获。如果使用类似于(?<myPattern>a(b)c)并重复使用它,那么b将永远不会被捕获 - 在重复使用模式时,所有组都是非捕获的。
然而,与任何插值或串联的最重要优点是,您永远不会使用无效的模式,并且您也不会弄乱捕获组计数。

2
此答案已添加到Stack Overflow正则表达式FAQ,位于“控制字符和递归”下。 - aliteralmind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接