上一章:迭代html
在第二章「文本解析」所实现的解析器程序里,为了判断一行文本是否以 \`\`\`
开头,我定义了一个函数:node
(defun text-match (source target) (setq n (length target)) (if (< (length source) n) nil (string= (substring source 0 n) target)))
事实上,Elisp 提供了更强大的文本匹配函数。如何的强大呢?强大到了支持正则表达式匹配。正则表达式
正则表达式,就像古代官府捉拿江洋大盗时在城门边上张贴的通缉告示上的罪犯画像。罪犯的长相越有特色,他的画像便越有用处。我还以为现代的机器学习程序在识别照片里的人脸,其原理也像是在城门边上张贴通缉告示。编程
如何给一段文本画像呢?具体而言,如何给以 \`\`\`
做为开头的文本画像呢?很简单,只要像下面这样画segmentfault
^```
^
的意思是「开头」,后面紧跟着 \`\`\`
,就表示开头是 \`\`\`
。markdown
Elisp 的 string-match
函数能够用正则表达式构成的字符串对象去匹配另外一个字符串对象,例如:机器学习
(string-match "^```" "```lisp")
注意,为了便于讲述,从如今开始,诸如字符串对象(或字符串类型的实例),列表对象(或列表类型的实例),若没有特殊声明,通通简称为字符串、列表。应该不会致使误解。函数
上述示例中,因为字符串 "\`\`\`lisp"
是以 \`\`\`
开头的,因此 string-match
的求值结果不是 nil
,不然是 nil
。对于 Elisp 解释器而言,非 nil
即为真,亦即若一个值即不是 nil
,也不是 '()
,那么不管它是什么,Elisp 都会将其等价于 t
。还记得吗,以前说过的,nil
与 '()
等价。要牢记住这些。事实上,上例的求值结果是 0,但 0 即不是 nil
也不是 '()
。学习
为何上例的求值结果是 0 呢?由于 string-match
在字符串的开头就找到了与正则表达式相匹配的部分。字符串的开头,亦即字符串第一个字符的索引(或下标),它的值是 0。再看一个例子:code
(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。
下一章:缓冲区变换