Python 正则表达式中使用F-字符串和re.VERBOSE的方法

667 阅读2分钟

其中我们看了一两种方法,在使用 Python 正则表达式时可以使生活更轻松。

tl;dr:你可以使用 f-strings 组成冗长的正则表达式。

这里有一个真实的例子 - 而不是这样。

1
pattern = r"((?:\(\s*)?[A-Z]*H\d+[a-z]*(?:\s*\+\s*[A-Z]*H\d+[a-z]*)*(?:\s*[\):+])?)(.*?)(?=(?:\(\s*)?[A-Z]*H\d+[a-z]*(?:\s*\+\s*[A-Z]*H\d+[a-z]*)*(?:\s*[\):+])?(?![^\w\s])|$)"

这样做:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
code = r"""
[A-Z]*H  # prefix
\d+      # digits
[a-z]*   # suffix
"""


multicode = fr"""
(?: ( \s* )?               # maybe open paren and maybe space
{code}                      # one code
(?: \s* + \s* {code} )*    # maybe followed by other codes, plus-separated
(?: \s* [):+] )?           # maybe space and maybe close paren or colon or plus
"""




pattern = fr"""
( {multicode} )             # code (capture)
( .*? )                     # message (capture): everything ...
(?=                         # ... up to (but excluding) ...
    {multicode}             # ... the next code
        (?! [^\w\s] )       # (but not when followed by punctuation)
    | $                     # ... or the end
)
"""

pattern = fr""" ( {multicode} ) # code (capture) ( .*? ) # message (capture): everything ... (?= # ... up to (but excluding) ... {multicode} # ... the next code (?! [^\w\s] ) # (but not when followed by punctuation) | $ # ... or the end ) """

作为比较,同样的模式没有使用f-strings(点击展开)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
pattern = r"""
(                       # code (capture)
    # BEGIN multicode


    (?: ( \s* )?       # maybe open paren and maybe space




    # code
    [A-Z]H  # prefix
    \d+      # digits
    [a-z]   # suffix




    (?:                 # maybe followed by other codes,
        \s* + \s*      # ... plus-separated




        # code
        [A-Z]H  # prefix
        \d+      # digits
        [a-z]   # suffix
    )*




    (?: \s* [):+] )?   # maybe space and maybe close paren or colon or plus




    # END multicode
)




( .*? )                 # message (capture): everything ...




(?=                     # ... up to (but excluding) ...
    # ... the next code




    # BEGIN multicode




    (?: ( \s* )?       # maybe open paren and maybe space




    # code
    [A-Z]H  # prefix
    \d+      # digits
    [a-z]   # suffix




    (?:                 # maybe followed by other codes,
        \s* + \s*      # ... plus-separated




        # code
        [A-Z]H  # prefix
        \d+      # digits
        [a-z]   # suffix
    )*




    (?: \s* [):+] )?   # maybe space and maybe close paren or colon or plus




    # END multicode




        # (but not when followed by punctuation)
        (?! [^\w\s] )




    # ... or the end
    | $
)




"""

"""

这比不简洁的那个好,但即使有仔细的格式化和注释,重复也使它很难被理解--等到你不得不改变一些东西的时候,就会发现它很难被理解!

继续阅读,了解细节和一些注意事项。

先决条件

格式化的字符串字元(f-strings) 是在 Python 3.61 中添加的,它提供了一种在字符串字元中嵌入表达式的方法,使用的语法与str.format() 类似。

>>> name = "world"
>>>
>>> "Hello, {name}!".format(name=name)
'Hello, world!'
>>>
>>> f"Hello, {name}!"
'Hello, world!'

粗略的正则表达式(re.VERBOSE) 从很久以前就存在了2,它允许用不重要的空白和注释来编写正则表达式。

>>> text = "H1 code (AH2b+EUH3) fancy code"
>>>
>>> code = r"[A-Z]*H\d+[a-z]*"
>>> re.findall(code, text)
['H1', 'AH2b', 'EUH3']
>>>
>>> code = r"""
... [A-Z]*H  # prefix
... \d+      # digits
... [a-z]*   # suffix
... """
>>> re.findall(code, text, re.VERBOSE)
['H1', 'AH2b', 'EUH3']

一个奇怪的技巧

一旦你看到它,它就很明显了--你可以使用f-strings来组成正则表达式。

>>> multicode = fr"""
... (?: \( )?         # maybe open paren
... {code}            # one code
... (?: \+ {code} )*  # maybe other codes, plus-separated
... (?: \) )?         # maybe close paren
... """
>>> re.findall(multicode, text, re.VERBOSE)
['H1', '(AH2b+EUH3)']

它是如此明显,在我开始使用Python 3.6+之后,我只花了三年时间就做到了,尽管在这段时间里我一直在使用这两个特性。

当然,有任何数量的库来构建正则表达式;这样做的好处是,它没有任何依赖性,也没有任何你需要学习的额外东西。

注意事项

哈希值和空格需要被转义

因为哈希值是用来标记注释的开始,而空格大多被忽略,所以你必须用其他方式来表示它们。

re.VERBOSE的文档很有帮助。

当一行中包含一个不属于字符类的# ,并且前面没有一个未回避的反斜杠,从最左边的这种# 到行尾的所有字符都会被忽略。

也就是说,这不会像非verbose版本那样工作。

>>> re.findall("\d+#\d+", "1#23a")
['1#23']
>>> re.findall("\d+ # \d+", "1#23a", re.VERBOSE)
['1', '23']

但这些可以:

>>> re.findall("\d+ [#] \d+", "1#23a", re.VERBOSE)
['1#23']
>>> re.findall("\d+ \# \d+", "1#23a", re.VERBOSE)
['1#23']

对于空格也是如此:

>>> re.findall("\d+ [ ] \d+", "1 23a", re.VERBOSE)
['1 23']
>>> re.findall("\d+ \  \d+", "1 23a", re.VERBOSE)
['1 23']

哈希值需要特别注意

当组成regexes时, 在同一行中结束一个模式的注释可能会意外地注释包围模式的下一行。

>>> one = "1 # comment"
>>> onetwo = f"{one} 2"
>>> re.findall(onetwo, '0123', re.VERBOSE)
['1']
>>> print(onetwo)
1 # comment 2

这种情况可以通过在新的一行结束模式来避免。

>>> one = """\
... 1 # comment
... """
>>> onetwo = f"""\
... {one} 2
... """
>>> re.findall(onetwo, '0123', re.VERBOSE)
['12']

虽然有点麻烦,但在现实生活中,大多数模式会跨越多行,所以这并不是一个真正的问题。

(注意,只有当你使用注释时才需要这样做)。

括号量词需要被转义

因为f-strings已经使用大括号进行替换,为了表示大括号量词,你必须将大括号加倍。

>>> re.findall("m{2}", "entire mm but only two of mmm")
['mm', 'mm']
>>> letter = "m"
>>> pattern = f"{letter}{{2}}"
>>> re.findall(pattern, "entire mm but only two of mmm")
['mm', 'mm']

我不控制标志

也许你想使用冗长的gex,但又不能控制传递给re函数的标志(例如,因为你要把gex传递给API)。

不用担心!正则表达式语法支持内联标志。

(?aiLmsux)

(一个或多个字母[...])该组匹配空字符串;字母设置相应的标志。[...]re.X(verbose),用于整个正则表达式。[...] 如果你希望将标志作为正则表达式的一部分,而不是将标志参数传递给re.compile()函数,这很有用。标志应该在表达式字符串中首先使用。

(?aiLmsux-imsx:...)

[...] 这些字母为表达式的部分设置或删除相应的标志[...]。[...]

所以,你可以这样做。

>>> onetwo = """\
... (?x)
... 1 # look, ma
... 2 # no flags
... """
>>> re.findall(onetwo, '0123')
['12']

或者这样:

>>> onetwo = """\
... (?x:
...     1 # verbose until the close paren
... )2"""
>>> re.findall(onetwo, '0123')
['12']

现在就这样了。

今天学到了一些新东西?**和别人分享一下吧,这真的很有帮助!

奖励:我不使用Python

很多其他语言也支持内联verbose标志!你可以在任何一种语言中建立一个模式,并在任何其他语言中使用它。3语言,比如...

C(有PCRE--以及扩展到C++、PHP和其他许多语言)。

echo '0123' | pcregrep -o '(?x)
1 2  # such inline

是的,C语言的版本其实真的很长,点击展开。

char *pattern =
    "(?x)\n"
    "1 2  # much verbose\n"
;
char *subject = "0123";
int subject_length = strlen(subject);

int errornumber;
PCRE2_SIZE erroroffset;

pcre2_code *re = pcre2_compile(
    (PCRE2_SPTR)pattern,
    PCRE2_ZERO_TERMINATED,
    0,
    &errornumber,
    &erroroffset,
    NULL
);

pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(re, NULL);

pcre2_match(
    re,
    (PCRE2_SPTR)subject,
    subject_length,
    0,
    0,
    match_data,
    NULL
);

PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
PCRE2_SPTR substring_start = (PCRE2_SPTR)subject + ovector[0];
size_t substring_length = ovector[1] - ovector[0];
printf("%.*s\n", (int)substring_length, (char *)substring_start);

C#:

Console.WriteLine(new Regex(@"(?x)
1 2  # wow
").Match("0123"));

grep(只有GNU的那个):

echo '0123' | grep -Po '(?x) 1 2  # no line'

Java(以及扩展到很多JVM语言,如Scala):

var p = Pattern.compile(
    "(?x)\n" +
    "1 2  # much class\n"
);
var m = p.matcher("0123");
m.find();
System.out.println(m.group(0));

Perl:

"0123" =~ /(?x)( 1 2  # no scare )/; print $1 . "\n";

PostgreSQL。

select substring(   '0123' from     $$(?x)     1 2  # such declarative     $$ );

鲁比:

puts /(?x) 1 2  # nice /.match('0123')

Rust:

let re = Regex::new(     r"(?x)     1 2  # much safe     " ).unwrap(); println!("{}", re.find("0123").unwrap().as_str());

Swift:

let string = "0123" let range = string.range(     of : """     (?x)     1 2  # omg hi     """,     options : .regularExpression ) print(string[range!])

不支持开箱即用的内联粗体标记的著名语言。

  • C (regex.h - POSIX 正则表达式)
  • C++ (regex)
  • Go (regexp)
  • Javascript
  • Lua