关于正则表达式:为什么正则表达式如此具有争议性?

Why are regular expressions so controversial?

在研究正则表达式(也称为regex es)时,有许多人似乎将正则表达式视为圣杯。看起来很复杂的东西-必须是任何问题的答案。他们倾向于认为每个问题都可以用正则表达式来解决。

另一方面,也有许多人试图不惜一切代价避免正则表达式。他们试图找到一种绕过正则表达式的方法,并且为了这个目的接受额外的编码,即使正则表达式是一个更紧凑的解决方案。

为什么正则表达式被认为是有争议的?他们是如何工作的普遍误解吗?或者可以广泛地认为正则表达式通常是缓慢的吗?


我不认为人们反对正则表达式是因为它们速度慢,而是因为它们很难读写,而且很难纠正。虽然在某些情况下,正则表达式提供了一个有效的、紧凑的问题解决方案,但有时它们会被推到使用易于阅读、可维护的代码部分更好的情况下。


使正则表达式可维护

对以前称为"正则表达式"的模式进行去神秘化的主要进展是Perl的/xregex标志(有时在嵌入时写入(?x)),它允许空白(换行、缩进)和注释。这会严重提高可读性,从而提高可维护性。空白区域允许认知分块,所以你可以看到哪些组包含什么。

现代模式现在也支持相对编号和命名的后参照。这意味着您不再需要计算捕获组来确定您需要$4\7。这有助于创建可以包含在其他模式中的模式。

下面是一个相对编号的捕获组示例:

1
2
$dupword = qr{ \b (?: ( \w+ ) (?: \s+ \g{-1} )+ ) \b }xi;
$quoted  = qr{ ( ["'] ) $dupword  \1 }x;

下面是命名捕获的高级方法示例:

1
2
$dupword = qr{ \b (?: (?<word> \w+ ) (?: \s+ \k<word> )+ ) \b }xi;
$quoted  = qr{ (?<quote> ["'] ) $dupword  \g{quote} }x;

语法规则

最重要的是,这些命名捕获可以放在(?(DEFINE)...)块中,这样您就可以将声明与模式中单个命名元素的执行分离开来。这使得它们在模式中的行为更像子程序。这种"语法regex"的一个很好的例子可以在这个答案和这个答案中找到。这些看起来更像是语法声明。

正如后者提醒您的:

… make sure never to write line‐noise patterns. You don’t have to, and you shouldn’t. No programming language can be maintainable that forbids white space, comments, subroutines, or alphanumeric identifiers. So use all those things in your patterns.

这一点不能过分强调。当然,如果你不在你的模式中使用这些东西,你经常会制造一个噩梦。但是如果你真的使用它们,你就不需要了。

下面是另一个现代语法模式的例子,这一个用于解析RFC5322:使用5.1.0;

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
$rfc5322 = qr{

   (?(DEFINE)

     (?         (?&mailbox) | (?&group))
     (?<mailbox>         (?&name_addr) | (?&addr_spec))
     (?<name_addr>       (?&display_name)? (?&angle_addr))
     (?      (?&CFWS)? < (?&addr_spec) > (?&CFWS)?)
     (?<group>           (?&display_name) : (?:(?&mailbox_list) | (?&CFWS))? ; (?&CFWS)?)
     (?<display_name>    (?&phrase))
     (?<mailbox_list>    (?&mailbox) (?: , (?&mailbox))*)

     (?       (?&local_part) \@ (?&domain))
     (?<local_part>      (?&dot_atom) | (?&quoted_string))
     (?<domain>          (?&dot_atom) | (?&domain_literal))
     (?<domain_literal>  (?&CFWS)? \[ (?: (?&FWS)? (?&dcontent))* (?&FWS)?
                                   \] (?&CFWS)?)
     (?<dcontent>        (?&dtext) | (?&quoted_pair))
     (?<dtext>           (?&NO_WS_CTL) | [\x21-\x5a\x5e-\x7e])

     (?           (?&ALPHA) | (?&DIGIT) | [!#\$%&'*+-/=?^_`{|}~])
     (?            (?&CFWS)? (?&atext)+ (?&CFWS)?)
     (?<dot_atom>        (?&CFWS)? (?&dot_atom_text) (?&CFWS)?)
     (?<dot_atom_text>   (?&atext)+ (?: \. (?&atext)+)*)

     (?<text>            [\x01-\x09\x0b\x0c\x0e-\x7f])
     (?<quoted_pair>     \\ (?&text))

     (?<qtext>           (?&NO_WS_CTL) | [\x21\x23-\x5b\x5d-\x7e])
     (?<qcontent>        (?&qtext) | (?&quoted_pair))
     (?<quoted_string>   (?&CFWS)? (?&DQUOTE) (?:(?&FWS)? (?&qcontent))*
                          (?&FWS)? (?&DQUOTE) (?&CFWS)?)

     (?<word>            (?&atom) | (?&quoted_string))
     (?<phrase>          (?&word)+)

     # Folding white space
     (?<FWS>             (?: (?&WSP)* (?&CRLF))? (?&WSP)+)
     (?<ctext>           (?&NO_WS_CTL) | [\x21-\x27\x2a-\x5b\x5d-\x7e])
     (?<ccontent>        (?&ctext) | (?&quoted_pair) | (?&comment))
     (?<comment>         \( (?: (?&FWS)? (?&ccontent))* (?&FWS)? \) )
     (?<CFWS>            (?: (?&FWS)? (?&comment))*
                         (?: (?:(?&FWS)? (?&comment)) | (?&FWS)))

     # No whitespace control
     (?<NO_WS_CTL>       [\x01-\x08\x0b\x0c\x0e-\x1f\x7f])

     (?<ALPHA>           [A-Za-z])
     (?<DIGIT>           [0-9])
     (?<CRLF>            \x0d \x0a)
     (?<DQUOTE>         ")
     (?<WSP>             [\x20\x09])
   )

   (?&address)

}x;

这不是很了不起吗?您可以采用BNF风格的语法,并将其直接转换为代码,而不会丢失其基本结构!

如果现代语法模式还不足以满足您的需要,那么Damian Conway的出色的Regexp::Grammars模块提供了一个更为简洁的语法,同时也提供了出色的调试功能。下面是从该模块将RFC5322重新转换为模式的解析代码:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/perl

use strict;
use warnings;
use 5.010;
use Data::Dumper"Dumper";

my $rfc5322 = do {
    use Regexp::Grammars;    # ...the magic is lexically scoped
    qr{

    # Keep the big stick handy, just in case...
    # <debug:on>

    # Match this...
   

    # As defined by these...
    <token: address>         <mailbox> | <group>
    <token: mailbox>         <name_addr> |
    <token: name_addr>       <display_name>?
    <token: angle_addr>      <CFWS>? \<  \> <CFWS>?
    <token: group>           <display_name> : (?:<mailbox_list> | <CFWS>)? ; <CFWS>?
    <token: display_name>    <phrase>
    <token: mailbox_list>    <[mailbox]> ** (,)

    <token: addr_spec>       <local_part> \@ <domain>
    <token: local_part>      <dot_atom> | <quoted_string>
    <token: domain>          <dot_atom> | <domain_literal>
    <token: domain_literal>  <CFWS>? \[ (?: <FWS>? <[dcontent]>)* <FWS>?

    <token: dcontent>        <dtext> | <quoted_pair>
    <token: dtext>           <.NO_WS_CTL> | [\x21-\x5a\x5e-\x7e]

    <token: atext>           <.ALPHA> | <.DIGIT> | [!#\$%&'*+-/=?^_`{|}~]
    <token: atom>            <.CFWS>? <.atext>+ <.CFWS>?
    <token: dot_atom>        <.CFWS>? <.dot_atom_text> <.CFWS>?
    <token: dot_atom>        <.CFWS>? <.dot_atom_text> <.CFWS>?
    <token: dot_atom_text>   <.atext>+ (?: \. <.atext>+)*

    <token: text>            [\x01-\x09\x0b\x0c\x0e-\x7f]
    <token: quoted_pair>     \\ <.text>

    <token: qtext>           <.NO_WS_CTL> | [\x21\x23-\x5b\x5d-\x7e]
    <token: qcontent>        <.qtext> | <.quoted_pair>
    <token: quoted_string>   <.CFWS>? <.DQUOTE> (?:<.FWS>? <.qcontent>)*
                             <.FWS>? <.DQUOTE> <.CFWS>?

    <token: word>            <.atom> | <.quoted_string>
    <token: phrase>          <.word>+

    # Folding white space
    <token: FWS>             (?: <.WSP>* <.CRLF>)? <.WSP>+
    <token: ctext>           <.NO_WS_CTL> | [\x21-\x27\x2a-\x5b\x5d-\x7e]
    <token: ccontent>        <.ctext> | <.quoted_pair> | <.comment>
    <token: comment>         \( (?: <.FWS>? <.ccontent>)* <.FWS>? \)
    <token: CFWS>            (?: <.FWS>? <.comment>)*
                             (?: (?:<.FWS>? <.comment>) | <.FWS>)

    # No whitespace control
    <token: NO_WS_CTL>       [\x01-\x08\x0b\x0c\x0e-\x1f\x7f]

    <token: ALPHA>           [A-Za-z]
    <token: DIGIT>           [0-9]
    <token: CRLF>            \x0d \x0a
    <token: DQUOTE>         "
    <token: WSP>             [\x20\x09]

    }x;

};


while (my $input = <>) {
    if ($input =~ $rfc5322) {
        say Dumper \%/;       # ...the parse tree of any successful match
                              # appears in this punctuation variable
    }
}

Perlre手册页中有很多好东西,但是这些对regex基本设计特性的显著改进绝不仅仅局限于Perl。实际上,pcrepattern手册页可能更容易阅读,并且涵盖相同的领域。

现代模式与你在有限自动机课上所学的原始事物几乎没有任何共同之处。


正则表达式是一个很好的工具,但人们认为"嘿,多好的工具,我会用它来做X!"其中x是另一种工具更好的工具(通常是解析器)。在需要螺丝刀的地方使用锤子是标准的。


我认识的几乎所有定期使用正则表达式(pun-intended)的人都来自于一个类似Unix的背景,他们使用将res视为一流编程结构的工具,如grep、sed、awk和perl。由于使用正则表达式几乎没有语法开销,因此当使用正则表达式时,它们的生产率会大大提高。

相反,使用RES是外部库的语言的程序员往往不考虑正则表达式可以为表带来什么。程序员的"时间成本"太高了,要么a)RES从来没有作为他们培训的一部分出现,要么b)他们在RES方面不"思考",而宁愿回到更熟悉的模式上。


正则表达式允许您以紧凑的方式编写自定义有限状态机(FSM),以处理输入字符串。使用正则表达式困难的原因至少有两个:

  • 老派的软件开发涉及到很多计划、纸张模型和仔细的思考。正则表达式非常适合这个模型,因为正确地编写一个有效的表达式需要很多人盯着它,可视化FSM的路径。

    现代软件开发人员更愿意敲出代码,并使用调试器逐步执行,以查看代码是否正确。正则表达式不太支持这种工作方式。正则表达式的一个"运行"实际上是一个原子操作。很难在调试器中观察到逐步执行。

  • 编写一个不小心接受了比预期更多输入的正则表达式太容易了。正则表达式的值不是真正匹配有效输入,而是无法匹配无效输入。对正则表达式进行"负检验"的技术不是很先进,或者至少没有广泛使用。

    这就到了正则表达式难以阅读的地步。仅仅通过观察一个正则表达式,就需要花费大量的精力来可视化所有可能的输入,这些输入应该被拒绝,但却被错误地接受。试过调试别人的正则表达式代码吗?

如果现在软件开发人员抵制使用正则表达式,我认为主要是由于这两个因素。


人们往往认为正则表达式很难理解,但那是因为他们用错了。在没有任何注释、缩进或命名捕获的情况下编写复杂的一行程序。(您不会将复杂的SQL表达式塞进一行中,没有注释、缩进或别名,是吗?)是的,对很多人来说,他们没有意义。

但是,如果你的工作与解析文本(大致上是任何一个网络应用程序…)有任何关系,而且你不知道正则表达式,那么你的工作很差劲,而且你在浪费自己和雇主的时间。有很多优秀的资源可以教你所有你需要知道的东西,还有更多。


因为它们缺少最流行的IDES学习工具:没有regex向导。甚至不能自动完成。你必须自己编写整个代码。


"正则表达式:现在你有两个问题了"是杰夫·阿特伍德关于这个问题的一篇很好的文章。基本上,正则表达式是"硬的"!它们会产生新的问题。然而,它们是有效的。


我认为他们没有那么有争议。

我也认为你已经回答了你自己的问题,因为你指出在任何地方使用它们(并非所有东西都是常规语言2)或根本避免使用它们是多么愚蠢。作为程序员,您必须做出一个明智的决定,决定正则表达式何时会帮助代码或损害代码。当面对这样的决定时,需要牢记的两个重要事项是可维护性(这意味着可读性)和可扩展性。

对于那些特别讨厌它们的人,我的猜测是他们从未学会正确使用它们。我认为,大多数人只要花几个小时在一个体面的辅导上,就会明白他们的意思,并很快变得流利。以下是我关于从哪里开始的建议:

http://docs.python.org/howto/regex

尽管该页面在Python上下文中讨论正则表达式,但我发现该信息在其他地方非常适用。有一些特定于Python的东西,但我相信它们被清楚地指出,并且易于记忆。


正则表达式是字符串,算术运算符是数字,我不认为它们有争议。我认为,即使是像我这样一个毫不起眼的OO活动家(他倾向于选择其他对象而不是字符串),也很难拒绝他们。


问题是,正则表达式的潜在功能非常强大,您可以使用它们进行操作,因此您应该使用不同的方法。

一个好的程序员应该知道在哪里使用它们,在哪里不使用它们。典型的示例是解析非常规语言(请参见确定语言是否为常规语言)。

我认为,如果一开始把自己局限于真正的正则表达式(没有扩展),就不会出错。有些扩展可以让您的生活更轻松,但是如果您发现一些难以表达为真正的regex的东西,这很可能表明regex不是正确的工具。


你可能也会问为什么Goto会有争议。

基本上,当你获得如此"明显"的权力时,人们往往会因为他们不是最佳选择的情况而滥用他们。例如,请求用regex解析csv、xml或html的人数让我吃惊。这是做这项工作的错误工具。但有些用户还是坚持使用regex。

就我个人而言,我试着找到一种快乐的、中等程度的正则表达式,并在它们不太理想时避免使用它们。

请注意,regex仍然可以用于解析csv、xml、html等,但通常不在单个regex中。


我不认为"有争议"这个词是对的。

但是我看到过很多这样的例子,人们说"我需要做什么样的正则表达式,这样的字符串操作?"这是X-Y问题。

换言之,它们是从一个regex是它们所需要的假设开始的,但是它们最好使用split(),类似perl的tr///这样的翻译,其中一个字符替换另一个字符,或者只使用index()。


这是一个有趣的主题。
许多regexp爱好者似乎把公式的简洁性与效率混淆了。
除此之外,一个需要大量思考的regexp会给作者带来巨大的满足感,使其直接成为合法的。

但是…例如,当性能不是一个问题,并且您需要在Perl中快速处理文本输出时,regexps非常方便。此外,虽然性能是一个问题,但人们可能不喜欢尝试使用自制的算法来击败regexp库,这种算法可能有问题,或者效率较低。

此外,有许多原因导致了regexp受到不公平的批评,例如

  • regexp是不高效的,因为构建顶部的regexp并不明显
  • 有些程序员"忘记"只编译一次ReEXP多次使用(就像爪哇中的静态模式)。
  • 有些程序员采用试用和错误策略-使用regexps的效果更差!


我认为学习regex和保持regex在不受欢迎的情况下,大多数开发人员都很懒惰,或者他们中的大多数依靠外部库来为他们做解析工作…他们依靠谷歌来回答问题,甚至在论坛上要求他们提供问题的完整代码。但是,当涉及到实现或修改/维护一个regex时,它们只会失败。

有句流行语"朋友不允许朋友使用regex解析HTML"

但就我所知,我已经使用regex制作了完整的HTML解析器,我发现regex在速度和内存方面都更擅长解析HTML字符串(如果你知道要实现什么)。


正则表达式对很多人来说都是一个严重的谜,包括我自己。它很好用,但就像看一个数学方程式。尽管有人最终在http://regexlib.com/上创建了各种正则表达式函数的统一位置,但我还是很高兴地报告说。现在,如果微软只创建一个正则表达式类,它将自动执行许多常见的操作,比如删除字母或过滤日期。


我发现正则表达式有时非常宝贵。当我需要做一些"模糊"的搜索时,也许可以替换。当数据可能发生变化并具有一定的随机性时。但是,当我需要执行简单的搜索和替换,或者检查字符串时,我不使用正则表达式。虽然我认识很多人,但他们什么都用它。这就是争议所在。

如果你想在墙上钉一个钉子,不要用锤子。是的,它可以用,但是当你拿到锤子时,我可以在墙上钉20个钉子。

正则表达式应该用于它们的设计目的,而不应少于此。


在某些情况下,我认为你必须使用它们。例如,构建一个lexer。

在我看来,这是一个能写regexp的人和不写regexp的人的观点。我个人认为这是一个很好的想法,例如,为了有效地输入表单,无论是在javascript中警告用户,还是在服务器端语言中。


我认为这是程序员中不太知名的技术。因此,人们对它的接受度并不高。如果你有一个非技术性的经理来审查你的代码或者审查你的工作,那么一个正则表达式是非常糟糕的。您将花费数小时来编写一个完美的正则表达式,并且您会认为他/她编写了这么少的代码行,而对于该模块来说,您将获得很少的分数。另外,正如其他地方所说,阅读正则表达式是非常困难的任务。


像lex和yacc中用于编译器定义的正规表达式系统是好的,非常有用和干净的。在这些系统中,表达式类型是根据其他类型定义的。在Perl和Sed代码(等等)中常见的可怕的、畸形的、不可读的行噪声巨大的单行正则表达式是"有争议的"(垃圾)。


虽然我认为正则表达式是一个必不可少的工具,但它们最恼人的地方在于有不同的实现。语法、修饰符上的细微差别,尤其是"贪婪"会使事情变得非常混乱,需要反复尝试,有时还会产生令人费解的错误。


regex的最佳有效和正常用法是电子邮件地址格式验证。

这是一个很好的应用。

我在textpad中多次使用正则表达式作为一次性工具来按摩平面文件、创建csv文件、创建SQL插入语句等等。

写得好的正则表达式不应该太慢。通常情况下,替代方案,如大量的呼叫替换,都是速度慢得多的选项。不妨一次完成。

许多情况下都需要精确的正则表达式,而不需要其他任何表达式。

用无害的字符替换特殊的非打印字符是另一个很好的用法。

当然,我可以想象有一些代码基过度使用正则表达式而损害了可维护性。我自己从未见过。实际上,代码审查人员一直在回避我没有使用足够的正则表达式。