正则表达式真的可以维护吗?

时间:2020-03-06 14:53:53  来源:igfitidea点击:

我见过的任何使用Regexes的代码都倾向于将它们用作黑匣子:

  • 放在字符串中
  • 魔术正则表达式
  • 拿出绳子

在生产代码中使用这似乎不是一个好主意,因为即使很小的更改也常常会导致完全不同的正则表达式。

除了标准是永久不变的情况之外,正则表达式是做事的方式还是尝试其他方法更好?

解决方案

必须的。

真正归结为正则表达式。如果这是一个巨大的整体表达式,那么是的,这是一个可维护性问题。如果我们可以简洁地表达它们(也许通过分解来表达它们),或者我们有很好的注释和工具来理解它们,那么它们可以是一个强大的工具。

正则表达式不是唯一做某事的方法。我们可以在逻辑上以代码方式执行正则表达式可以执行的所有操作。正则表达式只是

  • 快速地
  • 经过测试和证明
  • 强大的

复杂的正则表达式对我来说是一劳永逸的。编写它,对其进行测试,并在其起作用时,对它的作用发表评论,我们还不错。

但是,在许多情况下,我们可以将正则表达式分解为较小的部分,或者编写一些结合这些正则表达式的有据可查的代码。但是,如果在代码中找到多行正则表达式,则最好不要成为必须维护它的人:)

听起来很熟悉?任何代码都差不多。我们不想要很长的方法,不想拥有很长的类,也不想拥有很长的正则表达式,尽管到目前为止,方法和类的重构更容易。但从本质上讲,这是相同的概念。

关于正则表达式的名言:

"Some people, when confronted with a problem, think 
  “I know, I'll use regular expressions.”   Now they have two problems." --  Jamie Zawinski

当我确实使用正则表达式时,我发现它们是可维护的,但是在特殊情况下会使用它们。通常,有一种更好的,非正则表达式的方法可以执行几乎所有操作。

如果我们不了解正则表达式,这似乎就像魔术。生产代码中的许多微小更改都可能导致重大问题,因此,我认为这不是使用正则表达式的充分理由。全面的测试应指出任何问题。

当有意识地使用正则表达式时,正则表达式是一种强大的机制,可以使我们免于可能进行文本解析的行和行。当然,应该正确,有效地跟踪这些记录,以验证初始假设是否仍然有效,并相应地对其进行更新。关于维护,IMHO最好更改一行代码(正则表达式模式),而不是理解解析代码的行和正则表达式的目的。

如果正则表达式较长且难以理解,则使其难以维护,则应对其进行注释。

许多正则表达式实现使我们可以用空格和注释填充正则表达式。
参见http://www.regular-expressions.info/comments.html
和编码恐怖:正则表达式:现在我们有两个问题

Any code I've seen that uses Regexes tends to use them as a black box:

如果用黑匣子表示抽象,那就是所有编程的目的,就是尝试抽象出困难的部分(解析字符串),以便我们可以专注于问题域(我想匹配哪种字符串)。

even a small change can often result in a completely different regex.

任何代码都是如此。只要我们正在测试正则表达式以确保它与我们期望的字符串匹配(理想情况下与单元测试匹配),那么我们就应该对更改它们充满信心。

编辑:也请阅读Jeff对这个有关生产代码的答案的评论。

对任何语言的任何代码进行小的更改都会导致完全不同的结果。其中一些甚至阻止编译。

用" C"或者" C#"或者" Java"或者" Python"或者" Perl"或者" SQL"或者" Ruby"或者" awk"或者任何东西代替正则表达式,实际上,我们会遇到相同的问题。

正则表达式只是另一种语言,霍夫曼编码为字符串匹配有效。就像Java,Perl,PHP或者特别是SQL一样,每种语言都有其优点和缺点,并且在编写(或者维护)它时,我们需要了解所使用的语言,以期提高工作效率。

编辑:迈克,正则表达式是霍夫曼编码的,因为要做的普通事比稀有事短。文字的文字匹配通常是单个字符(我们要匹配的字符)。存在特殊字符,常见字符较短。特殊构造(例如(?:))更长。这些与Perl,C ++等通用语言中不常见的东西相同,因此Huffman编码针对的是这一专业。

我在应用程序中使用了它们,但是我将实际的regEx表达式保留在配置文件中,因此,如果我正在解析的源文本(例如电子邮件)由于某种原因更改了格式,我可以快速更新配置以处理更改,而无需重新构建应用程序。

我不知道我们使用的是哪种语言,但是例如Perl支持x标志,因此除非在转义中忽略空格,否则除非将其转义,否则我们可以将其分成几行并在行中注释所有内容:

$foo =~ m{
    (some-thing)          # matches something
    \s*                   # matches any amount of spaces
    (match another thing) # matches something else
}x;

这有助于使长的正则表达式更具可读性。

确实,正则表达式已被称为"只写"编程语言。但是,我认为这并不意味着我们应该避免使用它们。我只是认为我们应该出于他们的意图发表评论。我通常不喜欢解释行的注释,我可以阅读代码,但是Regexs是个例外。评论一切!

正则表达式是做事的方式吗?这取决于任务。

与所有编程一样,没有一成不变的正确或者错误的答案。

如果正则表达式可以快速简单地解决特定任务,那么它可能比更冗长的解决方案更好。

如果正则表达式正试图完成一项复杂的任务,那么更冗长的内容可能更易于理解和维护。

我有一个彻底评论非平凡正则表达式的政策。这意味着要描述和证明每个不匹配的原子。有些语言(例如Python)提供了"冗长的"正则表达式,这些正则表达式忽略空白并允许注释。尽可能使用它。否则,请在正则表达式上方的注释中逐个原子。

我通常会写一个扫描仪规范文件。扫描仪或者"扫描仪生成器"实质上是一种优化的文本解析器。由于我通常使用Java,因此我的首选方法是JFlex(http://www.jflex.de),但是还有Lex,YACC和其他几种方法。

扫描程序处理可以定义为宏的正则表达式。然后,当正则表达式匹配文本的一部分时,我们将实现回调。

当涉及到代码时,我有一个包含所有解析逻辑的规范文件。我通过选择的扫描仪生成器工具运行它,以使用选择的语言生成源代码。然后,我将所有内容包装到某种解析器函数或者类中。然后,这种抽象使管理所有正则表达式逻辑变得容易,并且性能非常好。当然,如果我们只使用一两个正则表达式,那就太过分了,很容易花费至少2-3天的时间来了解到底发生了什么,但是如果我们曾经使用过5或者6或者30,它们之中,它成为一个非常好的功能,并且实现解析逻辑的过程仅需花费几分钟,因此它们易于维护且易于记录。

问题不在于正则表达式本身,而在于它们被视为黑匣子。与任何编程语言一样,可维护性更多地与编写语言的人和阅读它的人有关,而不是与语言本身有关。

使用正确的工具完成工作还有很多要说的。在我们对原始帖子的评论中提到的示例中,正则表达式是用于解析HTML的错误工具,就像在PerlMonks上经常提到的那样。如果我们尝试仅使用正则表达式以类似于一般方式的任何方式来解析HTML,那么我们最终将以一种不正确且脆弱的方式进行操作,编写令人难以置信的正则表达式怪异,或者(很可能)两个都。

有很多可能性可以使RegEx更具可维护性。最后,这只是(好?)程序员必须学习的有关重大(有时甚至是次要)更改的技术。在没有真正优秀的专业人士的情况下,没有人会因为它们复杂的语法而为他们烦恼。但是他们工作迅速,紧凑且非常灵活。

对于.NET People,可能会出现外观较差的" Linq to RegEx"库或者"可读正则表达式库"。它使它们更易于维护,但更易于编写。我在自己的项目中都使用了它们,因为我知道与它们一起分析的html源代码可能随时更改。

但是请相信我:当我们着迷于它们时,它们甚至可以使写作和阅读变得有趣。 :)

我一直将这个问题作为构建模块问题来对待。

我们不仅要编写约3000个字符的正则表达式,而且希望获得最好的结果。我们编写了一堆加在一起的小块。

例如,要匹配URI,我们必须具有协议,权限,子域,域,tld,路径,参数(至少)。其中一些是可选的!

我敢肯定,我们可以编写一个怪物来处理它,但是编写块并将它们添加在一起会更容易。

如果我们使用Perl 5.10引入的新功能,则RegEx可以非常容易维护。我所指的功能是Perl 6中的反向移植功能。

直接从perlretut复制的示例。

定义命名模式

一些正则表达式在多个地方使用相同的子模式。从Perl 5.10开始,可以在模式的一部分中定义命名子模式,以便可以在模式中的任何位置通过名称来调用它们。这个定义组的语法模式是(?(DEFINE)(?<name> pattern)...)。命名模式的插入被写为(?&name)`。

下面的示例使用前面介绍的浮点数模式来说明此功能。多次使用的三个子模式是可选符号,整数的数字序列和小数部分。模式末尾的" DEFINE"组包含其定义。注意,十进制分数模式是我们可以重用整数模式的第一位。

/^
  (?&osg)\ * ( (?&int)(?&dec)? | (?&dec) )
        (?: [eE](?&osg)(?&int) )?
 $
 (?(DEFINE)
     (?<osg>[-+]?)         # optional sign
     (?<int>\d++)          # integer
     (?<dec>\.(?&int))     # decimal fraction
 )
/x

我通常将正则表达式分解成带注释的部分,然后将它们放在一起进行最后的推送。片段可以是子字符串或者数组元素

两个PHP PCRE示例(具体细节或者特定用途并不重要):

1)
  $dktpat = '/^[^a-z0-9]*'. // skip any initial non-digits
    '([a-z0-9]:)?'. // division within the district
    '(\d+)'. // year
    '((-)|-?([a-z][a-z])-?)'. // type of court if any - cv, bk, etc.
    '(\d+)'. // docket sequence number
    '[^0-9]*$/i'; // ignore anything after the sequence number
  if (preg_match($dktpat,$DocketID,$m)) {

2)
    $pat= array (
      'Row'        => '\s*(\d*)',
      'Parties'    => '(.*)',
      'CourtID'    => '<a[^>]*>([a-z]*)</a>',
      'CaseNo'     => '<a[^>]*>([a-z0-9:\-]*)</a>',
      'FirstFiled' => '([0-9\/]*)',
      'NOS'        => '(\d*)',
      'CaseClosed' => '([0-9\/]*)',
      'CaseTitle'  => '(.*)',
    );
    // wrap terms in table syntax
    $pat = '#<tr>(<td[^>]*>'.
      implode('</td>)(</tr><tr>)?(<td[^>]*>',$pat).
      '</td>)</tr>#iUx';
    if (preg_match_all ($pat,$this->DocketText,$matches, PREG_PATTERN_ORDER))

问题似乎与正则表达式本身无关,而仅涉及通常用于表达正则表达式的语法。在许多硬核编码器中,这种语法已经被认为是非常简洁和强大的,但是对于更长的正则表达式来说,它实际上是不可读和不可维护的。

有人已经在Perl中提到了x标志,这虽然有帮助,但作用不大。

我非常喜欢正则表达式,但不喜欢语法。能够从可读的,有意义的方法名称中构造一个正则表达式会很好。例如,代替此Ccode:

foreach (var match in Regex.Matches(input, @"-?(?<number>\d+)"))
{
    Console.WriteLine(match.Groups["number"].Value);
}

我们可能会有更多冗长但更易读和可维护的内容:

int number = 0;
Regex r = Regex.Char('-').Optional().Then(
    Regex.Digit().OneOrMore().Capture(c => number = int.Parse(c))
);
foreach (var match in r.Matches(input))
{
    Console.WriteLine(number);
}

这只是一个简单的想法。我知道与此相关的还有其他不相关的可维护性问题(尽管我认为它们越来越少了)。这样做的另一个好处是编译时验证。

当然,如果我们认为这太过头了并且太冗长,我们仍然可以使用介于两者之间的正则表达式语法。

instead of:   -?(?<number>\d+)
could have:   ("-" or "") + (number = digit * [1..])

这仍然是可读性的一百万倍,并且只有两倍长。可以很容易地使这种语法具有与普通正则表达式相同的表达能力,并且可以肯定地将其集成到用于静态分析的编程语言编译器中。

我真的不知道为什么即使重新考虑整个编程语言时(例如Perl 6或者Cwa是新的),对于重新思考正则表达式的语法仍然会有如此多的反对。此外,上述非常冗长的想法甚至与旧的正则表达式也不兼容;该API可以很容易地实现为在幕后构造老式正则表达式的API。