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

我们都同意selfDocumentingMethodName()e()更好。为什么这也不适用于正则表达式?

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

/>
这甚至不是对URL的严格解析!

相反,对于一个基本示例,我们可以使一些管道结构井井有条并易于阅读: />
var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})(0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;


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

评论

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

我尝试使用称为RegexToolbox的库来解决此可读性问题。到目前为止,它已移植到C#,Java和JavaScript-请参见github.com/markwhitaker/RegexToolbox.CSharp。

为解决此问题已进行了许多尝试,但是文化很难改变。在这里查看我有关口头表达的答案。人们力争找到可用的最低通用工具。

#1 楼

正则表达式设计得如此简洁的一个重要原因是:它们被设计为用作代码编辑器的命令,而不是用作进行编码的语言。更确切地说,ed是最早使用正则表达式的程序之一表达式,从那里开始正则表达式征服世界的统治。例如,ed命令g/<regular expression>/p很快启发了一个名为grep的单独程序,该程序至今仍在使用。由于功能强大,它们随后被标准化并用于各种工具,例如sedvim。那么,为什么这个起源偏向于简洁的语法呢?因为您无需键入编辑器命令就可以再读取一次。您可以记住如何将其组合在一起,并且可以使用它想要做的事情就足够了。但是,您必须键入的每个字符都会减慢文件编辑的进度。正则表达式语法旨在以一种一次性的方式来编写相对复杂的搜索,而这正是使人们头疼的问题,人们将它们用作代码来解析程序的某些输入。

评论


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

– njzk2
2015年9月30日,下午3:34

@ njzk2这个答案实际上是错误的。 HTML文档不是常规语言,而是问题所要求的HTML open标签。

–Random832
2015年9月30日4:30在

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

–布朗博士
2015年9月30日,下午5:56

因此,对于那些认为grep是错误发音的“抓斗”的人来说,它实际上来自g / re(对于正则表达式)/ p吗?

– Hagen von Eitzen
15年9月30日在6:34

@DannyPflughoeft不,不是。开放标签只是,其中没有嵌套标签。您不能嵌套标签,嵌套的是元素(打开标签,包括子元素的内容,关闭标签),这些问题并不是问解析问题。 HTML标记是一种常规语言-平衡/嵌套发生在标记之上。

–Random832
2015年9月30日在17:31



#2 楼

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

一个很大的原因是惯性和普遍存在。它并没有说明它们最初是如何变得如此流行的,但是现在它们已经流行了,任何知道正则表达式的人都可以用一百种不同的语言和另外一千种软件工具来使用这些技能(方言之间的差异很小)(例如文本编辑器和命令行工具)。顺便说一句,后者不会也不会使用任何等同于编写程序的解决方案,因为它们被非程序员大量使用。即使在其他工具更好的情况下也可以使用。我不认为regex语法很糟糕。但这显然在短而简单的模式上要好得多:类似C语言的标识符的原型示例[a-zA-Z_][a-zA-Z0-9_]*可以使用最少的正则表达式知识来阅读,并且一旦满足该条件,它便是显而易见且简洁的。需要更少的字符并不是天生的坏事,相反。只要保持理解,简洁就是一种美德。

至少有两个原因使该语法在诸如此类的简单模式上表现出色:它不需要对大多数字符进行转义,因此它读起来相对自然,并且它使用所有可用的标点来表示各种简单的解析组合器。也许最重要的是,它根本不需要任何东西进行测序。您首先写东西,然后再写东西。将此与您的followedBy进行对比,尤其是在以下模式不是文字而是更复杂的表达式的情况下。我看到了三个主要问题:没有抽象功能。形式语法与正则表达式起源于理论计算机科学的同一领域,它具有一系列生成形式,因此可以为模式的中间部分命名: >正如我们在上面可以看到的,没有特殊意义的空格对于允许格式化更容易使眼睛有用。评论也一样。正则表达式无法做到这一点,因为一个空格就是那个' '。但是请注意:有些实现允许“冗长”的模式,其中空格被忽略,并且可以注释。
没有元语言来描述常见的模式和组合器。例如,一个人可以编写一次digit规则,并在上下文无关的语法中继续使用它,但是一个人不能定义一个“函数”,也就是说,给定了一个产生式p并创建了一个新的产生式,它为此做了更多的工作例如,为p的出现的逗号分隔列表创建一个产生。

您提出的方法肯定可以解决这些问题。它只是不能很好地解决它们,因为它的简洁程度远远超出了必要。可以解决前两个问题,同时保持相对简单和简洁的领域特定语言。当然,第三个……程序解决方案需要通用的编程语言,但以我的经验,第三个问题是迄今为止最少的那些问题。很少有模式会发生程序员想要的能够定义新组合器的相同复杂任务。而且在必要时,该语言通常非常复杂,以至于无论如何也不应使用正则表达式对其进行解析。大约一万个解析器组合器库可以大致满足您的建议,只是使用一组不同的操作,通常使用不同的语法,并且几乎总是具有比正则表达式更多的解析能力(即,它们处理的是无上下文语言或某些这些子集)。然后是解析器生成器,它们与上述“使用更好的DSL”方法一起使用。而且始终可以选择以适当的代码手动编写一些解析。您甚至可以混合搭配,使用正则表达式处理简单的子任务,并在调用正则表达式的代码中做复杂的事情。

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

评论


我对计算的早期了解不足,无法解释正则表达式如何变得如此流行。但是,我们可能会引起猜测:基本的正则表达式引擎非常易于实现,比高效的无上下文解析器容易得多。

–biziclop
2015年9月29日19:08



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

–user7043
2015年9月29日19:15



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

–biziclop
15年9月29日在21:36

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

–J.Todd
15年9月29日在23:29

方言之间的差异很小,我不会说这是“很少”。任何预定义的字符类在不同的方言之间都有几个定义。此外,还有针对每种方言的解析怪癖。

– nhahtdh
15年9月30日在4:42

#3 楼

历史观点

维基百科上的文章非常详细地介绍了正则表达式的起源(Kleene,1956年)。原始语法相对简单,仅*+?|和分组(...)。它是简洁的(而且可读性强,两者不一定是对立的),因为形式语言倾向于用简洁的数学符号来表示。试图通过设计使其简洁(“常见的结构应该简短”)。这使语法复杂化了很多,但是请注意,人们现在已经习惯于正则表达式,并且擅长编写(如果不阅读)它们。它们有时只是写的事实表明,当它们太长时,它们通常不是正确的工具。
正则表达式在被滥用时往往不可读。
基于正则表达式的内容

关于替代语法,让我们看一下已经存在的一种语法(Common Lisp中的cl-ppcre)。可以使用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答案)。对于您的示例,我们可以编写:

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


基于字符串的正则表达式也可以是组成,使用字符串串联和/或内插在辅助函数中的插值。但是,字符串操作存在一些局限性,这些局限往往会使代码混乱(请考虑嵌套问题,与bash中的反引号和$(...)不同;此外,转义字符可能会让您头疼)。

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

总结



在大多数情况下,简洁符号实际上是可读的。在处理涉及回溯等扩展符号时会遇到困难,但是很少使用它们。不正当地使用正则表达式可能会导致无法理解的表达式。
正则表达式不需要编码为字符串。如果您有一个库或工具可以帮助您构建和组合正则表达式,则可以避免与字符串操作有关的许多潜在错误。子表达式。终端通常表示为简单的正则表达式。


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

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


评论


也许我只是习惯了传统的RegEx语法,但是我不确定22条可读性比同等的正则表达式更容易理解的行。

–user82096
2015年9月30日在6:04

@)可以理解,但是如果您需要一个非常长的正则表达式,则可以定义一些子集,例如数字,ident和组成它们。我认为这样做通常是通过字符串操作(串联或插值)完成的,这带来了其他问题,例如正确的转义。例如,在emacs软件包中搜索\\\\`的出现。顺便说一句,这变得更糟,因为对于\ n和\“之类的特殊字符以及正则表达式语法\(都使用相同的转义字符。良好语法的非lisp示例是printf,其中%d与\ d不冲突。

– coredump
2015年9月30日在7:20



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

–user82096
2015年9月30日13:02

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

– coredump
2015年9月30日13:14在

@ dan1111也许我应该建议使用Haskell进行编辑? Parsec仅用9行就能做到;作为单行代码:do {optional(many1(letter)>> char':');选择(映射​​字符串[“ ///”,“ //”,“ /”,“”])); many1(“ 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-”中的一个);可选(char':'>> many1位数字);可选(char'/'>> many(noneOf“?#”));可选(char'?'>> many(noneOf“#”));可选(char'#'>> many(noneOf“ \ n”)); eof}。用几行像将长字符串指定为domainChars = ...和section start p = optional(char start >> many p)看起来很简单。

– CR Drost
2015年10月1日在16:01



#4 楼

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

将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语法的变体来编写几乎相同的东西。 regex之类的语法适合于字符类,串联,选择或重复等常用功能,但对于更复杂和稀有的功能(如超前详细名称)则更可取。与我们在常规编程中使用+*之类的运算符非常相似,并切换到命名函数以进行较少的操作。

#5 楼


selfDocumentingMethodName()远胜于e()


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

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

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

评论


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

–卢申科
2015年10月1日在8:18



@Leushenko:您是否声称e()比selfDocumentingMethodName()更好?

–雅克B
2015年10月1日19:37

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

–卢申科
2015年10月1日在22:26

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

–雅克B
2015年10月5日,11:02

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

–雅克B
2015年10月7日在10:01



#6 楼

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

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

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

您可以在SO上找到成千上万个正则表达式问题,而这些问题很少被标记为重复项。仅此一项就说明了可能存在的独特用例,它们彼此之间非常不同。您具有用于此类任务的字符串函数,但是如果这些函数不足以满足您的指定任务,那么该使用regex

#7 楼

我认识到这是实践问题,而不是效力问题。当直接执行正则表达式而不是假定复合性质时,通常会出现此问题。同样,一个好的程序员会将其程序的功能分解为简洁的方法。例如,URL的正则表达式字符串可以从以下形式减少:

/>
到:

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


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

评论


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

– CodesInChaos
15年9月30日在11:35

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

–JDługosz
15年9月30日在17:52

#8 楼

正如@cmaster所说,正则表达式最初设计为仅即时使用,奇怪的是(线压语法)仍然是最受欢迎的一种。我能想到的唯一解释涉及惯性,受虐狂或大男子主义(“惯性”通常不是做某事的最吸引人的理由...)
通过允许使用空格和注释,使它们更具可读性,但并不能起到任何想象力。

还有其他语法。 regexp的scsh语法是一个很好的语法,以我的经验,它生成的regexp相当容易键入,但事后仍然可读。是其著名的致谢文字]

评论


Perl6可以!看语法。

–JDługosz
2015年9月30日下午0:59

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

–诺曼·格雷(Norman Gray)
2015年9月30日15:31在

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

–JDługosz
15年9月30日在17:47

#9 楼

我相信正则表达式的设计应尽可能“通用”且简单,因此可以在任何地方(大致)以相同的方式使用它们。特定编程语言的语法,甚至可能是面向对象的样式(方法链)。代码必须进行更改。这就是(几乎)正则表达式。

#10 楼

Perl兼容的正则表达式引擎被广泛使用,它提供了许多编辑器和语言都可以理解的简洁的正则表达式语法。正如@JDługosz在评论中指出的那样,Perl 6(不仅仅是Perl 5的新版本,而是一种完全不同的语言)试图通过从单独定义的元素中构建正则表达式来提高可读性。例如,下面是一个语法示例,用于解析Wikibooks中的URL:是字母数字)或通过子类扩展(例如,将domain约束为FileURL is URLprotocol)。代表他们的方法已经在这里!因此,希望我们能在该领域看到一些新想法。