和坛友分享下「日本語非辞書形辞典」的项目的最新成果:Mikann,专为辞典检索设计,基于语义的高精度日语分词器。
建议点击这里阅读原文。
如果链接失效,请用「开心」「啃生肉」「演讲稿」等关键字搜索。
这里是我演讲稿,关于 PyCon China 2025 的参加报告,请参考[[PyCon It’s MyGo:我的 PyCon China 2025 参加报告]]。
题图:
![[pycon-china-2025-proposal.png|360]]
我从来没觉得啃生肉开心过:用 Python 打造日语泛读利器
摘要
和英语不同,日语不会用空格区分单词,再加上单词变形复杂,初学者在刚开始泛读时往往要花费大量时间才能准确判断一句话中想查单词的原型。通过形态素解析器分析文本,我们其实能够获取单词原型,这可以有效提高泛读效率。但是,现有的解析器处理字幕、漫画和 Galgame 等口语化的文本时会出现较为明显的未登录词(Out-of-Vocabulary, OOV)问题。本次分享除了介绍 Python 调用 Sudachi 的方法,还会重点介绍如何解决这个问题。
下面是发表用的PPT:[[PyCon China 2025-卿学童-我从来没觉得啃生肉开心过:用 Python 打造日语泛读利器.pdf]]
视频
- 补充B站视频 [[2025-10-16]] 未公开
正文
(观察下会场的气氛)我听到刚才有同学在笑了,我知道你们在期待什么,放心,我不会让你们失望的!
自我介绍
首先简单自我介绍一下,大家好,我姓卿,我在社交平台上经常用的 ID 来自一位我很喜欢的日本作家「清少纳言」。我目前在日本的一家外包公司工作,主要是用 Java 写 Web 网页(但Python 才是我的真爱),后面我就不念了,下面就进入正题233。
引入
既然大家来听这个Talk,那应该或多或少都对日语有点兴趣,而且说不定还有一个的小目标:「能轻松啃生肉就好了」。
这个听起来好像不是那么简单,但其实……也确实有一定难度。比如,今年我看到一个帖子,一大群 V 友吐槽「日语怎么这么难学」。
[[V2ex:没有人觉得日语的学习难度很大吗?]] https://www.v2ex.com/t/1138764
除了这篇帖子里提到的各种问题,还有一个比较重要的点:「查日语单词其实是个技术活儿」。
为什么这么说呢?这个帖子吐槽了「日语单词不空格」的问题,大家知道(すもももももももものうち)该怎么断句吗。
如果每次查单词,都要猜到底哪几个假名是一个单词,才开始学日语的同学估计查着查着心态就崩了。
另外,还有个比较关键的问题:虽然日语里有大量汉字,但我们并不熟悉这些汉字的日语发音,这会让我们查单词的时候,更加困难。比如,我们可以看到,这个同学一开始查不到就是因为打错了这个单词的读音,但是,后面直接复制这个单词一下子就查到了。
这给了我们一点灵感:我们是不是可以更进一步写这样一个程序:当用户点击的时候,直接分析出点击位置附近的单词原型,然后直接调用辞典程序—这听起来好像能大幅提高我们的学习效率。
实际上已经有很多沿着这个思路做的工具了,我个人用得最多,也是最想推荐给大家 ,就是这个 jidoujisho(暂无官方中文译名…自动辞书?),之所以最推荐它,一个原因是它不仅支持电子书和漫画,还支持边看动漫边查字幕里的单词(注:个人认为,动漫字幕是最适合日语学习者的泛读入门材料),另一个原因是这个项目直接内置了形态素解析器,解析效果相对来说稍微好点,但是,也有不少问题。
基本操作
首先,我们简单就来简单实现一个的 DEMO,然后再说说到底存在哪些问题。
# 安装依赖
pip install sudachipy sudachidict_full
由于前面已经有 Talk 介绍过 Sudachi ,所以我就不过多介绍,直接给出最关键的代码。
from typing import List
from sudachipy import Dictionary
def analyze_text(text: str) -> List[List[str]]:
tokenizer = Dictionary(dict="full").create()
result_list = []
for token in tokenizer.tokenize(text):
result = [token.surface(), token.normalized_form()]
result_list.append(result)
return result_list
print(analyze_text("晩ご飯を食べましたか。"))
# [["晩ご飯", "晩御飯"], ["を", "を"], ["食べ", "食べる"],["まし", "ます"], ["た", "た"], ["か", "か"], ["。", "。"]]
function getCursorResult(analysisResult, cursorIndex) {
// 计算所有表层形的总长度
const totalLength = analysisResult.reduce((sum, result) => sum + result[0].length, 0);
// ... 省略 cursorIndex > totalLength 和 cursorIndex < 0
let lengthBeforeCursor = 0;
for (const result of analysisResult) {
const [surface, dictinary_word] = result;
lengthBeforeCursor += surface.length;
if (lengthBeforeCursor >= cursorIndex) {
return dictinary_word;
}
}
return null;
}
注:完整代码请参考[[FastMikannAPI]] https://fast-mikann-api.vercel.app/。
大家可能会好奇,为什么要用「[["晩ご飯", "晩御飯"], ["を", "を"], ["食べ", "食べる"],["まし", "ます"], ["た", "た"], ["か", "か"], ["。", "。"]]
」这种冗余的格式返回数据?
这是因为,如果文本「不变」,前端就可以用「一次」解析结果,通过索引位置返回最靠近用户点击位置的单词原型。
看起来我们已经完美实现了我们想要的功能,下面,我们来做一个比较有挑战性的测试:
from typing import List
from sudachipy import Dictionary
def analyze_text(text: str) -> List[List[str]]:
tokenizer = Dictionary(dict="full").create()
result_list = []
for token in tokenizer.tokenize(text):
result = [token.surface(), token.normalized_form()]
result_list.append(result)
return result_list
print(analyze_text("なんで『春日影』やったの!?"))
# [['なん', '何'], ['で', 'で'], ['『', '『'], ['春日', '春日'], ['影', '影'], ['』', '』'], ['やっ', '遣る'], ['た', 'た'], ['の', 'の'], ['!', '!'], ['?', '?']]
大家可能会奇怪:为什么用「春日影」作测试.jpg?(恼)
懂日语的同学可能已经注意到了,这句话里「春日影」解析结果不对,解析成「春日」和「影」了。
至于为什么会解析成这样,这就涉及到 Sudachi 的原理。简单理解的话,解析器高度依赖「指定词典」,如果某个词不在这个辞典里,那解析结果很有可能就会错,而中文信息处理也有这个的问题,也就是(未登录词)。
有趣的是,虽然目前 SudachiDict 词库里确实没有这个词,但不少日语词典,比如《大辞泉》就收录「春日影」了。当然,收录不是大家想的那个意思,就是字面的意思,「春日的阳光」(笑)。
如何添加未登录词
那如何解决这个问题呢?
官方文档[[Sudachi 自定义词典说明书]] 提供了详细的说明,我们只需要以下面的格式将未登录词的相关信息整理成 csv 文件,然后通过指令生成词库即可。
# 見出し (TRIE 用),左連接ID,右連接ID,コスト,見出し (解析結果表示用),品詞1,品詞2,品詞3,品詞4,品詞 (活用型),品詞 (活用形),読み,正規化表記,辞書形ID,分割タイプ,A単位分割情報,B単位分割情報,※未使用
春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,*
大家可能看不懂这个,但没关系,绝大多数时候,我们都是添加有名词,所以重点关注「 見出し (TRIE 用)」和「読み」(读音)即可,其他配置基本可以照着抄。
但如果公司业务比较特殊,需要登录名词之外的词,大家就要重点关注最前面的三个数字,具体我稍后解释,我们先来看看效果。
sudachipy ubuild -s system_full.dic -o user.dic user.csv
备注: system_full.dic 可以从 SudachiDict下载,也可以从 pip install 安装路径复制。
参考路径: .venv/lib/python3.13/site-packages/sudachidict_full/resources
通过这条指令,我们在指定的路径下生成一个名叫 user.dic
的文件,然后,调用 Sudachi 时,我们也要告诉它:除了官方提供的词库,还要加载我们自定义的词库:
from typing import List
from sudachipy import Dictionary
def analyze_text(text: str) -> List[List[str]]:
# 就是用 config='{"userDict": ["user.dic"]}'指定
tokenizer = Dictionary(dict="full", config='{"userDict": ["user.dic"]}').create()
result_list = []
for token in tokenizer.tokenize(text):
result = [token.surface(), token.normalized_form()]
result_list.append(result)
return result_list
print(analyze_text("なんで『春日影』やったの!?"))
# [['なん', '何'], ['で', 'で'], ['『', '『'], ['春日影', '春日影'], ['』', '』'], ['やっ', '遣る'], ['た', 'た'], ['の', 'の'], ['!', '!'], ['?', '?']]
很好!现在,「春日影」能被正确的解析了!
当然,日语比较好的同学可能会发现,前面的「なんで」是不是也解析错了,现在这个词被拆成了「なん」和「で」,没错,所以这个词也需要我们手动整理添加。但其实,这句话还有一个未登录词——「やった」。
なんで,4,4,-32768,なんで,副詞,*,*,*,*,*,ナンデ,なんで,*,*,*,*,*
春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,*
未登录词的分类
不懂日语的同学可以把这个地方的「やった」简单理解成英语的「did」,也就是「do」的过去式就好了。
Sudachi 确实是收录了表示「做」的这个意思词条,但问题在于日语的「やった」其实还可以表示感叹(幻灯片右边的 やった的日语解释),大家可以简单理解成汉语的「好耶」。
但是,这个词是「连语」,而 Sudachi 词典没有这个分类,那我们该怎么办呢?
懂日语的同学可能会说,这个词没有活用,不如就把这个词当作「名词」吧。听起来不错,我们来试试看。
春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,*
やった,5146,5146,-32768,やった,名詞,普通名詞,一般,*,*,*,ヤッタ,やった,*,*,*,*,*
很不幸,如果我们这么做,这个句子是对了,但前面的句子就会错,因为第一个句子里的「やった」不是名词,就是动词。
那怎么办呢?其实,如果大家翻下其他词典,比如 MOJi 对「やった」分类,我们会发现一个有趣现象:除了《大辞泉》,市面上的日语词典对这个词的分类都是「感动词」,而这个分类, Sudachi 正好也有,我们以这个分类登录进去试试。
なんで,4,4,-32768,なんで,副詞,*,*,*,*,*,ナンデ,なんで,*,*,*,*,*
春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,*
やった,5687,5687,-32768,やった,感動詞,一般,*,*,*,*,ヤッタ,やった,*,*,*,*,*
很好!我们可以发现两句话的解析结果都对的了。
大家可以鼓掌庆祝一下,到目前为止,通过这句很短的话,我们就发现并整理了三个未登录词。这是我们的一==大==步,但却只是 Sudachi 的一==小==「步。
未登录词的数量
为什么这么说呢?通过分析比较 Suadchi 和《大辞泉》的全部词条,我们发现:尽管目前 Sudachi 的词库已经收录了近两百万的单词,但《大辞泉》的近三十万词条中仍有近 1/3 未被收录,这意味着我们刚才的操作只是将解析精度提高了 0.002%(百分之零点零零二)。
有同学可能会好奇,为什么会有近 1/3 的词条都没有被收录? Sudachi 词库是不是不行?其实不是,Sudachi 的词库已经是目前的开源项目中,收词量最大的了。
之所以会有这个现象,一方面是因为日语的特点决定了各行各业都会有大量专有名词,而且每天都不停的产生新词,专业的自然语言处理团队应该通过分析日志或者其他手段,找出这些未登录的词条(切幻灯片);另一方面 Sudachi 词库受日本国立国语研究所的 UniDic 项目的影响非常大,而 UnDic 项目的初衷是面向语料库等学术研究的场景,收词原则和大多数人理解的会有一定的区别,比如,它的单词词性体系和《大辞泉》就不太一样,而如何处理这些分类不一样的单词是个非常麻烦的问题。
注:如果你对这部分内容有兴趣,请查看[[我的 YANS 2025 发表]]。
为什么要查春日影?
最后,回到最初的问题:为什么我要专门用「春日影」来做测试呢?
因为这句话里的几乎每个「词」都是「未登录词」,而且还有「最棘手」的那种,也就是像「やった」这样分类本身就有一定争议的词,这些词很可能存在多种理解,所以登录时需要大量测试覆盖边界情况。所以,在最后,有件事想拜托大家:
如果大家发现 Sudachi 有解析不对的句子,欢迎分享给我,我会一个一个整理的。另外,也求求大家去看摇曳露营吧,我什么都愿意做的.jpg。
以上就是我演讲的全部内容了,谢谢大家(手动滑稽)!
彩蛋
很高兴你愿意看到最后,也许你已经发现了:Sudachi 没有收录任何「惯用句」!(不懂日语的同学可以理解成英语的短语搭配)。
如果有机会,我会在明年(2026年)的 PyCon 上分享如何解决这个问题,期待明年与你见面(笑)。
提前剧透:词典收录的惯用句和实际使用时长得可能不太一样:除了后项动词的活用,还会有下面这些复杂的变形:
![[GBC 惯用句]]