关于lisp:Elisp-08文本跨行提取

6次阅读

共计 4991 个字符,预计需要花费 13 分钟才能阅读完成。

上一章:命令行程序界面

在上一章的结语里,我说这个教程是否会有第二局部,取决于我是否遇到了新的文本处理问题。后果很快如愿以偿。

问题

上面是 XML 文件 foo.xml 的内容:

<bib>
  <title>foo</title>
</bib>
<attachment>
  <resource src="files/foo.html"/>
  <title>foo</title>
</attachment>
<bib>
  <title>bar</title>
</bib>
<attachment>
  <resource src="files/bar.html"/>
  <title>bar</title>
</attachment>

我须要从 <attachment>...<attachment> 块里提取以下条目:

<resource src="files/foo.html"/>
<title>foo</title>
<resource src="files/bar.html"/>
<title>bar</title>

文本跨行匹配

当初假如已用 Elisp 函数 find-file 将 foo.xml 文件内容全副载入了缓冲区,即

(find-file "foo.xml")

而后发现,之前学过的 Elisp 常识简直派不上用场了。之前学过的文本匹配和提取办法仅实用于单行文本,而当初面临的问题是多行文本的匹配和提取,即从以后缓冲区内提取

<attachment>
  <resource src="files/foo.html"/>
  <title>foo</title>
</attachment>
<attachment>
  <resource src="files/bar.html"/>
  <title>bar</title>
</attachment>

莫说提取,仅仅是如何匹配 <attachment>...</attachment> 块就曾经不好解决了。例如,以下程序

(find-file "foo.xml")

(let ((x (buffer-string)))
  (string-match "<attachment>\\(.+\\)</attachment>" x)
  (princ\' (match-string 1 x)))

输入 nil,意味着 string-match 在以后缓冲区内容中匹配 <attachment>...</attachment> 块失败。导致失败的起因也很简略,因为正则表达式 . 尽管能够匹配任意一个字符,但它不包含换行符。

瞒天过海

实现文本的跨行匹配,并非不可行,然而须要比当初更多的 Elisp 的正则表达式常识 1。然而,我想说的是,对于上述问题,现有的 Elisp 常识其实也是足够用,只须要转换一下思路。

文本为什么是多行的?是因为在输出文本的时候,每一行开端由人或程序增加了换行符。假使能将这些换行符长期替换为一个很非凡的记号,那么多行文本就变成了单行文本。在文本匹配和解决完结后,再将这个非凡记号再替换为换行符,单行文本又还原为多行文本。此为瞒天过海之计。

将以后缓冲区内所有的换行符替换为一个非凡记号,可基于第 6 章所讲的缓冲区变换办法予以实现。本章给出一个更快捷的办法。Elisp 函数 replace-string 可在以后缓冲区内应用指定字串替换所有指标字串,例如

(let ((x "")
      (y "")
      (one-line (generate-new-buffer "one-line")))
  (find-file "foo.xml")
  (setq x (buffer-string))
  (with-current-buffer one-line
    (insert x)
    (goto-char (point-min))
    (replace-string "\n" "<linebreak/>")
    (setq y (buffer-string)))
  (princ\' y))

执行上述程序后,在新创建的缓冲区 one-line 里寄存的便是 foo.xml 缓冲区的单行化后果。假使将上述代码里的 (princ\' y) 语句替换为

(string-match "<attachment>\\(.+\\)</attachment>" y)
(princ\' (match-string 1 y))

便可提取 <attachment>...</attachment> 块,只管提取的后果是错的。

为了更不便察看谬误,须要结构一个简略的例子:

(setq x "abcabcabc")
(string-match "a\\(.+\\)a" x)
(princ\' (match-string 1 x))

这个例子会输入什么呢?尽管我很冀望它输入 bc,但事实上它输入的是 bcabc。这是因为 + 是很贪心的,它总是心愿能匹配最长的后果,而不是最短的。* 也是如此。在 Elisp 的正则表达式里,在它们的前面加一个 ?,便能够克制它们的贪心,例如

(setq x "abcabcabc")
(string-match "a\\(.+?\\)a" x)
(princ\' (match-string 1 x))

此时,程序的输入后果便是 bc 了。

递增搜寻

Elisp 函数 re-search-forward 能够在缓冲区内搜寻与正则表达式匹配的文本的同时,将插入点挪动到缓冲区的匹配地位。基于该函数,再借助 Elisp 正则表达式的文本捕捉性能,便可从上一节结构的 one-line 缓冲区内提取多个 <attachment>...</attaqchment> 块了。

为了演示 re-search-forward 的用法,我将上一节的那段示例代码革新为以下代码:

(let ((x "")
      (one-line (generate-new-buffer "one-line"))
      (output (generate-new-buffer "output")))
  (find-file "foo.xml")
  (setq x (buffer-string))
  (with-current-buffer one-line
    (insert x)
    (goto-char (point-min))
    (replace-string "\n" "<linebreak/>")
    (goto-char (point-min))
    (while t
      (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1)
          程序分支 1
        程序分支 2))))

re-search-forward 是迄今为止我用过的最为简单的 Elisp 函数了,它有 4 个参数,但只有第 1 个参数是必须的,其余 3 个参数皆为可选——假使不设定它们的值,re-search-forward 会应用它们的默认值。这 4 个参数释义如下:

  • 第一个参数,是用于文本匹配的 Elisp 正则表达式。
  • 第二个参数,用于设定最大搜寻范畴。因为 re-search-forward 是在以后缓冲区内进行文本匹配搜寻,搜寻的起始地位是插入点所在位置,终止地位可通过它的第二个参数设定,若该参数值为 nil,则将以后缓冲区的止境作为搜寻范畴的终止地位。
  • 第三个参数值若为 nil,在未搜寻到匹配文本时,re-search-forward 便会报错。若该参数值为 tre-search-forward 会返回 nil。若该参数值即不是 nil,也不是 t,则 re-search-forward 函数将插入点挪动到搜寻区域的止境,而后返回 nil
  • 第四个参数 CUNT,可令 re-search-forward 的搜寻过程维持到第 COUNT 次匹配后完结,假使未设定这个参数,其值默认为 1。

若充沛了解了 re-search-forward 函数的用法,则上述代码虚设的程序分支 1 对应的代码便可写进去了,不再须要新的 Elisp 常识,即

(let ((y (match-string 1)))
  (with-current-buffer output
    (insert (concat y "\n"))))

就是将 re-search-forward 捕捉的文本用 match-string 函数取出后插入 output 缓冲区。在此须要留神,若正则表达式捕捉的文本属于以后缓冲区,match-string 函数无需写第 2 个参数。

对于程序分支 2,即 re-search-forward 匹配失败状况的解决,现有的 Elisp 常识是真的不够用了。因为该程序分支属于一个有限迭代过程,要从后者跳出,须要像其余编程语言那样,须要有 returnbreak 语法,可提前终止迭代过程。

catch/throw

Elisp 语言没有 returnbreak,然而它有 catch/throw 表达式。

上面的示例

(catch 'foo
  (princ\'"foo")
  (princ\'"bar"))

可输入

foo
bar

当初,假使我将上述代码批改为

(catch 'foo
  (princ\'"foo")
  (throw 'foo nil)
  (princ\'"bar"))

那么位于 throw 表达式之后的代码便会被 Elisp 解释器疏忽,因此当初的代码只能输入

foo

假使将上述代码批改为

(princ\'(catch'foo
           (princ\'"foo")
           (throw 'foo nil)
           (princ\'"bar")))

输入后果则变为

foo
nil

因为 throw 表达式的第 3 个参数 nilcatch 表达式的求值后果。

catch/throw 在 Elisp 语言里称为「非本地退出」,基于它们便可模仿其余编程语言里的 returnbreak 以及异样机制。

基于 catch/throw,便可实现上一节所述的程序分支 2 了,例如

(throw 'break nil)

而后只需将 while 表达式放在 catch 块里,即

(catch 'break
  (while t
    (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1)
        程序分支 1
      (throw 'break nil))))

复原多行文本

当初,以下代码

(let ((x "")
      (one-line (generate-new-buffer "one-line"))
      (output (generate-new-buffer "output")))
  (find-file "foo.xml")
  (setq x (buffer-string))
  (with-current-buffer one-line
    (insert x)
    (goto-char (point-min))
    (replace-string "\n" "<linebreak/>")
    (goto-char (point-min))
    (catch 'break
        (while t
          (if (re-search-forward "\\(<attachment>.+?</attachment>\\)" nil t 1)
              (let ((y (match-string 1)))
                (with-current-buffer output
                  (insert (concat y "\n"))))
            (throw 'break nil))))))

已根本解决本章开始所提出的问题了,因为 output 缓冲区内寄存着从 foo.xml 文件里提取的两个 <attachment>...</attachment> 块,接下来,我只需将其中的 <linebreak/> 替换为 \n,问题便齐全解决了。然而,我感觉这个工作能够留作本章习题。

save-excursion

在以后缓冲区内,insertreplace-string 以及 re-search-forward 等函数,皆有副作用,它们会挪动插入点。在文本处理时,要记住以后的插入点所在的地位,而后调用这些函数之后,须要再将插入点复原原位。这是后面几节代码屡次呈现

(goto-char (point-min))

的次要起因。Elisp 提供了 save-excursion 语法,它能够主动将插入点的地位保留下来,而后执行一些可能会挪动插入点的运算,最初再将插入点复原原位。例如

(save-excursion
  (insert x))

(let ((p (point)))
  (insert x)
  (goto-char p))

等价。

因而,本章第二个习题是,基于 save-excursion 语法批改上一节习题的答案。

结语

本章介绍了 Elisp 缓冲区里更多的运算以及非本地退出语法。把握了这些常识,可从任何文本文档内提取合乎模式的由多行文本形成的文本块。


  1. https://www.emacswiki.org/ema…
正文完
 0