通过 find-word-boundary-function-table 进行中文分词

Emacs 内的中文分词方案已经有不少,但几乎都需要重绑定基础命令,覆盖难免不全,在 words count 等基于 forward-word 的场景更是无能为力。而实际上 Emacs 早在 23 版本就加入了 find-word-boundary-function-table 来配置单词边界的查找方法,曾同样依赖重绑键位的 subword 之后就是用此 char-table 重写的,以实现直接作用于所有 word motion。

find-word-boundary-function-table 中的函数接收 POS 、 LIMIT 两个参数, POS 小于 LIMIT 时返回词的结束位置,否则返回词的起始位置。

我之前一直在使用 kanglmf/emacs-chinese-word-segmentation,本文以此为例提供一种实现思路,所有类似的分词包应该都可复用。

;; 配置好 cns.el 之后
(defconst cns-boundary-table
  (let ((tab (make-char-table nil)))
    (map-char-table
     (lambda (key value)
       (when (aref value ?C)
         (set-char-table-range tab key #'cns-boundary-find-word-boundary)))
     (standard-category-table))
    tab)
  "Char table assigned to `find-word-boundary-function-table' in
`cns-boundary-mode' buffers.")

(defun cns-boundary-han-run-at (pos)
  "Return (START . END) of the maximal run of HànZì around POS."
  (save-excursion
    (goto-char pos)
    (cons (if (re-search-backward "\\CC" nil t) (1+ (point)) (point-min))
          (progn (goto-char pos)
                 (if (re-search-forward "\\CC" nil t) (1- (point)) (point-max))))))

(defun cns-boundary-word-boundaries (pos)
  "Return the ascending list of absolute word boundary positions of
the HànZì run around POS.  e.g. (1 3 5) for \"中文分词\"."
  (let* ((run (cns-boundary-han-run-at pos))
         (start (car run))
         (end (cdr run))
         (segs (cns-segmentation-handler
                (buffer-substring-no-properties start end))))
    (let ((bounds (list start)))
      (dolist (len segs)
        (push (+ (car bounds) len) bounds))
      (setq bounds (nreverse bounds)))))

(defun cns-boundary-find-word-boundary (pos limit)
  "Find the Chinese word boundary near POS bounded by LIMIT.."
  (let ((bounds (cns-boundary-word-boundaries pos)))
    (if (< pos limit)
        ;; POS is at the first character of a word.
        (cl-find-if (lambda (b) (> b pos)) bounds)
      ;; POS is at the last character of a word.
      (car (last (cl-remove-if (lambda (b) (> b pos)) bounds))))))

(defun cns-boundary-mode-enable nil
  "Enable `cns-boundary-mode'."
  (unless (process-live-p cns-process)
    (cns-start-process))
  (cns-buffer-list-add-buffer)
  (setq-local find-word-boundary-function-table cns-boundary-table)
  (add-hook 'kill-buffer-hook #'cns-boundary-mode-disable nil t))

(defun cns-boundary-mode-disable nil
  "Disable `cns-boundary-mode'."
  (cns-buffer-list-delete-buffer)
  (kill-local-variable 'find-word-boundary-function-table)
  (when (and (process-live-p cns-process) (= 0 (length cns-buffer-list)))
    (cns-stop-process))
  (remove-hook 'kill-buffer-hook #'cns-boundary-mode-disable t))

(define-minor-mode cns-boundary-mode
  "Toggle Chinese word segmentation mode in current buffer.
Unlike `cns-mode', this mode rebinds no keys."
  :group 'cns
  (if cns-boundary-mode (cns-boundary-mode-enable) (cns-boundary-mode-disable)))

(define-globalized-minor-mode global-cns-boundary-mode cns-boundary-mode
  (lambda nil (cns-boundary-mode 1))
  :group 'cns)

基本就是通过 cns-segmentation-handler 获取分词结果,再想办法塞进 find-word-boundary-function-table 里,然后包括 forward-word 、 makr-word 在内的原生命令就都获得了中文分词能力。代价是与其他分词模式———实际上也只有 subword-mode 冲突。

不过仍有部分命令,例如 count-words 使用的是 forward-word-strictly ,会故意遮蔽掉这个 char-table,只能重写。

(defun my-count-words-region (start end &optional totals)
  "Count the number of words in the region.
With prefix ARG, report for the entire buffer rather than the region."
  (interactive (if current-prefix-arg
                   (list nil nil current-prefix-arg)
                 (list (region-beginning) (region-end) nil)))
  (cl-letf (((symbol-function 'forward-word-strictly) #'forward-word))
    (if totals
        (message "%s" (count-words--buffer-format))
      (message "%s" (count-words--format
                     "Region" (region-beginning) (region-end))))))

(keymap-global-set [remap count-words-region] #'my-count-words-region)

Date: 2026-10-17 Sat 01:24