EMT - 使用 macOS 内建的分词器对 CJK 进行分词

EMT stands for Emilia maji tenshi Emacs MacOS Tokenizer.

录屏2023-11-14 11.16.19

录屏来自群友

EMT 通过 dynamic modules 调用 macOS 内建的分词器对 CJK 进行分词,并支持 CJK 文本的 forward-word, backward-word, kill-word, backward-kill-wordmark-word

EMT 提供了几个方便使用的 API:

emt-word-at-point-or-forward

返回光标所在位置的单词。如果当前光标位于单词边界,则返回后面的一个单词。

emt-word-at-point-or-backward

返回光标所在位置的单词。如果当前光标位于单词边界,则返回前面的一个单词。

emt-ensure

加载动态模块。

emt-split

将字符串拆分为单词列表。

返回一个cons列表,每个cons包含一个单词及其边界(单词的起始位置和结束位置的cons)。

emt-split-without-bounds

将字符串拆分为单词列表。只返回一个单词列表。比 emt-split 更快。

安装

下载 Releases · roife/emt · GitHub 中的 pre-built dynamic module,放在 ~/.emacs.d/modules/libEMT.dylib 下面。然后就可以用 emt-ensure 加载 dynamic module 了;或者直接打开 emt-mode,它会将 forward-word 等函数自动映射到 EMT 实现的版本

14 个赞

不知 macOS 10.13 内建的分词功能如何调用,系统目录下倒是有个中文分词的 lib:

$ nm -gU /usr/lib/libChineseTokenizer.dylib
00000000000032d5 T _ChineseTokenizerAdvanceToNextToken
0000000000003228 T _ChineseTokenizerCreate
00000000000032e8 T _ChineseTokenizerGetCurrentTokenRange
00000000000032fd T _ChineseTokenizerGoToTokenAtIndex
00000000000032b2 T _ChineseTokenizerRelease
00000000000033ba T _ChineseTokenizerSetCustomWordCheckBlock
0000000000003310 T _ChineseTokenizerSetDynamicLexicon
00000000000032c4 T _ChineseTokenizerSetString
000000000000cc00 D __ZTINSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEEE
000000000000cb60 D __ZTINSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE
000000000000a800 S __ZTSNSt3__115basic_stringbufIcNS_11char_traitsIcEENS_9allocatorIcEEEE
000000000000a7b0 S __ZTSNSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE

NLTokenizer 的 API 应该是稳定的,只是 swift 的 dynamic module binding 只支持 10.15 or later,所以我被迫提升了版本😂

等我抽空整个 ObjC 的 binding 就不用整 swift dynamic module 这么麻烦

1 个赞

看了下代码,API 设计得感觉不太好,只是用来光标移动的话不需要返回分拆的词组,这样反而对 Emacs 产生不必要的內存压力。

emt--do-split-without-bounds-helper

It is faster than `do-split’ while it does not calculate bounds.

认真的?你的 Swift 代码用的都是 self.tokenizer.tokens,我觉得差不了多少,真要区別可能就是 consing 多少。

我重新设计了下 dynamic module 的 API,想要得到分词结果可以在 emacs lisp side 用 substring 实现,emacs lisp 也还有一些地方可以改进。ObjC 代码我放在 GitHub - LdBeth/emt: Emacs macOS Tokenizer, tokenizing CJK words with macOS's built-in NLP tokenizer.

(load-file "libEMT.dylib")
(emt--tokens-range-helper "你好我好大家好")
;; [(0 . 1) (1 . 2) (2 . 3) (3 . 4) (4 . 6) (6 . 7)]
(emt--token-range-at-index-helper "What the fuck is this" 5)
;; (5 . 8)

一时半会没时间改 emacs lisp 代码, @twlz0ne 可以帮忙看下。

1 个赞

确实没有必要返回计算的字符串,可能是因为一开始就想着做分词,所以做 API 的时候就绕进去了。

without-bounds 的版本设计之初是因为考虑到swift的字符串不能随机访问,计算 bounds 需要额外计算一次遍历,所以会有一定性能差异,并且实际 benchmark 测试里也表现出了一定的性能差异,考虑到可能有些情况下不需要边界因此保留。(事实上也没有在包里用到,当时考虑到其他人需要做分词可能会用到就保留了)

不过考虑到只需要返回边界的话,那确实不再需要这个 API 了😂。考虑到 emacs 的 dynamic module 处理字符串的开销不小,或许这样能带来一些加速。

另外好奇同样的逻辑 ObjC 的实现能比 swift 快多少

快应该不会快多少,但体积绝对是能小不少的,默认参数编译的动态库只要 49K,是 swift 的十分之一。

而且我用 ObjC 写 emacs dynamic module 最大的理由是不想装 Xcode,有 CommandLineTools 就行。

3 个赞

已经按照这个 API 实现了新版本的 module 和 el

2 个赞

试用一下,提个小建议,可以加个命令,在 Emacs 里就能更新 lib 就方便不少

1 个赞

我在考虑改进自动编译和自动下载包,以及检测module和包版本是否匹配

使用emt-backward-kill-word,会显示Symbol’s value as variable is void: emt–cache-lru-size。

已经修复 ((

感谢大佬,紫薯补丁

1 个赞

更新了,用 emt-download-module。开启 mode 后如果检测不到 module 也会询问是否自动下载

2 个赞

如果有问题或者想要的 feature,推荐大家优先去 GitHub 提,防止老帖被不断顶起来 :wink: