正则表达式--探索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 宏可在更高的抽象等级上来构造正则表达式,可在更清晰的语义下,构造正则表达式。 上面没有给出所有的正则语法构造,只给出一些常用的特征,有任何疑惑,请直接阅读函数文档。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容