可读的正则表达式又不会失去其功能?


77

如今,许多程序员都知道快速编写正则表达式的乐趣,这些天通常是在某些Web服务的帮助下,或更传统的是在交互式提示下,或者编写一个正在开发正则表达式的小脚本以及一系列测试用例。 。无论哪种情况,该过程都是迭代的并且相当快:不断破解看起来像神秘的字符串,直到它匹配并捕获您想要的内容,然后拒绝您不需要的内容。

对于一个简单的情况,结果可能是这样的,例如Java regexp:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

许多程序员还知道需要编辑正则表达式或仅在遗留代码库中围绕正则表达式进行编码的痛苦。经过一些编辑后,对于熟悉regexp的任何人来说,regexp仍然很容易理解,regexp的资深人士应立即查看其功能(如果有人想要练习,请在帖子末尾回答自己弄清楚)。

但是,将正则表达式变成真正的只写对象并不需要变得更加复杂,即使有了勤奋的文档(每个人当然会对他们编写的所有复杂正则表达式都执行此操作……),修改正则表达式就变成了艰巨的任务。如果未对regexp进行仔细的单元测试(这当然也是每个人对其所有复杂的regexp的肯定和否定都有全面的单元测试...),这也可能是非常危险的任务。

因此,长话短说,是否存在不损失正则表达式的读写解决方案/替代方案?上面的正则表达式在替代方法下的外观如何?任何语言都可以,尽管最好使用多语言解决方案,但正则表达式是多语言的。


然后,较早的regexp所做的就是这样:解析格式为数字的字符串1:2:3.4,捕获每个数字,其中允许空格,并且只3需要空格。


2
SO上的相关内容: stackoverflow.com/a/143636/674039
2013年

24
如果您知道正则表达式应该捕获的内容,那么实际上读/编辑正则表达式就很简单。您可能听说过大多数语言称为“注释”的很少使用的功能。如果您不把它放在复杂的正则表达式之上,而不用解释它是做什么的,那您以后就要付出代价。另外,代码审查。
TC1 2013年

2
有两种选择可以清理它,而不必实际将其分解成小块。它们的存在与否因语言而异。(1)扩展行正则表达式,其中忽略正则表达式中的空格(除非进行转义),并添加了单行注释形式,因此您可以将其分解为具有缩进,行间距和注释的逻辑块。(2)命名捕获组,您可以在其中为每个括号命名,既可以添加一些自文档,又可以自动填充匹配的哈希值-比数字匹配的数组或$ N变量要好得多。
李·李

3
问题的一部分是正则表达式语言本身,以及其设计中糟糕的历史选择,这些选择像行李一样拖来拖去。用理智的语言,将括号分组纯粹是用来构造解析树的语法设备。但是在回溯到Unix的正则表达式实现中,它们具有语义:将寄存器绑定到子表达式匹配项。因此,您需要一些更复杂,丑陋的括号才能实现纯分组!
卡兹(Kaz)

2
这并不是一个实际的答案,但是提及正则表达式的功能与有限自动机的功能完全一样可能会很有用。也就是说,正则表达式可以验证/解析由有限自动机验证和解析的同一类字符串。因此,人类可读的正则表达式表示可能应该能够快速构建图形,并且我相信大多数基于文本的语言都确实不好用。这就是为什么我们将视觉工具用于此类事物。请访问hackingoff.com/compilers/regular-expression-to-nfa-dfa以获取一些启发。
damix911

Answers:


80

许多人提到了由较小部分组成的构图,但还没有人提供示例,所以这是我的:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

不是最易读的内容,但我觉得它比原始内容更清晰。

此外,C#具有@可以表明它是按字面(无转义字符)被预先考虑到的字符串操作,所以number@"([\d]+)";


刚才注意到了两者[\\d]+[0-9]+应该是怎样的\\d+(嗯,有些可能会[0-9]+变得更具可读性)。我不会编辑问题,但您可能需要解决此问题。
海德

@hyde-好收获。从技术上讲,它们不是完全相同的东西- \d会匹配被认为是数字的任何内容,即使在其他编号系统(中文,阿拉伯文等)中,[0-9]也只会匹配标准数字。\\d不过,我确实对进行了标准化,并将其纳入optionalDecimal模式中。
Bobson

42

记录正则表达式的关键是对其进行记录。人们常常会把似乎是线路噪音的东西扔掉,而留在那儿。

perl/x中,正则表达式末尾的运算符会抑制空格,从而允许人们记录正则表达式。

上面的正则表达式将变为:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

是的,它会占用一些垂直空白,尽管可以在不牺牲太多可读性的情况下将其缩短。

然后,较早的regexp会执行以下操作:解析格式为1:2:3.4的数字字符串,捕获每个数字,其中允许有空格,只需要3个即可。

查看此正则表达式,您可以看到它是如何工作的(并且不工作)。在这种情况下,此正则表达式将匹配字符串1

可以用其他语言采取类似的方法。python re.VERBOSE选项在那里工作。

Perl6(上面的示例是针对perl5的)以规则的概念进一步完善了这一点,它带来了比PCRE更强大的结构(它提供对其他语法(无上下文和上下文敏感)的访问,而不仅仅是常规和扩展常规的语法)。

在Java(此示例来自该示例)中,可以使用字符串串联来形成正则表达式。

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

诚然,这会"在字符串中创建更多内容,可能导致字符串混乱,更易于阅读(尤其是在大多数IDE上使用语法突出显示)并进行记录。

关键是要认识到正则表达式经常具有的功能和“一次编写”的特性。编写代码以防患于未然,从而使正则表达式保持清晰易懂是关键。为了清晰起见,我们对Java代码进行了格式化-当语言为您提供了相应的选择时,正则表达式没有什么不同。


13
“记录”和“添加换行符”之间有很大的区别。

4
@JonofAllTrades使代码能够被读取是任何事情的第一步。添加换行符还可以使人们在同一行上为RE的该子集添加注释(这在正则表达式文本的单个长行上很难完成)。

2
@JonofAllTrades,我非常不同意。“文档”和“添加换行符”并没有什么不同,因为它们都具有相同的目的-使代码更易于理解。对于格式较差的代码,“添加换行符”比添加文档要好得多。
李·李

2
添加换行符只是一个开始,但这大约是工作的10%。其他答案提供了更多细节,这很有帮助。

26

某些语言和库提供的“详细”模式是解决这些问题的方法之一。在这种模式下,正则表达式字符串中的空格被去除(因此您需要使用\s),并且可以进行注释。这是Python中的一个简短示例,默认情况下支持此功能:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

在没有这种语言的情况下,实现从冗长到“正常”模式的翻译器应该是一个简单的任务。如果您担心正则表达式的可读性,则可以很轻松地证明这次投资的合理性。


15

每种使用正则表达式的语言都允许您从更简单的块中编写它们,以使阅读更轻松,并且在比示例更复杂的情况下,您一定应该利用该选项。Java和许多其他语言的特别麻烦在于,它们不将正则表达式视为“一流”公民,而是要求它们通过字符串文字潜入语言中。这意味着许多引号和反斜杠实际上不是正则表达式语法的一部分,并且使内容难以阅读,并且这意味着在没有有效定义自己的迷你语言和解释器的情况下,您将无法获得比这更多的可读性。

集成正则表达式的原型更好的方法当然是Perl,它带有空格选项和正则表达式引用运算符。Perl 6将构建正则表达式的概念从各个部分扩展到了实际的递归语法,使用起来要好得多,实际上完全没有可比性。该语言可能错过了及时性,但是它的正则表达式支持是The Good Stuff(tm)。


1
通过答案开头提到的“简单块”,您是说仅仅是字符串连接,还是更高级的东西?
海德

7
我的意思是将子表达式定义为较短的字符串文字,将它们分配给具有有意义名称的局部变量,然后进行串联。我发现名称对可读性比布局改进更重要。
Kilian Foth,

11

我喜欢使用Expresso:http://www.ultrapico.com/Expresso.htm

这个免费的应用程序具有以下功能,随着时间的推移,我发现它们很有用:

  • 您只需复制并粘贴您的正则表达式,应用程序就会为您解析
  • 编写完正则表达式后,您可以直接从应用程序对其进行测试(该应用程序将为您提供捕获,替换列表...)
  • 一旦测试完,它将生成C#代码来实现它(请注意,该代码将包含有关正则表达式的说明)。

例如,使用您刚提交的正则表达式,它看起来像: 带有初始给定正则表达式的示例屏幕

当然,尝试一下值得一千个单词来形容它。另请注意,我与该应用程序的编辑器有任何关系。


4
您介意对此进行更详细的解释-它如何以及为什么回答所提出的问题?在Stack Exchange上不太欢迎“仅链接的答案”
gnat 2013年

5
@gnat抱歉。你是绝对正确的。我希望编辑后的答案确实能提供更多见解。
E. Jaep 2013年

9

对于某些事情,仅使用像BNF这样的语法可能会有所帮助。这些可能比正则表达式更容易阅读。然后,诸如GoldParser Builder之类的工具可以将语法转换为解析器,从而为您完成繁重的工作。

BNF,EBNF等语法比复杂的正则表达式更易于阅读和制作。黄金是用于此类事情的一种工具。

下面的c2 Wiki链接列出了可以搜索的可能替代品列表,其中包括一些讨论。从根本上来说,这是一个“请参阅”链接,以补充我的语法引擎建议:

正则表达式的替代

将“替代”表示为“具有不同语法的语义上等效的工具”,RegularExpressions至少有以下替代方法:

  • 基本正则表达式
  • “扩展”正则表达式
  • Perl兼容的正则表达式
  • ...以及许多其他变体...
  • SNOBOL样式的RE语法(SnobolLanguage,IconLanguage)
  • SRE语法(RE作为EssExpressions)
  • 不同的FSM句柄
  • 有限状态交集语法(相当有表现力)
  • ParsingExpressionGrammars,如OMetaLanguage和LuaLanguage(http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html
  • RebolLanguage的解析模式
  • 基于概率的解析...

您介意进一步解释此链接的作用以及它的优点吗?在Stack Exchange上不太欢迎“仅链接的答案”
gnat 2013年

1
欢迎使用程序员NickP。请忽略downvote / r,但请阅读@gnat链接到的meta页面。
Christoffer Lette

@ Christoffer Lette感谢您的答复。在以后的帖子中会尽量记住这一点。@ gnat Paulo Scardine的评论反映了我帖子的意图。BNF,EBNF等语法比复杂的正则表达式更易于阅读和制作。黄金是用于此类事情的一种工具。c2链接列出了可以搜索的可能替代品列表,其中包括一些讨论。从根本上来说,这是一个“请参阅”链接,以补充我的语法引擎建议。
Nick P

6

这是一个古老的问题,我没有提到口头表达,因此我想在此也将这些信息添加给将来的求职者。言语表达是专门为使正则表达式易于理解而设计的,而无需学习正则表达式的符号含义。请参见以下示例。我认为这最符合您的要求。

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

这个例子是针对javascript的,您现在可以在许多编程语言中找到该库


2
这太棒了!
杰里米·汤普森


3

我认为这将会是值得一提的logstash的神交表达式。Grok建立在从较短的表达式构成长解析表达式的思想上。它可以方便地测试这些构建基块,并预先包装了100多种常用模式。除了这些模式,它还允许使用所有正则表达式语法。

上面用grok表示的模式是(我在调试器应用中进行了测试,但可能会犯错误):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

可选的部分和空间使它看起来比平常更难看,但是在这里和其他情况下,使用grok可以使人的生活变得更好。


2

在F#中,您具有FsVerbalExpressions模块。它允许您从口头表达来组成正则表达式,还具有一些预先构建的正则表达式(例如URL)。

以下是该语法的示例之一:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

如果您不熟悉F#语法,则groupName是字符串“ GroupNumber”。

然后,他们创建一个口头表达(VerbEx),并将其构造为“ COD(?<GroupNumber> [0-9] {3})END”。然后,他们在字符串“ COD123END”上进行测试,并在其中获得命名的捕获组“ GroupNumber”。结果为123。

老实说,我很容易理解普通的正则表达式。


-2

首先,了解仅起作用的代码是错误的代码。好的代码还需要准确报告遇到的任何错误。

例如,如果您正在编写一个将现金从一个用户的帐户转移到另一用户的帐户的功能;您不会只返回“工作或失败”的布尔值,因为这不会给调用者带来任何错误信息,也不允许调用者正确地通知用户。相反,您可能会有一组错误代码(或一组异常):找不到目标帐户,源帐户中的资金不足,权限被拒绝,无法连接到数据库,负载过多(请稍后重试)等。

现在考虑您的“以1:2:3.4格式解析数字字符串”的示例。regex所做的只是报告“通过/失败”,该“通过/失败”不允许向用户提供足够的反馈(无论此反馈是日志中的错误消息,还是交互式GUI,其中错误均以红色显示为用户类型或其他类型)。无法正确描述哪些类型的错误?第一个数字中的错误字符,第一个数字太大,第一个数字后缺少冒号等。

要将“仅能正常工作的错误代码”转换为“提供足够描述性错误的良好代码”,您必须将正则表达式分解为许多较小的正则表达式(通常,正则表达式非常小,因此首先不需要正则表达式就更容易做到) )。

使代码可读/可维护只是使代码良好的偶然结果。


6
可能不是一个很好的假设。我的原因是:A)这没有解决问题(如何使其可读?),B)正则表达式匹配通过/失败,并且如果将其分解到可以确切地说出失败原因的程度,会失去很多功能和速度,并增加复杂性,C)问题没有迹象表明甚至有匹配失败的可能-这仅仅是使Regex可读的问题。当您事先控制要输入的数据和/或对其进行验证时,可以认为它是有效的。
Bobson

A)将其分成较小的部分使其更具可读性(作为改进的结果)。C)在未知/未经验证的字符串输入某个软件的地方,有理智的开发人员会在此时解析(带有错误报告)并将数据转换为不需要重新解析的形式-之后不再需要正则表达式。B)是胡说八道,仅适用于错误代码(请参阅A和C点)。
布伦丹2013年

从您的C来:如果这他的验证逻辑怎么办?OP的代码可能恰好是您所建议的-验证输入,报告输入是否无效,然后将其转换为可用形式(通过捕获)。我们所拥有的只是表达本身。除了使用正则表达式外,您如何建议解析?如果您添加一些示例代码来实现相同的结果,那么我将删除我的下注。
Bobson

如果这是“ C:验证(带有错误报告)”,则它是错误的代码,因为错误报告是错误的。如果失败;是因为字符串为NULL,还是因为第一个数字位数过多,或者因为第一个分隔符不是:?想象一下,只有一个错误消息(“ ERROR”)的编译器太愚蠢,无法告诉用户问题出在哪里。现在,想象成千上万个同样愚蠢并显示(例如)“电子邮件地址错误”的网站,仅此而已。
布伦丹

另外,想象一个受过半培训的服务台操作员从一个完全未经培训的用户那里获得错误报告,该错误报告说:软件停止工作-软件日志中的最后一行是“错误:无法从版本字符串'1:2-3.4中提取次要版本号” '(第二个数字之后的预期冒号)”
布伦丹
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.