这里是我演讲稿,关于 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 下面是发表用的PPT:[[PyCon China 2025-卿学童-我从来没觉得啃生肉开心过:用 Python 打造日语泛读利器.pdf]] [SpeakDeck 备份](https://speakerdeck.com/noheartpen/wo-cong-lai-mei-jue-de-ken-sheng-rou-kai-xin-guo-yong-python-da-zao-ri-yu-fan-du-li-qi) ## 视频 - [ ] 这是 PyCon China 官方录制的演讲视频。 注:由于我今年参加的是闪电演讲,录屏不会把演讲者本人录进去,所以如果你是想看我一脸奸笑地反复迫害素世,那还是等明年吧~~大雾~~。 ## GitHub - [ ] 这里提供了一个开箱即用的仓库。 ## 正文 (观察下会场的气氛)我发现有同学听到演讲标题就笑出来了,我知道你们在期待什么,放心,我不会让你们失望的! ## 自我介绍 首先简单自我介绍一下,大家好,我叫卿学童。我在社交平台上经常用的 ID 来自一位我很喜欢的日本作家「清少纳言」。 我目前在日本的一家外包公司工作,主要是用 Java 写 Web 网页~~但Python 才是我的真爱!~~,后面我就不念了,直接进入正题吧233。 ---- ## 引入 既然大家来听这个 Talk,那应该或多或少都对日语有点兴趣,而且说不定还有一个的小目标:「能轻松啃生肉就好了」。 这个听起来好像不是那么简单,但其实……也确实有一定难度。比如,今年我看到一个帖子,一大群 V 友吐槽「日语怎么这么难学」。 [[V2ex:没有人觉得日语的学习难度很大吗?]] <https://www.v2ex.com/t/1138764> 除了这篇帖子里提到的各种问题,还有一个比较重要的点:「查日语单词其实是个技术活儿」。 为什么这么说呢?这个帖子吐槽了「日语单词不空格」的问题,大家知道(すもももももももものうち)该怎么断句吗。 如果每次查单词,都要猜到底哪几个假名是一个单词,才开始学日语的同学估计查着查着心态就崩了。 另外,还有个比较关键的问题:虽然日语有汉字,但我们并不熟悉这些汉字的日语发音,这会让我们查单词的时候,更加困难。比如,我们可以看到,这个同学一开始查不到就是因为打错了这个单词的读音,但是,后面直接复制这个单词一下子就查到了。 这给了我们一点灵感:我们是不是可以更进一步写这样一个程序:当用户点击的时候,直接分析出点击位置附近的单词原型,然后直接调用辞典程序—这听起来好像能大幅提高我们的学习效率。 实际上已经有很多沿着这个思路做的工具了,我个人用得最多,也是最想推荐给大家 ,就是这个 jidoujisho(暂无官方中文译名…自动辞书?),之所以最推荐它,一个原因是它不仅支持电子书和漫画,还支持边看动漫边查字幕里的单词(注:个人认为,动漫字幕是最适合日语学习者的泛读入门材料),另一个原因是这个项目直接内置了形态素解析器,解析效果相对来说稍微好点,但是,也有「不少」问题。 ## 基本操作 我们先简单实现一个的 Demo,然后说说到底存在哪些问题。 ```bash # 安装依赖 pip install sudachipy sudachidict_full ``` 由于前面已经有 Talk 介绍过 Sudachi ,所以我就不过多介绍,直接给出最关键的代码。 ```python 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("晩ご飯を食べましたか。")) # [["晩ご飯", "晩御飯"], ["を", "を"], ["食べ", "食べる"],["まし", "ます"], ["た", "た"], ["か", "か"], ["。", "。"]] ``` ```js 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/>。 大家可能觉得奇怪,为什么要用「`[["晩ご飯", "晩御飯"], ["を", "を"], ["食べ", "食べる"],["まし", "ます"], ["た", "た"], ["か", "か"], ["。", "。"]]`」这种冗余的格式返回数据? 这是因为,如果文本「不变」,前端就可以用「一次」解析结果,通过索引位置返回最靠近用户点击位置的单词原型。 看起来我们已经完美实现了我们想要的功能,下面,我们来做一个比较有挑战性的测试: ```python 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 文件,然后通过指令生成词库即可。 [Sudachi ユーザー辞書作成方法](https://github.com/WorksApplications/Sudachi/blob/develop/docs/user_dict.md) ```csv # 見出し (TRIE 用),左連接ID,右連接ID,コスト,見出し (解析結果表示用),品詞1,品詞2,品詞3,品詞4,品詞 (活用型),品詞 (活用形),読み,正規化表記,辞書形ID,分割タイプ,A単位分割情報,B単位分割情報,※未使用 春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,* ``` 大家可能看不懂这个,但没关系,绝大多数时候,我们都是添加有「名词」,所以重点关注「 見出し (TRIE 用)」和「読み」(读音)即可,其他配置基本可以照着抄。 但如果公司业务比较特殊,需要登录名词之外的词,大家就要重点关注最前面的三个数字,具体我稍后解释,我们先来看看效果。 ```bash sudachipy ubuild -s system_full.dic -o user.dic user.csv ``` 备注: system_full.dic 可以从 [SudachiDict](https://github.com/WorksApplications/SudachiDict/releases)下载,也可以从 pip install 安装路径复制。 参考路径: `.venv/lib/python3.13/site-packages/sudachidict_full/resources` --- 通过上面的这条指令,我们在指定的路径下生成一个名叫 `user.dic` 的文件,然后,在调用 Sudachi 时,我们也要告诉它:除了官方提供的词库,还要加载我们自定义的词库: ```python 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("なんで『春日影』やったの!?")) # [['なん', '何'], ['で', 'で'], ['『', '『'], ['春日影', '春日影'], ['』', '』'], ['やっ', '遣る'], ['た', 'た'], ['の', 'の'], ['!', '!'], ['?', '?']] ``` 很好!现在「春日影」能被正确解析了! 当然,日语比较好的同学可能会发现,前面的「なんで」是不是也解析错了? 现在这个词被拆成了「なん」和「で」,没错,所以这个词也需要我们手动整理添加。但其实,这句话还有一个未登录词——「やった」。 ```csv なんで,4,4,-32768,なんで,副詞,*,*,*,*,*,ナンデ,なんで,*,*,*,*,* 春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,* ``` ## 未登录词的分类 不懂日语的同学可以把这个地方的「やった」简单理解成英语的「did」,也就是「do」的过去式就好了。 Sudachi 确实是收录了表示「做」的这个意思词条,但问题在于日语的「やった」其实还可以表示感叹(幻灯片右边的 やった的日语解释),大家可以简单理解成汉语的「好耶」。 但是,这个词是「连语」,而 Sudachi 词典没有这个分类,那我们该怎么办呢? 懂日语的同学可能会说,这个词没有活用,不如就把这个词当作「名词」吧。听起来不错,我们来试试看。 ```csv 春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,* やった,5146,5146,-32768,やった,名詞,普通名詞,一般,*,*,*,ヤッタ,やった,*,*,*,*,* ``` 很不幸,如果我们这么做,这个句子是对了,但前面的句子就会错,因为第一个句子里的「やった」不是名词,就是动词。 --- 那怎么办呢?其实,如果大家翻下其他词典,比如 MOJi 对「やった」分类,我们会发现一个有趣现象:除了《大辞泉》,市面上的日语词典对这个词的分类都是「感动词」,而这个分类, Sudachi 正好也有,我们以这个分类登录进去试试。 ```csv なんで,4,4,-32768,なんで,副詞,*,*,*,*,*,ナンデ,なんで,*,*,*,*,* 春日影,5146,5146,-32768,春日影,名詞,普通名詞,一般,*,*,*,ハルヒカゲ,春日影,*,*,*,*,* やった,5687,5687,-32768,やった,感動詞,一般,*,*,*,*,ヤッタ,やった,*,*,*,*,* ``` 很好!我们可以发现两句话的解析结果都对的了。 大家可以鼓掌庆祝一下,到目前为止,通过这句很短的话,我们就发现并整理了三个未登录词。这是我们的一==大==步,但却只是 Sudachi 的一==小==步。 --- ## 未登录词的数量 为什么这么说呢?通过分析比较 Suadchi 和《大辞泉》的全部词条,我们会发现:尽管目前 Sudachi 的词库已经收录了近两百万的单词,但《大辞泉》的近三十万词条中仍有近 1/2 未被收录,这意味着我们刚才的操作只是将解析精度提高了 0.002%(百分之零点零零二)。 有同学可能会好奇,为什么会有这么多的词条都没有被收录? Sudachi 词库是不是不行?其实不是,Sudachi 的词库已经是目前的开源项目中,收词量最大的了。 之所以会出现这样的情况,一方面是因为日语的特点决定了各行各业都会有大量专有名词,而且每天还在不停地产生新词。 另一方面 Sudachi 词库受日本国立国语研究所的 UniDic 项目的影响非常大,而 UnDic 项目的初衷是面向语料库等学术研究的场景,所以收词原则和大多数人理解的会有一定的区别。 比如,它的单词词性体系和《大辞泉》就不太一样,而如何处理这些分类不一样的单词其实是个非常麻烦的问题。 注:如果你对这部分内容有兴趣,请查看[[我的 YANS 2025 发表]]。 ## 为什么要查春日影? 最后,回到最初的问题:为什么我要专门用「春日影」来做测试呢? 因为这句话几乎每个词都是「未登录词」,而且还有「最棘手」的未登录词—也就是像「やった」这样,词性本身就有一定争议的词。这些词很可能存在多种理解,所以手动登录时需要大量测试覆盖边界情况,不然很容易出现前面提到的「这句对了,那句又错了」的情况。 所以,在演讲的最后,我有「两」件事想拜托大家: 如果大家发现 Sudachi 有解析不对的句子,欢迎分享给我,我会一个一个整理的。 另外,也求求大家去看看「摇曳露营」吧,我真的什么都愿意做的.jpg~~大雾~~。 以上就是我演讲的全部内容了,谢谢大家!(手动滑稽🤪) ## 彩蛋 很高兴你愿意看到最后,也许你已经发现了:Sudachi 没有收录任何「惯用句」!(不懂日语的同学可以理解成英语的短语搭配)。 如果有机会,我会在明年(2026年)的 PyCon China 上分享如何解决这个问题,期待明年与你见面(笑)。 提前剧透:问题的关键在于词典收录的惯用句和实际使用时长得可能不太一样,除了后项动词的活用,还会有下面这些复杂的变形: ![[GBC 惯用句]]