上一章:迭代
在第二章「文本解析」所实现的解析器程序里,为了判断一行文本是否以 \`\`\`
开头,我定义了一个函数:
(defun text-match (source target)
(setq n (length target))
(if (< (length source) n)
nil
(string= (substring source 0 n) target)))
事实上,Elisp 提供了更强大的文本匹配函数。如何的强大呢?强大到了支持正则表达式匹配。
正则表达式,就像古代官府捉拿江洋大盗时在城门边上张贴的通缉告示上的罪犯画像。罪犯的长相越有特点,他的画像便越有用处。我还觉得现代的机器学习程序在识别照片里的人脸,其原理也像是在城门边上张贴通缉告示。
如何给一段文本画像呢?具体而言,如何给以 \`\`\`
作为开头的文本画像呢?很简单,只要像下面这样画
^```
^
的意思是「开头」,后面紧跟着 \`\`\`
,就表示开头是 \`\`\`
。
Elisp 的 string-match
函数可以用正则表达式构成的字符串对象去匹配另一个字符串对象,例如:
(string-match "^```" "```lisp")
注意,为了便于讲述,从现在开始,诸如字符串对象(或字符串类型的实例),列表对象(或列表类型的实例),若没有特殊声明,统统简称为字符串、列表。应该不会导致误解。
上述示例中,由于字符串 "\`\`\`lisp"
是以 \`\`\`
开头的,所以 string-match
的求值结果不是 nil
,否则是 nil
。对于 Elisp 解释器而言,非 nil
即为真,亦即若一个值即不是 nil
,也不是 '()
,那么无论它是什么,Elisp 都会将其等价于 t
。还记得吗,之前说过的,nil
与 '()
等价。要牢记住这些。事实上,上例的求值结果是 0,但 0 即不是 nil
也不是 '()
。
为什么上例的求值结果是 0 呢?因为 string-match
在字符串的开头就找到了与正则表达式相匹配的部分。字符串的开头,亦即字符串第一个字符的索引(或下标),它的值是 0。再看一个例子:
(setq r "```")
(setq x "foo```bar")
(string-match r x)
此时,string-match
是判断字符串 x
中是否存在与正则表达式 r
相匹配的文本,求值结果是匹配的文本的第一个字符的索引。由于在 x
里, \`\`\`
的首字符的索引是 3,所以上例里 string-match
的求值结果就是 3。这个求值结果的含义是,符合正则表达式 r
的的文本在 x
的第 4 个字符位置开始出现。
下面的这个例子,
(setq r "```$")
(setq x "foo```")
(string-match r x)
可以判断 x
是否以 \`\`\`
结尾。在正则表达式里,$
表示文本的结尾。
猜一下,^\`\`\`$
是什么意思?猜中了,虽然没有奖励,但可以确定自己并不笨。
现在,第二章的解析器程序里有关文本匹配的功能,便可以使用 string-match
代替了。至此,与该解析器有关的知识,均已普及。它所解决的问题,现在已不是问题了。我需要发现新的问题。
新的问题还是在 foo.md 文件里。下面仅给出它的部分内容:
# Hello world!
下面是 C 语言的 Hello world 程序源文件 hello.c 的内容:
```
#include <stdio.h>
... ... ...
```
... ... ...
其中,# Hello world!
是文档小节的标题。使用正则表达式 ^#
可以匹配它,但是抄录环境里也有以 #
开头的文本行。现在是不是有一些明白了,为什么从第二章到现在,我对 \`\`\`
开头的文本行如此有执念了吧?只有先识别出抄录环境,将它们忽略,方有足够的可能匹配文档小节的标题。至于如何忽略抄录环境里的文本,现在且放下。只需要记得,现在有了一个新的问题,而且接下来我也不知道还需要用几章能彻底解决它。
在忽略抄录环境的前提下,使用 ^#
可以匹配文档小节标题,但是它太粗糙了。因为,文档小节标题的真实样子可以是以下几种
# 标题
# 标题
# 标题
亦即,#
和标题的名字之间至少要有 1 个空格。此外,标题的名字之后也允许出现空格,比如输入标题时,不小心引入的。因此,对于匹配文档小节标题而言,更精确一些的正则表达式是
^#[[:blank:]]+.+$
其中,[[:blank:]]
可匹配空白字符,它涵盖了空格。+
表示位于它前面的字符可能存在 1 个或更多个。*
表示位于它前面的字符可能不存在,也可能存在 1 个或更多个。.
可匹配任意一个字符。因此 [[:blank:]]+
可匹配 1 个或更多个空格,.+
可匹配 1 个或更多个字符,而 [[:blank:]]*
可匹配 0 个,1 个或更多个空格。使用这个正则表达式,便可更为稳准地匹配文档小节标题了,例如:
(setq x "# Hello world! ")
(setq r "^#[[:blank:]]+.+[[:blank:]]*$")
(string-match r x)
string-match
的求值结果为 0,是正确的。现在可以思考,倘若自行定义一个类似功能的文本匹配函数,其工作量,以我现在的 Elisp 编程技能以及对 NFA(不确定的有穷自动机)的了解程度,不敢估计。
正则表达式不仅仅用于匹配,也能用于文本捕获。例如,从上述示例里的字符串 x
中捕获文档小节标题名 Hello world!
,对应的正则表达式应当写为
(setq r "^#[[:blank:]]+\\(.+\\)[[:blank:]]*$")
亦即,在正则表达式中使用 \\(
和 \\)
将要捕获的文本对应的正则表达式段 .+
包含起来。string-match
使用这个正则表达式进行文本匹配时,会将 \\(
和 \\)
包含的 .+
匹配到的文本段保存下来,需使用 (match-string 1)
提取。例如
(setq x "# Hello world! ")
(setq r "^#[[:blank:]]+\\(.+\\)[[:blank:]]*$")
(string-match r x)
(princ\' (match-string 1 x))
上述程序输出 Hello world!
。
match-string
的第 1 个参数是正则表达式中 \\(...\\)
的序号。因为一个正则表达式里可以有多处 \\(..\\))
,因此需在 match-string
中指定要获取的文本是哪一处 \\(...\\)
捕获的。
下面这个程序使用了两处正则表达式捕获
(setq x "############ Hello world! ")
(setq r "^\\(#+\\)[[:blank:]]+\\(.+\\)[[:blank:]]*$")
(string-match r x)
(princ\' (match-string 1 x))
(princ\' (match-string 2 x))
输出:
############
Hello world!
以上所述的仅仅是正则表达式的一些基本知识,因为当前的主要问题是如何在 Elisp 程序中使用正则表达式匹配文本。至于正则表达式本身的更多知识,可以在遇到实际问题时,临时抱抱佛脚 1。
下一章:缓冲区变换
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。