正则表达式--探索rx宏

原文地址: Regular Expression,你喜欢阅读原汁原味的,请阅读原文 。本文只做学习之用。

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

最近一直忙于crystal-mode的扩展与维护工作,说实话,真不容易,特别是正则表达式部分,太难阅读。 所以我准备把之前的正则表达式,改造成更可读的 s-expression 形式的,这样的话,维护起来就能简单很多。 rx 宏更好能很好的满足要实现的目标。

(require 's)  ;; All we need is =s-matches-p=
(require 'rx)

;; Creating a regexp that will match -> <File> [<Line>:<Column] <Suggestion>
(setq this-file-name "regular-expression.org")

(s-matches-p
 (rx bol
     (eval this-file-name)
     space
     "[" (group (one-or-more digit)) ":" (group (one-or-more digit)) "]"
     space
     (group (zero-or-more anything))
     eol)
 "blog.org [17:16] Emacs Lisp, not emacs lisp")

;; Produced regexp, I do not want to write or maintain this by hand
"^blog\\.org[[:space:]]\\[\\([[:digit:]]+\\):\\([[:digit:]]+\\)][[:space:]]\\(\\(?:.\\|
\\)*\\)$"

虽然不那么简洁,但上面的示例很好的说明了在更高抽象等级下编写正则表达式的优点:更易于理解,写起来更舒适,更容易维护。 同时,使用符号表达式的形式更符合emacs的气质。

Strings And Quoting<a id="sec-1"></a>

STRING
     matches string STRING literally.

CHAR
     matches character CHAR literally.

‘(eval FORM)’
     evaluate FORM and insert result.  If result is a string,
     ‘regexp-quote’ it.

问题:什么样的正则表达式匹配这个字符串: ASCII表中的标点字符: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}

;; Escape the double quote here
(setq input "The punctuation characters in the ASCII table are: !\"#$%&'()*+,-./:;<=>?@[\]^_`{|}")

(s-matches-p (rx "The punctuation characters in the ASCII table are: !\"#$%&'()*+,-./:;<=>?@[\]^_`{|}")
             input) ;; Direct use of strings

(not (s-matches-p input input)) ;; Does not work because of quoting
(s-matches-p (regexp-quote input) input)

(s-matches-p (rx (eval input)) input) ;; More rx

如果你很清楚(正则表达式)语法字符的话, 可以很容易的看出,这个问题只是由引用或转义语法字符引起的。 函数 regexp-quote 可以转义这些字符,这很简单。 rx 默认转义,可以直接传入字符串。 最后,可通过 eval 语法来使用字符串变量,来完成转义。

Variables And Ranges<a id="sec-2"></a>

    ‘(any SET ...)’
    ‘(in SET ...)’
    ‘(char SET ...)’
         matches any character in SET ....  SET may be a character or string.
         Ranges of characters can be specified as ‘A-Z’ in strings.
         Ranges may also be specified as conses like ‘(?A . ?Z)’.

         SET may also be the name of a character class: ‘digit’,
         ‘control’, ‘hex-digit’, ‘blank’, ‘graph’, ‘print’, ‘alnum’,
         ‘alpha’, ‘ascii’, ‘nonascii’, ‘lower’, ‘punct’, ‘space’, ‘upper’,
         ‘word’, or one of their synonyms.

问题:创建一个正则表达式来匹配 calendar 的所有常见拼写错误,这样就可在文档中找到这个词, 从而不必来考验写作者的拼写能力。 允许在每个元音位置使用a或e。

(s-matches-p (rx "c"
                 (any "a" "e")
                 "l"
                 (any "a" "e")
                 "nd"
                 (any "a" "e")
                 "r")
             "celander")

(setq misspelling-pattern `(any "a" "e"))

(s-matches-p (rx "c"
                 (eval misspelling-pattern)
                 "l"
                 (eval misspelling-pattern)
                 "nd"
                 (eval misspelling-pattern)
                 "r")
             "calendar")

"c[ae]l[ae]nd[ae]r" ;; Generated pattern

除了演示一个简单的范围构造,通过熟悉的 eval 使用子模式允许更加模块化地处理这些表达式,这有助于摆脱单一的连接字符串。

问题:创建一个正则表达式来匹配单个十六进制字符。

(s-matches-p (rx (any "a-f" "A-F" "0-9"))
             "A")
(s-matches-p (rx (in "a-f" "A-F" "0-9"))
             "A") ;; Equivalently

"[0-9A-Fa-f]" ;; Generated pattern


(s-matches-p (rx (char hex-digit))
             "d") ;; More rx
(s-matches-p (rx hex-digit)
             "d") ;; Equivalently

"[[:xdigit:]]" ;; Generated pattern

最后,范围语法允许熟悉的破折号来表示字符范围。 Rather, the abstraction of special character ranges like [:upper:] or [:xdigit:] is nice to know. Other useful constructs such as word-start, line-end, and punctuation exist that is worthy to be explored.

Alternatives And Depth<a id="sec-3"></a>

    ‘(or SEXP1 SEXP2 ...)’
    ‘(| SEXP1 SEXP2 ...)’
         matches anything that matches SEXP1 or SEXP2, etc.  If all
         args are strings, use ‘regexp-opt’ to optimize the resulting
         regular expression.

    ‘(zero-or-one SEXP ...)’
    ‘(optional SEXP ...)’
    ‘(opt SEXP ...)’
         matches zero or one occurrences of A.

    ‘(and SEXP1 SEXP2 ...)’
    ‘(: SEXP1 SEXP2 ...)’
    ‘(seq SEXP1 SEXP2 ...)’
    ‘(sequence SEXP1 SEXP2 ...)’
         matches what SEXP1 matches, followed by what SEXP2 matches, etc.

    ‘(repeat N SEXP)’
    ‘(= N SEXP ...)’
         matches N occurrences.

问题:创建一个正则表达式,当重复应用于文本 Mary, Jane, and Sue went to Mary's house 会匹配 Mary, Jane, Sue 然后再次匹配 Mary

(s-match-strings-all
 (rx (or "Mary" "Jane" "Sue"))
 "Mary, Jane, and Sue went to Mary's house")

;; Output
'(("Mary") ("Jane") ("Sue") ("Mary"))

;; Generated pattern
"\\(?:Jane\\|Mary\\|Sue\\)"

这个简单的问题是使用与范围和类有关的交替构造的示例。 没有什么花哨的东西,但存在使其细微差别的可能性。

问题:创建一个匹配0到255的正则表达式。

(setq range-expression ;; Expression and pattern separated for reuse
      `(or "0"
           (sequence "1" (optional digit (optional digit)))
           (sequence "2" (optional
                          (or
                           (sequence (any "0-4") (optional digit))
                           (sequence "5" (optional (any "0-5")))
                           (sequence (any "6-9") (optional digit)))))
           (sequence (any "3-9") (optional digit))))

(setq range-pattern (rx (eval range-expression)))

;; A test for the regular expression
(require 'cl)
(cl-every (lambda (number)
            (s-matches-p range-pattern (number-to-string number)))
          (number-sequence 0 255))

;; Generated pattern
"0\\|1\\(?:[[:digit:]][[:digit:]]?\\)?\\|2\\(?:[0-4][[:digit:]]?\\|5[0-5]?\\|[6-9][[:digit:]]?\\)?\\|[3-9][[:digit:]]?"

;; To use this IP Addresses
(setq ip4-pattern (rx (repeat 3 (sequence (eval range-expression) "."))
                      (eval range-expression)))

;; Testing for permutation might take too long, one is good enough
(s-matches-p ip4-pattern
             "61.12.234.251")

;; Generated pattern
"\\(?:\\(?:0\\|1\\(?:[[:digit:]][[:digit:]]?\\)?\\|2\\(?:[0-4][[:digit:]]?\\|5[0-5]?\\|[6-9][[:digit:]]?\\)?\\|[3-9][[:digit:]]?\\)\\.\\)\\{3\\}\\(?:0\\|1\\(?:[[:digit:]][[:digit:]]?\\)?\\|2\\(?:[0-4][[:digit:]]?\\|5[0-5]?\\|[6-9][[:digit:]]?\\)?\\|[3-9][[:digit:]]?\\)"

上面的 range-expression 有问题,下面给出改正和测试如下:

(setq range-expression ;; Expression and pattern separated for reuse
      `(or "0"
           (sequence "1" (optional digit (optional digit)))
           (sequence "2" (optional
                          (or
                           (sequence (any "0-4") (optional digit))
                           (sequence "5" (optional (any "0-5")))
                           (optional digit))))
           (sequence (any "3-9") (optional digit))))

(setq range-pattern (rx bol (eval range-expression) eol ))

;; A test for the regular expression
(require 'cl)
(cl-every (lambda (number)
            (s-matches-p range-pattern (number-to-string number)))
          (number-sequence 0 255))

(cl-every (lambda (number)
            (not (s-matches-p range-pattern (number-to-string number))))
          (number-sequence 256 355))

这个表达的想法是匹配第一个数字,然后考虑分支。 即使不深入解释,语法应该是有帮助的; 但三个新的结构值得好好说明下。 首先, optionalopt 语法与 zero-or-one 结构等价。 其次, sequenceseq 语法主要是一个表达式包装器,其中列表不是一个原子是必需的。 第三, repeat 语法与先前模式的重复构造相同。 不管新的语法如何,问题只是在展示语法。

另外,请记住为正则表达式编写测试。

在我忘记之前, eval 要求变量存在于解释器中; 这意味着,它们必须在使用之前通过 setq 进行全局设置。 这就是为什么在片段中的两个 setters 分别设置表达和模式的原因。 建议通过 defconstdefvar 设置表达式或模式作为重构。 不幸的是, let 不能与 eval 一起工作,但这不是一项巨大的成本。

Groups And Backreferencs<a id="sec-4"></a>

    ‘(submatch SEXP1 SEXP2 ...)’
    ‘(group SEXP1 SEXP2 ...)’
         like ‘and’, but makes the match accessible with ‘match-end’,
         ‘match-beginning’, and ‘match-string’.

    ‘(submatch-n N SEXP1 SEXP2 ...)’
    ‘(group-n N SEXP1 SEXP2 ...)’
         like ‘group’, but make it an explicitly-numbered group with
         group number N.

问题:创建一个正则表达式,以yyyy-mm-dd格式匹配任何日期,并分别捕获年,月和日。 作为额外的挑战,请将组命名。

(setq date-pattern
      (rx (group-n 3 (repeat 4 digit))
          "-"
          (group-n 2 (repeat 2 digit))
          "-"
          (group-n 1 (repeat 2 digit))))

(s-match-strings-all date-pattern
                     (format-time-string "%F"))

;; Output and pattern, notice it is day, month and year or reverse order
"\\(?3:[[:digit:]]\\{4\\}\\)-\\(?2:[[:digit:]]\\{2\\}\\)-\\(?1:[[:digit:]]\\{2\\}\\)"
'(("2017-03-30" "30" "03" "2017"))

捕获 group 是本质的, 这是语法起作用的地方。 命名 group 在这里是不可能的,相反,仅限于编号 group 。 需要注意的,这不是宏的限制,而是 Emacs Lisp 正则表达式语法的限制。

group-ngroup 语法在意图上很明显。 第一个参数代表组号,其余的是实际的表达式。 没有什么花哨。

问题:创建一个正则表达式,以yyyy-mm-dd格式匹配“神奇”日期。 如果年份减去世纪,月份和月份的日期都是相同的数字,则日期是神奇的。 例如,2008-08-08是一个神奇的约会。

(setq magical-pattern
      (rx
       (repeat 2 digit)
       (group-n 1 (repeat 2 digit))
       "-"
       (backref 1)
       "-"
       (backref 1)))

(s-matches-p magical-pattern
             "2008-08-08")

;; Generated pattern
"[[:digit:]]\\{2\\}\\(?1:[[:digit:]]\\{2\\}\\)-\\1-\\1"

这只是显示反向引用可用。 backref 语法只是用数字参数调用组。 再一次,没什么复杂的。

re-builder<a id="sec-5"></a>

为了更好的检测编写的正则表达式, Emacs 中存在用于测试和试验正则表达式的用户界面: re-builder 。 在包含文本的缓冲区上执行命令 re-builderregexp-builder ,然后执行 reb-change-syntax 并选择 rx 。 如下图所示。 [站外图片上传中...(image-9a45fe-1519915620386)]

这个UI可以处理原始表达式,但我们这里只对rx感兴趣。 详细说明,每次表达式更新时,它都会突出显示任何可能的匹配项。 虽然它不像动态或程序化,但它作为一个快速实验和检查很方便。

总结<a id="sec-6"></a>

rx 宏不能作为学习正则表达式的替代品,因为它构建的DSL不能完全覆盖所有的细节。但是写原始的正则表达式,真是很痛苦, 所以使用 rx 宏可在更高的抽象等级上来构造正则表达式,可在更清晰的语义下,构造正则表达式。 上面没有给出所有的正则语法构造,只给出一些常用的特征,有任何疑惑,请直接阅读函数文档。

推荐阅读更多精彩内容