《elisp 每日一练》正则搜索与 rx 宏

《elisp 每日一练》正则搜索与 rx 宏

Masimaro
2026-08-17 / 0 评论 / 0 阅读 / 正在检测是否收录...

看到这个标题我想读者可能有这样的疑问,前面已经介绍了正则表达式了,这次又来,不会是冷饭重炒没什么新东西了吧。实际上Emacs的正则表达式算是比较高级,操作相对复杂的部分了,一篇简单的函数介绍还不足以覆盖。所以在关于文本操作相关的主题中值得专门拿出一篇介绍一些新鲜内容。各位读者请放心往下看,绝不浪费时间。

背景知识铺垫

在前面我们介绍过,想要使用Emacs中的正则表达式,有下面一个函数:

  • re-search-forwardre-search-backward 用于搜索
  • match-string 用于提取搜索到的文本
  • replace-match 用于替换

但是Emacs 中使用正则表达式有一个比较坑的点是转义字符必须使用 \\ 来表示一个 \ , 这个细节如果不熟悉Emacs正则表达式的读者可能经常发生明明写了 \\([0-9]+\\) 进行查找结果就是什么都没有,你只能写下 \\\\([0-9]+\\\\) 。 这也太反人类了。

好在Emacs解决了这个问题,它提供了 rx 宏。rx宏使我们可以用S 表达式来组织正则表达式。本节我们的重点是介绍rx宏,以便摆脱正则表达式的纠缠。它可以使我们像写elisp代码一样来描述我们的匹配需求。

例如看到下面这个Emacs中的正则表达式:

"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]\\{2,\\}"

我们看到它前面匹配任意字符,中间加一个@符号,后面又是任意字符表示域名,而最后一个 . 后面接至少两个由A到Z组成的后缀。

而使用 rx 表达式则可以写成:

;; 使用 rx:可读性极大提升
(rx (one-or-more (any "a-z" "A-Z" "0-9" "._%+-")) ;; 第一部分这些任意字符,至少一个
    "@" ;; 第二部分是一个@固定字符
    (one-or-more (any "a-z" "A-Z" "0-9" ".-")) ;; 第三个部分又是这些字符里面任意字符, 至少一个
    "." ;; 第四部分是一个 .
    (repeat 2 nil (any "a-z" "A-Z"))) ;; 最后一个部分是英文字母但是至少两个

我们看到rx 表达式表示的是否更清楚呢。如果你的答案是肯定的,那么本篇的内容值得你继续往下阅读。

rx的定义如下

(rx REGEXPS...)

rx 接收一个或者多个 rx 形式的s表达式,这些形式最终会被翻译成普通的正则表达式字符串,下面按类别介绍最常用的构造。

  1. 字面量

字面量主要有字符串和字符,它们没有特殊含义就是表示字符本身。例如

(rx "hello")          ; → "hello"
(rx ?a)               ; → "a"
(rx "a.b")            ; → "a\\.b"(点被自动转义)
  1. 序列与选择
    我们可以通过 (seq FORM...)(sequence FORM...)(: FORM...)(and FORM...):按顺序匹配。也可以通过 (or FORM...) 或者 (| FORM...) 表示匹配其中的任意一个。

就像上面的例子那样,通过多个基础的rx 单元进行组合,拼接成一个完整的匹配项。又或者如下面的例子:

(rx (seq "http" (opt "s") "://"))   ; → "https?://"
(rx (or "cat" "dog" "bird"))        ; → "cat\\|dog\\|bird"
  1. 重复
    有下列方式表示重复:
  2. zero-or-more 表示0次或者多次
  3. one-or-more 表示至少出现一次
  4. zero-or-one 表示至多一次,或者说出现0次或1次
  5. repeat N 或者 = N 恰好出现N次
  6. >= N 至少N次
  7. ** N M N次到M次

rx 中重复默认都采用贪婪模式,如果想要使用非贪婪模式,可以加上 ?

  1. 字符匹配
    如果我们想要表示匹配对应集合中任意一个字符,那么可以提供集合,并且通过 (any SET...) / (in SET...) / (char SET...) 表示匹配集合中的任意一个字符。使用 (not CHARSPEC) 匹配不在集合中的字符

集合我们可以采用正则表达式中的表现方式例如: a-z 表示小写英文字母,0-9 表示数字。在rx中也可以使用预定义的集合:

  • digitnumericnum 表示数字
  • alphaalphabeticletter 表示字母
  • alnumalphanumeric 表示数字+大写字母+小写字母的序列
  • xdigithex-digit 表示16进制数
  • blankspace / whitespace 表示空白字符
  • wordwordchar 表示单词
  1. 位置匹配
    如果我们想在上述匹配基础上加上位置,可以考虑使用下面列举的一些内容:
  • line-startbol:行首(^)
  • line-endeol:行尾($)
  • string-startbosbuffer-start:字符串/缓冲区开头(\`)
  • string-endeosbuffer-end:结尾(\')
  • word-startbow:单词开头(\<)
  • word-endeow:单词结尾(\>)
  • word-boundary:单词边界(\b)
  • not-word-boundary:非单词边界(\B)
  • symbol-startsymbol-end:符号边界
  • point:当前 point 位置(\=)
  1. 捕获组
    对于捕获组,有如下方式:
  • (group FORM...)(submatch FORM...) : 对应着正则表达式中的捕获组 \(...\)
  • (group-n N FORM...) :命名编号的捕获组
  • (backref N):反向引用第 N 个组

对于 rx 更加详细的内容,读者可以通过 describe-function 查阅有关 rx 的内容。另外我们也可以通过 rx-to-string 来将一个rx 表达式转化为正则表达式.

练习

本节的练习,我们利用rx表达式来实现一个将 markdown 中的标题修改为 org mode 中标题的小工具。为了实现这个转换,我们大致需要经历这么几步:

  • 通过正则表达式找到 markdown 的标题行
  • 获取匹配到的 # 数量以便确定是几级的标题
  • 根据#数量替换成等数量的 *

根据上面步骤,我写下如下的代码:

(defun my/convert-md-tilte-to-org()
  (interactive)
  (save-excursion
    (save-restriction
      (widen)
      (goto-char (point-min))
      (let ((count 0)
        (header-regexp (rx
                line-start (group (repeat 1 6 "#")) (one-or-more space))) ;;位于行首,捕获组1的特征是,# 重复1到6次,后面跟一个或者多个空格
        )
    (while (re-search-forward header-regexp nil t)
      (let* ((hashes (match-string 1)) ;; 从捕获组1当中提取匹配到的#字符串
         (has-count (length hashes)) ;; 计算#数量
         (starts (make-string has-count ?*)) ;; 生成对应数量的 * 字符串
         (replacement (concat starts " ")) ;; 组装org mode 标题行
         )
        (replace-match replacement t t)
        (setq 1+ count)))
    (if (> count 0)
        (message "共找到%d处并替换" count)
      (message "未找到对应的标题行"))))))

小结

本节内容主要介绍了通过 rx 来生成生成正则表达式,采用这种方式的好处在于正则表达式代码容易理解,不容易出错,而且还可以带上注释,更进一步降低正则表达式的阅读门槛

0

评论 (0)

取消