用代码解决日语学习问题
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
在精进技能的同时,我也喜欢学习语言。我学过英语,我的母语是葡萄牙语,现在因为热爱日本文化,我正在学习日语。昨天我突发奇想,想用Node.js自动化一项枯燥乏味的任务,这项任务一直阻碍着我的学习。让我们从头开始吧。
背景
我从2015年就开始学习日语,这是一段漫长的旅程,虽然离流利还很远,但我现在已经可以比较轻松地阅读漫画,也能借助字典阅读书籍。这周我开始读一本新书,并决定再次尝试使用Anki——一款功能强大的单词卡片应用,在日语学习者中非常流行,但它几乎可以用来学习任何内容。我之前也是这样用的:一边看书一边查字典,把所有不认识的单词都添加到一个.txt文件中,然后再导入到Anki里,开始记忆。但是,Anki存在一个问题,可能正是这个问题让我之前放弃了它。接下来我们就来探讨一下这个问题。
问题
Anki 有一个导入功能,你可以创建一个 .txt 文件,用分号分隔出闪卡的正反两面,像这样:
傍ら;side, edge, beside, besides, nearby, while (doing)
飢える;to starve, to thirst, to be hungry
へたり込む;to sit down hard, to sink down to the floor
払い除ける;to ward off, to brush away, to fling off, to drive away
但你必须想办法制作这个文件,一开始我是手动做的。我记下所有不认识的单词,每天最多记50个,以免一次性学习太多。之后,我查阅字典,把单词的意思抄写到单词卡的另一面。此外,日语中有三种类型的字符:平假名、片假名和汉字。简单来说,汉字代表概念,例如,“愛”表示爱;而平假名和片假名代表发音,用来描述汉字的读音。以“愛”为例,它的平假名读音是“あい”,用我们的字母拼写为“あい” ai。想要更详细的解释,你可以参考维基百科,那里有非常好的总结。因此,记住汉字单词的读音也很重要,所以我还需要制作另一个文件,格式如下,单词和读音用分号分隔:
傍ら;かたわら
飢える;うえる
へたり込む;へたりこむ
払い除ける;はらいのける
问题在于,这种手动操作非常枯燥乏味且耗时。我必须逐个复制单词,查阅词典,复制释义和读音,然后再导入到 Anki 中。虽然词典是电子版的,只需 Ctrl+C + Ctrl+V 即可,但即便如此,准备 50 个单词也需要大约 30 分钟。而且这种方法很容易出错,因为我可能会把读音和释义混淆,导入到错误的文件,或者把不同词义的单词放错行。我必须想办法改善这种体验,让阅读重新变得有趣,于是我想到了编写一个脚本来实现这个目标。
解决方案
由于脚本相对简单,我决定借此机会练习一下我正在学习的 NodeJS。然而,它并不像看起来那么简单,因为需要一个字典来为应用程序提供数据。幸运的是,我之前在另一个项目中使用 Lambda 和 API Gateway 访问时,在 DynamoDB 表中创建了一个字典。希望不久的将来我也能谈谈那个项目,但现在假设脚本可以访问一个 API,该 API 会返回根据给定参数找到的单词,例如example.com?term=愛:
解决了这个主要问题后,剩下的工作就是调用 API、解析响应并写入文件。整个脚本仅使用了三个库:
- axios:用于调用 API 的 HTTP 客户端库。我过去使用过它,体验非常好,因为它比我接触过的其他库要简单易用得多。
- fs:nodejs 中用于处理文件 I/O 的标准库。
- 进度:通过添加进度条,使工作进行时响应更加迅速。
首先,我声明了一些变量来存储输入文件的内容,该文件每行包含一个单词。我将它们分割并存储到一个数组中,以便后续使用。我还声明了一些变量来存储结果:
let input = fs.readFileSync('input.txt', {encoding: 'utf8'});
let terms = input.split('\r\n');
let outputReading = "";
let outputMeaning = "";
然后我创建了一个 axios 实例,以及我用来调用 API 并获取所需单词的函数:
var instance = axios.create({
baseURL: "https://api.example.com",
headers: {'x-api-key': "xxxxxxxxxx"}
});
async function getWord(term){
const response = await instance.get("/dictionary", {params: {term: term}});
return response.data.body[0];
}
该函数调用 API 并返回响应体。响应是一个包含搜索结果的数组。其结构简化描述如下:
{
"statusCode": 200,
"body": [
{
"Id": 1,
"kanji": [],
"kana": [],
"sense": [
{
"gloss":[]
}
]
}
]
}
答案中包含更多细节,详细描述了整个单词,但对于我试图解决的问题而言,重要的是以下几点:
- 假名:一个包含单词所有读音的数组。一个单词可以有多个读音,但数组中的第一个读音是最常用的,通常也是我要找的读音。
- 含义:一个包含词义及其信息的数组:词性、方言、相关词汇、反义词等。一个词可以有不同的含义,但一个含义可以有很多同义词。
- 释义:同义词存储在这里的数组中。
前面提到的数组中存储的所有对象都有一个text字段,其中存储着我们感兴趣的信息。以我们之前的例子“愛”为例,响应的概要如下:
{
"statusCode": 200,
"body": [{
"kanji": [{
"common": 1,
"text": "愛",
"tags": []
}],
"kana": [{
"appliesToKanji": ["*"],
"text": "あい",
"common": 1,
"tags": []
}],
"Id": 1150410,
"sense": [{
"gloss": [{
"lang": "eng",
"text": "love"
}, {
"lang": "eng",
"text": "affection"
}, {
"lang": "eng",
"text": "care"
}]
}, {
"gloss": [{
"lang": "eng",
"text": "attachment"
}, {
"lang": "eng",
"text": "craving"
}, {
"lang": "eng",
"text": "desire"
}]
}]
}]
}
收到响应后,为了处理并以我想要的格式获取结果,我创建了两个函数分别处理含义和读数。以下是函数handleMeanings示例:
function handleMeanings(term, word){
let meaningsArray = []
for(sindex in word.sense){
let glosses = word.sense[sindex].gloss;
for(gindex in glosses){
meaningsArray.push(glosses[gindex].text);
}
}
let joinMeanings = meaningsArray.join(", ");
let result = term + ";" + joinMeanings + "\r\n";
return result;
}
对于每个对象,sense我遍历它的词库列表并将其添加到数组中,然后将所有内容连接起来,非常简单,这正是我想要的。
结论
对于那些看到标题和“吓人”的图片就以为内容很复杂的人,我很抱歉。它其实非常简单,甚至有点虎头蛇尾,但它确实帮助我跟上了学习进度。现在的问题是完成所有的复习,我会尽力的!:D
如果你觉得有什么地方可以改进,请告诉我。NodeJS 对我来说还是个新东西!