是否可以在打印之前清理冗长的 python 正则表达式?

Is is possible to clean a verbose python regex before printing it?

设置:

假设我在脚本中定义了以下正则表达式。我想把评论留在那里留给以后的我,因为我很健忘。

RE_TEST = re.compile(r"""[0-9]            # 1 Number
                         [A-Z]            # 1 Uppercase Letter
                         [a-y]            # 1 lowercase, but not z
                         z                # gotta have z...
                         """,
                     re.VERBOSE)

print(magic_function(RE_TEST))   # returns: "[0-9][A-Z][a-y]z"

问题:

Python (3.4+) 是否有办法将其转换为简单的字符串 "[0-9][A-Z][a-y]z"

可能的解决方案:

This question ("strip a verbose python regex") seems to be pretty close to what I'm asking for and it was answered。但那是几年前的事了,所以我想知道是否找到了新的(最好是内置的)解决方案。

除上述方法外,还有变通方法,例如使用隐式字符串连接,然后使用 .pattern 属性:

RE_TEST = re.compile(r"[0-9]"      # 1 Number
                     r"[A-Z]"      # 1 Uppercase Letter
                     r"[a-y]"      # 1 lowercase, but not z
                     r"z",         # gotta have z...
                     re.VERBOSE)

print(RE_TEST.pattern)    # returns: "[0-9][A-Z][a-y]z"

或者只是单独注释模式而不编译它:

# matches pattern "nXxz"
RE_TEST = "[0-9][A-Z][a-y]z"
print(RE_TEST)

但我真的很想保持编译后的正则表达式不变(第一个示例)。也许我正在从某个文件中提取正则表达式字符串,而该文件已经在使用详细形式。

背景

我问是因为我想建议对 unittest 模块进行编辑。

现在,如果您 运行 assertRegex(string, pattern) 使用带有注释的已编译模式并且该断言失败,那么打印输出有点难看(下面是一个虚拟正则表达式):

Traceback (most recent call last):
  File "verify_yaml.py", line 113, in test_verify_mask_names
    self.assertRegex(mask, RE_MASK)
AssertionError: Regex didn't match: '(X[1-9]X[0-9]{2})      # comment\n                         |(XXX[0-9]{2})         # comment\n                         |(XXXX[0-9E])          # comment\n                         |(XXXX[O1-9])          # c
omment\n                         |(XXX[0-9][0-9])       # comment\n                         |(XXXX[
1-9])           # comment\n                         ' not found in 'string'

我建议 assertRegexassertNotRegex 方法在打印正则表达式之前清理正则表达式,方法是删除注释和额外的空格或以不同的方式打印它。

通过 sre_parse 处理此问题的方式,您的冗长正则表达式确实没有任何意义 "converted" 变成常规表达式然后进行解析。相反,您的冗长正则表达式被直接馈送到解析器,其中 VERBOSE 标志的存在使其忽略字符 classes 之外的未转义空格,以及从未转义的 # 到结尾-line 如果它不在字符 class 或捕获组 内(文档中缺少)。

解析冗长的正则表达式的结果不是 "[0-9][A-Z][a-y]z"。而是:

[(IN, [(RANGE, (48, 57))]), (IN, [(RANGE, (65, 90))]), (IN, [(RANGE, (97, 121))]), (LITERAL, 122)]

为了正确地将冗长的正则表达式转换为 "[0-9][A-Z][a-y]z",您可以自己解析它。你可以用像 pyparsing 这样的库来做到这一点。您问题中链接的另一个答案使用正则表达式,它通常不会正确复制行为(具体来说,字符内的空格 classes 和 # inside capture groups/character classes。甚至只是处理转义不如好的解析器方便。)

以下经过测试的脚本包含一个函数,可以很好地将 xmode 正则表达式字符串转换为非 xmode:

pcre_detidy(retext)

# Function pcre_detidy to convert xmode regex string to non-xmode.
# Rev: 20160225_1800
import re
def detidy_cb(m):
    if m.group(2): return m.group(2)
    if m.group(3): return m.group(3)
    return ""

def pcre_detidy(retext):
    decomment = re.compile(r"""(?#!py/mx decomment Rev:20160225_1800)
        # Discard whitespace, comments and the escapes of escaped spaces and hashes.
          ( (?: \s+                  # Either g1of3 : Stuff to discard (3 types). Either ws,
            | \#.*                   # or comments,
            | \(?=[\r\n]|$)         # or lone escape at EOL/EOS.
            )+                       # End one or more from 3 discardables.
          )                          # End : Stuff to discard.
        | ( [^\[(\s#\]+             # Or g2of3 : Stuff to keep. Either non-[(\s# \.
          | \[^# Q\r\n]             # Or escaped-anything-but: hash, space, Q or EOL.
          | \(                       # Or an open parentheses, optionally
            (?:\?\#[^)]*(?:\)|$))?   # starting a (?# Comment group).
          | \[\^?\]? [^\[\]\]*      # Or Character class. Allow unescaped ] if first char.
            (?:\[^Q][^\[\]\]*)*    # {normal*} Zero or more non-[], non-escaped-Q.
            (?:                      # Begin unrolling loop {((special1|2) normal*)*}.
              (?: \[(?::\^?\w+:\])?  # Either special1: "[", optional [:POSIX:] char class.
              | \Q       [^\]*     # Or special2: \Q..\E literal text. Begin with \Q.
                (?:\(?!E)[^\]*)*   # \Q..\E contents - everything up to \E.
                (?:\E|$)            # \Q..\E literal text ends with \E or EOL.
              )        [^\[\]\]*    # End special: One of 2 alternatives {(special1|2)}.
              (?:\[^Q][^\[\]\]*)*  # More {normal*} Zero or more non-[], non-escaped-Q.
            )* (?:\]|\?$)           # End character class with ']' or EOL (or \EOL).
          | \Q       [^\]*         # Or \Q..\E literal text start delimiter.
            (?:\(?!E)[^\]*)*       # \Q..\E contents - everything up to \E.
            (?:\E|$)                # \Q..\E literal text ends with \E or EOL.
          )                          # End : Stuff to keep.
        | \([# ])                   # Or g3of3 : Escaped-[hash|space], discard the escape.
        """, re.VERBOSE | re.MULTILINE)
    return re.sub(decomment, detidy_cb, retext)

test_text = r"""
        [0-9]            # 1 Number
        [A-Z]            # 1 Uppercase Letter
        [a-y]            # 1 lowercase, but not z
        z                # gotta have z...
        """
print(pcre_detidy(test_text))

此函数确定用 pcre-8/pcre2-10 xmode 语法编写的正则表达式。

它在 [character classes](?#comment groups)\Q...\E 文字文本范围内保留空格。

RegexTidy

上面的 decomment 正则表达式是我在即将发布的应用程序中使用的一个变体:RegexTidy 应用程序,它不仅会删除如上所示的正则表达式(很容易做到),但它也会采用另一种方式 Tidy 一个正则表达式——即将其从非 xmode 正则表达式转换为 xmode 语法,同时向嵌套组添加空格缩进添加评论(这更难)。

p.s。在给出这个答案之前,因为它使用的正则表达式长于几行,所以在对一般原则投反对票之前,请添加一条评论来描述一个未正确处理的示例。干杯!