本文是《实战Common Lisp》系列的第一篇文章。本系列主要讲述在使用Common Lisp时能派上用场的小函数,希望能为Common Lisp的复兴做一些微小的贡献。
序言
众所周知,Common Lisp没有内置多少处理字符串的函数,下面的代码便能看到所有以STRING开头的函数名:
(do-external-symbols (s :cl)
(when (and (fboundp s)
(equal (search "STRING" (symbol-name s)) 0))
(print s)))
屈指可数,而且大部分是比较两个字符串的!很多其它语言中的“标配”是不存在的,比如想要将多个字符串连接起来这么简单的功能都没有!要么自己实现,要么依赖第三方库,比如cl-str
提供的join
函数。
标准库也没有内置正则表达式,好在有一个优秀的第三方库可以用:cl-ppcre
。一种使用正则的常见需求是提取符合某种模式的内容,比如提取一篇Markdown文章中所有的图片链接。图片链接可以由下列正则括号的部分匹配:
"!\\[.*\\]\\((.*)\\)"
在cl-ppcre中这个括号匹配的部分叫做第一个register。我想要写一个能够提取出字符串中所有符合这段正则的第一个register,分两步走:
- 先实现一个
all-first-registers-to-strings
函数,实现通用的、提取一个字符串中所有符合某段正则的子串的第一个register的内容; - 基于
all-first-registers-to-strings
实现一个extract-image-paths
。
all-first-registers-to-strings
要实现这个函数,需要借助cl-ppcre的scan
函数。根据scan函数的文档,当正则匹配成功时,它的第三、第四个返回值表示正则中register的起点和终点在字符串中的偏移——它们是两个数组,起点和终点一一对应。有了起点和终点的偏移,再使用CL:SUBSEQ
便能提取出register对应的子串。如果要把字符串中所有匹配register的内容都拿出来,就反复调用scan函数,直到再也没有匹配成功为止。
all-first-registers-to-strings
的定义如下:
(ql:quickload 'cl-ppcre)
(defun all-first-registers-to-strings (regex target-string)
"返回TARGET-STRING中所有匹配正则表达式REGEX的字符串属于第一个register的内容。"
(check-type regex string)
(check-type target-string string)
(let ((pos 0)
(strs '()))
(loop
(multiple-value-bind (start end register-begins register-ends)
(cl-ppcre:scan regex target-string :start pos)
(unless start
(return-from all-first-registers-to-strings (nreverse strs)))
(push (subseq target-string (svref register-begins 0) (svref register-ends 0)) strs)
(setf pos end)))))
- 为了不停地在
target-string
中前进,用变量pos
存储scan
函数的第二个返回值,并作为下一次调用scan
时的start
参数——显然,pos
的初始值为0; - 为了跳出
loop
,使用了return-from
直接从函数返回; - 用
push
收集最终结果,再用nreverse
处理成最终的返回值——印象中《On Lisp》也说过这是一种比较常见的手法。
后记
有了all-first-registers-to-strings
,就可以轻松实现extract-image-paths
了:
(defun extract-image-paths (content)
"从博文内容CONTENT中提取出图片的绝对路径。"
(all-first-registers-to-strings "!\\[.*\\]\\((.*)\\)" content))
显然,这个all-first-registers-to-strings
函数实现得很糟糕,尤其是loop
的用法实在是太不Lispy了。Common Lisp的loop
的用法纷繁复杂如天上的星星,多半可以用更优雅的方法来重写一遍,这个就留给各位读者作为私下的乐趣吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。