Elisp 05:文本匹配

上一章:迭代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

下一章:緩衝區變換


  1. https://www.gnu.org/software/...
相關文章
相關標籤/搜索