正则表达式语法设计的可读性差是否有特定原因?


160

程序员似乎都同意,代码的可读性比起短语法的单行代码要重要得多,但是单行代码可以工作,但是需要高级开发人员以任何程度的精度进行解释-但这似乎正是正则表达式设计的方式。有这个原因吗?

大家都同意,那selfDocumentingMethodName()比更好e()。为什么这也不适用于正则表达式?

在我看来,与其设计没有结构组织的单行逻辑语法,不如:

var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})(0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

而且这甚至不是URL的严格解析!

取而代之的是,我们可以使一些流水线结构井井有条,可读性强,这是一个基本示例:

string.regex
   .isRange('A-Z' || 'a-z')
   .followedBy('/r');

除了最短的运算和逻辑语法外,正则表达式的极为简洁的语法还有什么优势?归根结底,正则表达式语法设计的可读性差是否有特定的技术原因?


评论不作进一步讨论;此对话已转移至聊天
maple_shaft

1
我试图使用一个名为RegexToolbox的库来解决这个可读性问题。到目前为止,它已移植到C#,Java和JavaScript-请参阅github.com/markwhitaker/RegexToolbox.CSharp
Mark Whitaker

为解决此问题已进行了许多尝试,但是文化很难改变。在这里查看我有关口头表达的答案。人们寻求最低的通用工具。
帕里瓦·萨拉夫(Parivar Saraff)

Answers:


178

正则表达式设计得如此简洁有一个很大的原因:它们被设计为用作代码编辑器的命令,而不是用作进行编码的语言。更确切地说,这ed是最早使用正则表达式的程序之一,从那里开始,正则表达式开始征服世界统治。例如,该ed命令g/<regular expression>/p很快激发了一个名为的单独程序grep,该程序至今仍在使用。因为他们的权力,他们随后进行了规范,并在各种喜欢的工具使用sedvim

但是足够琐事。那么,为什么这个起源偏向于简洁的语法呢?因为您无需键入编辑器命令即可读取该命令一次。您可以记住如何将它们组合在一起,并且可以使用它想要做的事情就足够了。但是,您必须键入的每个字符都会减慢文件编辑的进度。正则表达式语法旨在以一种一次性的方式来编写相对复杂的搜索,而这正是使人们头疼的问题,这些人将它们用作代码来解析程序的某些输入。


5
正则表达式不是要解析。否则,请使用stackoverflow.com/questions/1732348/…。和头痛。
njzk2

19
@ njzk2这个答案实际上是错误的。一个HTML 文档不是一种普通语言,而是一个HTML open标签,实际上这是一个问题。
Random832

11
这是一个很好的答案,可以解释为什么原始正则表达式仍然如此神秘,但是不能解释为什么当前没有替代标准可以提高可读性。
布朗

13
因此,对于那些认为grep是错误的“抓斗”的人来说,它实际上来自g/ re(对于正则表达式)/ p
哈根·冯·埃岑

6
@DannyPflughoeft不,不是。一个开放标签只是<aaa bbb="ccc" ddd='eee'>,里面没有嵌套的标签。您不能嵌套标签,嵌套的是元素(打开标签,包括子元素的内容,关闭标签),而这些问题并不是问解析问题。HTML 标记是一种常规语言-平衡/嵌套发生在标记之上的级别。
Random832

62

您引用的正则表达式是一团糟,我认为没有人同意它的可读性。同时,很多丑陋是要解决的问题所固有的:有多层嵌套,并且URL语法相对复杂(肯定太复杂,无法用任何语言简洁地交流)。但是,确实确实存在更好的方法来描述此正则表达式所描述的内容。那么为什么不使用它们呢?

一个很大的原因是惯性和普遍存在。它并没有说明它们最初是如何变得如此流行的,但是现在,它们已经成为流行,任何知道正则表达式的人都可以使用一百种不同的语言和另外一千种软件工具来使用这些技能(方言之间的差异很小)(例如文本编辑器和命令行工具)。顺便说一下,后者不会也不会使用任何等于编写程序的解决方案,因为非程序员大量使用它们。

尽管如此,即使使用其他工具会更好,也经常会使用正则表达式,即使用正则表达式。我认为regex语法并不可怕。但这显然在短而简单的模式上要好得多:[a-zA-Z_][a-zA-Z0-9_]*可以用最少的正则表达式知识来阅读类似C的语言的标识符的原型示例,并且一旦达到该标准,它就显而易见并且非常简洁。需要更少的字符并不是天生的坏事,相反。只要您保持理解力,简洁就是一种美德。

至少有两个原因使该语法在此类简单模式上表现出色:它不需要对大多数字符进行转义,因此它读起来相对自然,并且它使用所有可用的标点来表示各种简单的解析组合器。也许最重要的是,它根本不需要任何东西进行测序。您写第一件事,然后再写。将此与您的做对比followedBy,尤其是当以下模式不是文字而是更复杂的表达式时。

那么,为什么它们在更复杂的情况下不如预期呢?我可以看到三个主要问题:

  1. 没有抽象功能。形式语法与正则表达式源于理论计算机科学的同一领域,它具有一系列生成形式,因此可以为模式的中间部分命名:

    # This is not equivalent to the regex in the question
    # It's just a mock-up of what a grammar could look like
    url      ::= protocol? '/'? '/'? '/'? (domain_part '.')+ tld
    protocol ::= letter+ ':'
    ...
    
  2. 正如我们在上面可以看到的,没有特殊意义的空格对于允许格式化更容易使眼睛有用。评论也一样。正则表达式无法做到这一点,因为空格就是文字' '。但是请注意:某些实现允许“冗长”的模式,其中空格被忽略,并且可以进行注释。

  3. 没有描述通用模式和组合器的元语言。例如,一个人可以编写一次digit规则,并在上下文无关的语法中继续使用它,但是一个人不能定义一个“函数”,也就是说,它被赋予了一个生产,p并创建了一个新生产,该生产对它做了更多的工作,例如create用逗号分隔的出现列表的产生p

您提出的方法肯定可以解决这些问题。它只是不能很好地解决它们,因为它的简洁程度远远超出了必要。可以解决前两个问题,同时保持在相对简单且简洁的领域特定语言中。当然,第三个……程序解决方案需要通用的编程语言,但是以我的经验,第三个是这些问题中最少的。很少有模式会发生程序员想要的能够定义新组合器的相同复杂任务。并且在必要时,该语言通常非常复杂,以致无论如何也不应使用正则表达式对其进行解析。

存在针对这些情况的解决方案。大约一万个解析器组合器库可以大致满足您的建议,只是使用一组不同的操作,通常使用不同的语法,并且几乎总是具有比正则表达式更大的解析能力(即,它们处理的是上下文无关的语言或某些可调整大小的语言)这些子集)。然后是解析器生成器,它们与上述“使用更好的DSL”方法一起使用。并且总是可以选择以适当的代码手动编写一些解析。您甚至可以混合匹配,使用正则表达式执行简单的子任务,并在调用正则表达式的代码中执行复杂的操作。

我对计算的早期知识了解不足,无法解释正则表达式如何变得如此流行。但是他们在这里留下来。您只需要明智地使用它们,而不是在更明智时使用它们。


9
I don't know enough about the early years of computing to explain how regular expressions came to be so popular.但是,我们可能会引起猜测:基本的正则表达式引擎非常易于实现,比高效的无上下文解析器容易得多。
biziclop 2015年

15
@biziclop我不会高估这个变量。Yacc显然有足够的前辈被称为“ 另一个编译器编译器”,它是在70年代初创建的,并被包含在Unix之前的版本grep中(版本3与版本4)。似乎正则表达式的第一个主要用途是在1968

我只能继续浏览我在Wikipedia上找到的内容(所以我不会100%相信它),但是据此,它yacc创建于1975年,是LALR解析器的全部构想(它们属于其实用的第一类解析器)起源于1973年。JIT编译expressions(!)的第一个regexp引擎实现发布于1968年。但是,您说对了,很难说出它到底是什么,实际上很难说正则表达式何时开始“采用”。关闭”。但是我怀疑一旦将它们放入开发人员使用的文本编辑器中,他们也想在自己的软件中使用它们。
biziclop 2015年

1
@ jpmc26打开他的书《正则表达式章节的JavaScript 精粹》。
Viziionary

2
with very few differences between dialects我不会说这是“很少”。任何预定义的字符类在不同的方言之间都有几个定义。此外,还有针对每种方言的解析怪癖。
nhahtdh 2015年

39

历史的角度

Wikipedia文章详细介绍了正则表达式的起源(Kleene,1956年)。原来的语法比较简单,只有*+?|和分组(...)。它是简洁的(而且可读性强,两者不一定是对立的),因为形式语言倾向于用简洁的数学符号表示。

后来,语法和功能随着编辑器的发展而发展,并随着Perl的发展而增长,Perl试图通过设计使其简洁(“通用结构应该简短”)。这使语法复杂化了很多,但是请注意,人们现在已经习惯了正则表达式,并且擅长编写(如果不阅读)它们。它们有时是只写的事实表明,当它们太长时,它们通常不是正确的工具。 正则表达式在被滥用时往往不可读。

超越基于字符串的正则表达式

在谈到替代语法,让我们来看看一个已经存在(CL-ppcre,在Common Lisp的)。您的长正则表达式可以ppcre:parse-string按如下方式进行解析:

(let ((*print-case* :downcase)
      (*print-right-margin* 50))
  (pprint
   (ppcre:parse-string "^(?:([A-Za-z]+):)?(\\/{0,3})(0-9.\\-A-Za-z]+)(?::(\\d+))?(?:\\/([^?#]*))?(?:\\?([^#]*))?(?:#(.*))?$")))

...,结果为以下形式:

(:sequence :start-anchor
 (:greedy-repetition 0 1
  (:group
   (:sequence
    (:register
     (:greedy-repetition 1 nil
      (:char-class (:range #\A #\Z)
       (:range #\a #\z))))
    #\:)))
 (:register (:greedy-repetition 0 3 #\/))
 (:register
  (:sequence "0-9" :everything "-A-Za-z"
   (:greedy-repetition 1 nil #\])))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\:
    (:register
     (:greedy-repetition 1 nil :digit-class)))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\/
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\? #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\?
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\#
    (:register
     (:greedy-repetition 0 nil :everything)))))
 :end-anchor)

该语法较为冗长,如果您在下面查看注释,不一定更具可读性。因此,不要以为语法不那么紧凑,所以事情会自动变得更清晰

但是,如果您开始对正则表达式感到麻烦,请将它们转换为这种格式可能会帮助您解密和调试代码。与基于字符串的格式相比,这是一个优点,在基于字符串的格式中很难发现单个字符错误。 这种语法的主要优点是使用结构化格式而不是基于字符串的编码来处理正则表达式。这样,您就可以像程序中的任何其他数据结构一样组成构建这样的表达式。使用上述语法时,通常是因为我想从较小的部分构建表达式(另请参见CodeGolf答案)。对于您的示例,我们可以写1

`(:sequence
   :start-anchor
   ,(protocol)
   ,(slashes)
   ,(domain)
   ,(top-level-domain) ... )

基于字符串的正则表达式也可以使用辅助函数中的字符串连接和/或内插法来组成。不过,也有与字符串操作趋向于限制混乱代码(想想嵌套问题,不象反引号与$(...)在bash;同时,转义字符可以给你头痛)。

还请注意,上述形式允许使用(:regex "string")形式,以便您可以将简洁的符号与树混合。所有这些使IMHO拥有良好的可读性和可组合性;它间接解决了delnan表示的三个问题(即,不是使用正则表达式本身的语言)。

总结一下

  • 对于大多数目的,简洁的符号实际上是可读的。在处理涉及回溯等扩展符号时会遇到困难,但是很少使用它们。不正当使用正则表达式可能会导致表达式无法读取。

  • 正则表达式无需编码为字符串。如果您有一个库或工具可以帮助您构建和组成正则表达式,则可以避免许多与字符串操作有关的潜在错误。

  • 另外,形式语法更具可读性,并且在命名和抽象子表达式方面表现更好。终端通常表示为简单的正则表达式。


1.您可能更喜欢在读取时构建表达式,因为正则表达式在应用程序中往往是常量。请参阅create-scannerload-time-value

'(:sequence :start-anchor #.(protocol) #.(slashes) ... )

5
也许我只是习惯了传统的RegEx语法,但是我不确定与等效的一行regex相比,22条可读性更高的行更容易理解。

3
@)可以,但是如果您需要一个很长的正则表达式,则可以定义子集(如digitsident)并进行组合。我认为这样做通常是通过字符串操作(串联或插值)完成的,这带来了其他问题,例如正确的转义。例如,搜索\\\\`emacs软件包中的。顺便说一句,这变得更糟,因为相同的转义字符用于\n\"和regex语法等特殊字符\(。语法良好的非lisp示例是printf,其中%d与不冲突\d
coredump

1
关于定义的子集的公平点。这很有意义。我只是怀疑详细程度是否有所提高。对于初学者来说可能会更容易(尽管诸如此类greedy-repetition的概念并不直观,仍然需要学习)。但是,由于很难看到和掌握整个模式,因此牺牲了专家的可用性。

@ dan1111我同意冗长本身并不是一种改善。可以改进的是使用结构化数据而不是字符串来处理正则表达式。
coredump

@ dan1111也许我应该建议使用Haskell进行编辑?Parsec仅用9行就能做到;作为一衬垫:do {optional (many1 (letter) >> char ':'); choice (map string ["///","//","/",""]); many1 (oneOf "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-."); optional (char ':' >> many1 digit); optional (char '/' >> many (noneOf "?#")); optional (char '?' >> many (noneOf "#")); optional (char '#' >> many (noneOf "\n")); eof}。几行像指定长字符串domainChars = ...section start p = optional (char start >> many p)它看起来很简单。
CR Drost 2015年

25

正则表达式的最大问题不是过于简洁的语法,而是我们试图在单个表达式中表达一个复杂的定义,而不是用较小的构建块来组成它。这类似于编程,在其中您从不使用变量和函数,而是将代码全部嵌入在一行中。

将regex与BNF进行比较。它的语法并不比regex干净得多,但是用法不同。首先定义简单的命名符号,然后将它们组合起来,直到出现一个描述要匹配的整个模式的符号。

例如,看一下rfc3986中的URI语法:

URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
hier-part     = "//" authority path-abempty
              / path-absolute
              / path-rootless
              / path-empty
...

您可以使用支持嵌入命名子表达式的regex语法的变体编写几乎相同的东西。


我个人认为,像字符类,串联,选择或重复之类的常用功能可以使用像语法这样的简洁正则表达式,但是对于更复杂和稀少的功能(例如,超前详细名称)则更可取。与我们在正常编程中使用+或类似运算符*并切换到命名函数进行稀有操作非常相似。


12

selfDocumentingMethodName()比e()更好

是吗?大多数语言将{和}用作块定界符,而不是BEGIN和END是有原因的。

人们喜欢简洁,一旦您了解语法,短期术语会更好。想象一下您的正则表达式示例,如果d(用于数字)为“数字”,则正则表达式将更容易阅读。如果使它更易于使用控制字符进行解析,则它看起来更像XML。一旦知道语法,它们都不是很好。

为了正确回答您的问题,您必须意识到正则表达式来自于简洁的时代。很容易想到1 MB的XML文档在今天已经没什么大不了的,但是我们谈论的是1 MB的文档您的整个存储容量。那时使用的语言也较少,而regex距perl或C不到一百万英里,因此对于熟悉语法的当今程序员来说,语法是很熟悉的。因此,没有理由使其更加冗长。


1
selfDocumentingMethodName人们普遍认为更好是e因为程序员的直觉在实际构成可读性或高质量的代码方面现实不符。同意的人是错误的,但是事实就是如此。
Leushenko

1
@Leushenko:您是否声称e()比这更好selfDocumentingMethodName()
JacquesB 2015年

3
@JacquesB可能并非在所有情况下都像全局名称一样。但是对于范围狭窄的事情?几乎可以确定。绝对比传统观点更常见。
Leushenko

1
@Leushenko:我很难想象上下文是单个字母函数名称比更具描述性的名称更好。但是我想这是纯粹的意见。
JacquesB 2015年

1
@MilesRout:该示例实际上是针对e()自记录方法名称的。您能解释在哪种情况下使用单字母方法名称而不是描述性方法名称的改进吗?
JacquesB 2015年

6

正则表达式就像乐高积木。乍一看,您会看到一些可以连接的形状不同的塑料零件。您可能会认为可以塑造的东西可能不会太多,但是您会看到其他人所做的奇妙事情,而只是想知道它是多么奇妙的玩具。

正则表达式就像乐高积木。几乎没有可用的参数,但是以不同的形式链接它们将形成数百万种不同的正则表达式模式,这些模式可用于许多复杂的任务。

人们很少单独使用正则表达式参数。许多语言为您提供了检查字符串长度或将数字部分拆分出来的功能。您可以使用字符串函数来对文本进行切片和重新格式化。当您使用复杂的表格执行非常具体的复杂任务时,会注意到正则表达式的功能。

您可以在SO上找到数以万计的正则表达式问题,而这些问题很少被标记为重复项。仅此一项就显示出可能彼此不同的独特用例。

提供预定义的方法来处理这些非常不同的独特任务并不容易。您具有用于此类任务的字符串函数,但是如果这些函数不足以满足您的指定任务,那么该使用正则表达式了


2

我意识到这是实践问题,而不是效力问题。当直接执行正则表达式而不是假设其具有复合性质时,通常会出现此问题。同样,优秀的程序员会将其程序的功能分解为简洁的方法。

例如,URL的正则表达式字符串可以从以下近似减少:

UriRe = [scheme][hier-part][query][fragment]

至:

UriRe = UriSchemeRe + UriHierRe + "(/?|/" + UriQueryRe + UriFragRe + ")"
UriSchemeRe = [scheme]
UriHierRe = [hier-part]
UriQueryRe = [query]
UriFragRe = [fragment]

正则表达式是很漂亮的东西,但是它们却容易被那些看似复杂的人所滥用。所得的表达式是修辞性的,没有长期价值。


2
不幸的是,大多数编程语言都没有包含有助于编写正则表达式的功能,并且组捕获的工作方式对编写也不是很友好。
CodesInChaos

1
其他语言需要其“与perl兼容的正则表达式”支持赶上Perl 5。子表达式与简单地连接正则表达式规范的字符串不同。捕获应命名,而不要依赖隐式编号。
JDługosz

0

正如@cmaster所说,正则表达式最初设计为仅即时使用,奇怪的是(线压语法)仍然是最受欢迎的一种。我能想到的唯一解释涉及惯性,受虐狂或男子气概(“惯性”通常不是做某事的最吸引人的理由...)

Perl尝试通过允许空格和注释使它们更具可读性,但是进行了相当微弱的尝试,但是并没有做任何富有想象力的事情。

还有其他语法。regexp的scsh语法是一个很好的语法,以我的经验,该脚本生成的regexp相当容易键入,但事后仍然可读。

[ 由于其他原因,scsh非常出色,其中之一就是其著名的致谢文本 ]


2
Perl6可以!看语法。
JDługosz

就我所知,@JDługosz看起来更像是解析器生成器的一种机制,而不是正则表达式的另一种语法。但是区别可能不是很深。
诺曼·格雷

它可以替代,但不限于相同的功能。您可以将regedp转换为具有1到1的修饰符对应关系的内联语法,但语法更易于理解。最初的Perl Apocalypse中有推广它的示例。
JDługosz

0

我相信正则表达式的设计应尽可能“通用”且简单,因此可以在任何地方(大致)以相同的方式使用它们。

您的示例与regex.isRange(..).followedBy(..)特定编程语言的语法以及也许面向对象的样式(方法链接)结合在一起。

例如,这种精确的“正则表达式”在C中的外观如何?该代码将不得不更改。

最“通用”的方法是定义一种简单的简洁语言,然后可以将其轻松嵌入任何其他语言中而无需更改。这就是(几乎)正则表达式。


0

Perl兼容的正则表达式引擎被广泛使用,它提供了许多编辑器和语言都可以理解的简洁的正则表达式语法。正如@JDługosz在评论中指出的那样,Perl 6(不仅是Perl 5的新版本,还是一种完全不同的语言)已尝试通过从单独定义的元素中构建正则表达式来提高可读性。例如,以下是语法示例,用于解析Wikibooks中的 URL :

grammar URL {
  rule TOP {
    <protocol>'://'<address>
  }
  token protocol {
    'http'|'https'|'ftp'|'file'
  }
  rule address {
    <subdomain>'.'<domain>'.'<tld>
  }
  ...
}

像这样拆分正则表达式可允许分别定义每个位(例如,约束domain为字母数字)或通过子类扩展(例如FileURL is URL,将约束限制protocol"file")。

所以:不,没有任何技术上的理由使正则表达式简洁,但是已经有了更新,更简洁和可读性更高的表示它们的方法!因此,希望我们能在这一领域看到一些新想法。

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.