通用 Lisp/外部库/CL-PPCRE
Common Lisp 可移植 Perl 兼容正则表达式库,或 CL-PPCRE,将 Perl 正则表达式的强大功能带到 Common Lisp。用作者 Edi Weitz 的话说,CL-PPCRE 具有以下特点
- 它与 Perl 兼容。
- 它速度很快。
- 它可以在符合 ANSI 标准的 Common Lisp 实现之间移植。
- 它是线程安全的。
- 除了像 Perl 一样将正则表达式指定为字符串之外,您还可以使用 S 表达式。
- 它附带 BSD 风格的许可证,因此您可以基本上随心所欲地使用它。
CL-PPCRE 的主要入口点是scan函数。Scan接受一个正则表达式(或 regex)和一个要匹配的字符串,并返回 regex 的匹配开始和结束索引,以及您在 regex 中定义的任何寄存器的开始和结束索引。
CL-USER> (scan "b(.)r" "foo bar baz bur")
4
7
#(5)
#(6)
第一个和第二个返回值分别是匹配子字符串的开始和结束。第三和第四个返回值标记寄存器匹配的开始和结束索引。注意,它只找到了第一个实例 bar。要找到下一个实例,您可以为关键字参数传递值:start. 不用说,您也可以通过指定 :end 关键字来限制扫描在字符串上运行的距离。
;; Match the next instance of "b.r" at or after position 5
CL-USER> (scan "b.r" "foo bar baz bur" :start 5)
12
15
#()
#()
;; This fails, because there is no match between 5 and 13
;; (even though the regex has started at 12, it doesn't
;; finish by 13)
CL-USER> (scan "b.r" "foo bar baz bur" :start 5 :end 13)
NIL
您可能已经注意到,在扫描字符串时跟踪起点可能有点繁琐。为此,CL-PPCRE 为常见任务提供了一些方便的函数和宏,例如
- do-scans
- scan-to-strings
- do-matches
- do-register-groups
- all-matches
- split
- regex-replace
- regex-replace-all
虽然 CL-PPCRE 将正则表达式作为字符串,但实际上它会将该字符串解析为正则表达式树。除了是 Lisp 的做法之外,这也消除了正则表达式的许多神秘之处,但是,它用冗长性来交换它。一个很好的特性是,由于您手头有正则表达式的解析树,因此可以直观地以编程方式动态构建或修改正则表达式。
;;; The unexported function cl-ppcre::parse-string parses a
;;; regex string into a regex tree. This allows us to see
;;; how strings translate to trees.
;; Character alternatives, Classes, wildcards
CL-USER> (cl-ppcre::parse-string "[abcdef][^abcdef]")
(:SEQUENCE (:CHAR-CLASS #\a #\b #\c #\d #\e #\f)
(:INVERTED-CHAR-CLASS #\a #\b #\c #\d #\e #\f))
;; Note the double backslashes standard Lisp technique to
;; get a literal backslash
CL-USER> (cl-ppcre::parse-string ".\\d\\D\\w\\W\\s\\S")
(:SEQUENCE :EVERYTHING :DIGIT-CLASS :NON-DIGIT-CLASS :WORD-CHAR-CLASS
:NON-WORD-CHAR-CLASS :WHITESPACE-CHAR-CLASS :NON-WHITESPACE-CHAR-CLASS)
;; Repetitions (Note that "a*?" doesn't make much sense as
;; it will always match the empty string, but it does this
;; correctly)
CL-USER> (cl-ppcre::parse-string "Greedy:a*b+c{2,5}d{2,}Non-greedy:a*?b+?c{2,5}?d{2,}?")
(:SEQUENCE
"Greedy:"
(:GREEDY-REPETITION 0 NIL #\a)
(:GREEDY-REPETITION 1 NIL #\b)
(:GREEDY-REPETITION 2 5 #\c)
(:GREEDY-REPETITION 2 NIL #\d)
"Non-greedy:"
(:NON-GREEDY-REPETITION 0 NIL #\a)
(:NON-GREEDY-REPETITION 1 NIL #\b)
(:NON-GREEDY-REPETITION 2 5 #\c)
(:NON-GREEDY-REPETITION 2 NIL #\d))
;; Alternatives
CL-USER> (cl-ppcre::parse-string "a|b|c")
(:ALTERNATION #\a #\b #\c)
;; Groups, Registers, and Back References.
CL-USER> (cl-ppcre::parse-string "(a..)+(?:def)+\\1")
(:SEQUENCE
(:GREEDY-REPETITION 1 NIL (:REGISTER (:SEQUENCE #\a :EVERYTHING :EVERYTHING)))
(:GREEDY-REPETITION 1 NIL (:GROUP "def")) (:BACK-REFERENCE 1))
;; This matches strings like "abcdefdefabc"
在获得正则表达式的树形形式后,CL-PPCRE 使用函数create-scanner来编译该表示形式(许多 Lisp 编译为本地机器代码)。这做两件事:(1)它往往使正则表达式扫描非常快,并且(2)它往往使第一次定义正则表达式变得很昂贵(由于编译开销,尽管有一些变量可以设置为减少这种开销)。编译完成后,您可以重复使用同一个表达式来规避编译开销。CL-PPCRE 还使用适当的算法生成高效的正则表达式表示,即不是基于堆栈的系统。总的来说,这是一个非常高效的库。
;; Create a scanner closure that finds matches that resemble IPs
;; and fill registers with the numbers
CL-USER> (cl-ppcre::create-scanner "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})")
#<CLOSURE (LAMBDA (STRING CL-PPCRE::START CL-PPCRE::END)) {12E62C3D}>
NIL
;; using * as last return value
CL-USER> (cl-ppcre::scan-to-strings * "192.168.1.255")
"192.168.1.255"
#("192" "168" "1" "255")
好吧,使用 CL-PPCRE 与使用任何其他正则表达式引擎并没有太大区别。在这里,我们将看到一些快速而肮脏的使用示例。如果您在如何执行某个操作时遇到困难,请查阅正则表达式教程,perlre手册页,或 Perl 用户。您只需要记住将您的 regexs 双反斜杠(就像您始终必须执行的操作一样,将文字反斜杠插入 Lisp 字符串中)。
CL-USER> (defparameter *url-regex* "((([A-Za-z]{3,9}:(?://)?)(?:[-;:&=+$,\\w]+@)?[A-Za-z0-9.-]+|(?:www\\.|[-;:&=+$,\\w]+@)[A-Za-z0-9.-]+)((?:/[+~%/.\\w-]*)?\\??(?:[-+=&;%@.\\w]*)#?(?:[.!/\\w]*))?)")
CL-USER> (cl-ppcre::parse-string *url-regex*)
(:REGISTER
(:SEQUENCE
(:REGISTER
(:ALTERNATION
(:SEQUENCE (:REGISTER (:SEQUENCE (:GREEDY-REPETITION 3 9 (:CHAR-CLASS (:RANGE #\A #\Z) (:RANGE #\a #\z))) #\: (:GREEDY-REPETITION 0 1 (:GROUP "//"))))
(:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE (:GREEDY-REPETITION 1 NIL (:CHAR-CLASS #\- #\; #\: #\& #\= #\+ #\$ #\, :WORD-CHAR-CLASS)) #\@)))
(:GREEDY-REPETITION 1 NIL (:CHAR-CLASS (:RANGE #\A #\Z) (:RANGE #\a #\z) (:RANGE #\0 #\9) #\. #\-)))
(:SEQUENCE (:GROUP (:ALTERNATION "www." (:SEQUENCE (:GREEDY-REPETITION 1 NIL (:CHAR-CLASS #\- #\; #\: #\& #\= #\+ #\$ #\, :WORD-CHAR-CLASS)) #\@)))
(:GREEDY-REPETITION 1 NIL (:CHAR-CLASS (:RANGE #\A #\Z) (:RANGE #\a #\z) (:RANGE #\0 #\9) #\. #\-)))))
(:GREEDY-REPETITION 0 1
(:REGISTER
(:SEQUENCE (:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE #\/ (:GREEDY-REPETITION 0 NIL (:CHAR-CLASS #\+ #\~ #\% #\/ #\. :WORD-CHAR-CLASS #\-)))))
(:GREEDY-REPETITION 0 1 #\?) (:GROUP (:GREEDY-REPETITION 0 NIL (:CHAR-CLASS #\- #\+ #\= #\& #\; #\% #\@ #\. :WORD-CHAR-CLASS)))
(:GREEDY-REPETITION 0 1 #\#) (:GROUP (:GREEDY-REPETITION 0 NIL (:CHAR-CLASS #\. #\! #\/ :WORD-CHAR-CLASS))))))))
CL-USER> (cl-ppcre::scan-to-strings *url-regex* "yo mailto:[email protected] asd http://foo.com")
"mailto:[email protected]"
#("mailto:[email protected]" "mailto:[email protected]" "mailto:" "")