视频
B站视频链接:
视频内容概要
如果把想要分享的内容全部都介绍完,估计时间会很长,所以计划拆分为俩视频来介绍,每个视频把想要说明的核心要点给讲清楚就好。
- 视频 1:讲清楚 qwerty learner 是个啥?
- 上期视频简介:若不熟悉打字指法,建议回看一下上期视频的后半部分 —— 打字指法教程
- 对应的文档:027. 练习打字的工具分享
- 视频链接
- 介绍 qwerty learner 这款工具
- 简单说明第二个视频的内容
- 上期视频简介:若不熟悉打字指法,建议回看一下上期视频的后半部分 —— 打字指法教程
- 视频 2:
- 上期视频简介
- 介绍数据的获取方式
- 展示解析后的数据的结构
- 介绍 qwerty learner 词典数据源
- 介绍 qwerty learner 桌面版的获取方式
- 脚本使用 & 功能介绍
- 如何将数据导出为 PDF、word
视频源文件 第一期视频源文件: 第二期视频源文件: 第二期字幕.srt.zip
References
词典目录下载解析后的词典数据
一键下载所有资源:results.zip
封面 | 备注 |
---|---|
- 单词数量:2607 - 词典源数据下载:CET4_3.zip - 解析后的 markdown 下载:[markdown]CET4_3.zip |
|
| |
- 单词数量:2345
- 词典源数据下载:CET6_3.zip
- 解析后的 markdown 下载:[markdown]CET6_3.zip
|
| |
- 单词数量:3728
- 词典源数据下载:KaoYan_3.zip
- 解析后的 markdown 下载:[markdown]KaoYan_3.zip
|
| …… | …… |
Qwerty Learner 功能介绍?
对于 Qwerty Learner 工具的相关功能,可以通过 👇🏻👇🏻👇🏻 下面这个视频快速了解。
Qwerty Learner 练习打字 の 免费工具 —— part I_哔哩哔哩_bilibili
练习结束后的反馈面板?
当我们练习完一个 chapter 的内容之后,会弹出该面板。
- 面板左侧可以看到【正确率】【章节耗时】【码字速度】
- 面板右侧展示的内容主要是本次练习中出现错误的一些词汇,只需要将鼠标悬停在对应的词汇上即可查看该单词对应的中文意思
- 面板的底部的三个按钮【默写本章节
shift + 回车键
】【重复本章节空格键
】【下一章节回车键
】都有对应的快捷键。(若在敲完后,不想把放在键盘上的手移开去摸鼠标,完全可以通过对应快捷键来选择自己想要的功能)
如何给 Qwerty Learner 项目提建议 or 问题?
直接给项目提 Issues(建议 or 问题)即可。
🎙 功能与建议
目前项目处于开发初期,新功能正在持续添加中,如果你对软件有任何功能与建议,欢迎在 Issues 中提出 项目的进展与未来计划在 Issue 中详细介绍,内部也包含对未来功能的意见征询等,如果对 Qwerty Learner 的未来感兴趣,欢迎参与讨论。 如果你也喜欢本软件的设计思想,欢迎提交 pr,非常感谢你对我们的支持!
🏄♂️ 贡献指南
如果您对本项目感兴趣,我们非常欢迎参与到项目的贡献中,我们会尽可能地提供帮助 在贡献前,希望您阅读 Issue #42 了解我们目前的开发计划,我们希望您能参与到”计划中”的工作亦或者 Issue 区 Label 为 “Help Wanted” 的工作,我们也非常欢迎您实现自己的想法。 如果您确定了想要参与的工作,希望在有基本进展后提交 draft pr,我们可以在 draft pr 上进行讨论,也有利于听取其他 collaborator 的意见。 再次感谢您对项目的贡献!🎉
词库的数据来源是?
数据来源
字典数据来自于kajweb,项目爬取了常见的字典,也是这个项目让我看到了实现本项目的希望。 语音数据来源于有道词典开放 API,感谢有道的贡献让我们这种小项目也可以用上非常专业的发音资源,感谢有道团队以及考神团队为中国教育与中外交流做出的重要贡献。 JS API 来自于react-code-game ,感谢项目对 JS API 的爬取与预处理。
如何导入自己的词库?
如何导入属于自己的生词本
👆🏻👆🏻👆🏻 上面这是官方介绍的导入自己单词本的方法
Qwerty Learner 桌面版的获取方式?桌面应用
如果你也认为这是一款非常不错的工具,并且经常使用的话,那么你可以通过安装它的桌面版,以降低访问成本。成功安装完之后,它就像你电脑上的浏览器一样,需要的时候,直接点击桌面图标即可快速打开。
项目下载:https://github.com/tw93/Pake
安装包:
- windows:Qwerty_x64.msi.zip
- mac:Qwerty.dmg.zip
提取单词数据的脚本介绍?脚本
这涉及到一部分编程相关的知识,暂时还没能力跟大家介绍清楚原理。所以就简单说明一下步骤,和实现逻辑。
步骤:
- 安装 node 环境
- 随便找个地方,新建两个文件夹
sources
、results
- 下载词典源数据到 sources 目录中,并解压
- 执行脚本
具体步骤呢,如果你感兴趣的话,可以回看第二期视频的后半部分。 这里大家听一乐就好,毕竟所有的词典数据源都已经趴下来了……
需要具备的编程知识主要有以下两点:
- 文件的 I O 操作
- 字符串拼接
const fs = require('fs')
const path = require("path")
// const {
// EOL
// } = require('os')
/**
* 源目录名
*/
const SOURCE_FOLDER_NAME = 'sources'
/**
* 目标目录名
*/
const RESULT_FOLDER_NAME = 'results'
/**
* 一个单词下边的子标题
*/
const SUB_TITLE = {
sentence: '例句',
phrase: '短语',
trans: '词义',
relWord: '关联词汇',
ukphone: 'UK',
usphone: 'US',
syno: '同义词'
}
let sourcesFolderPath = path.join(__dirname, SOURCE_FOLDER_NAME); // sources 目录的绝对路径
let resultsFolderPath = path.join(__dirname, RESULT_FOLDER_NAME); // results 目录的绝对路径
const JSON_FileList = fs.readdirSync(sourcesFolderPath)
.filter(p => p.includes('.json'))
.map(p => path.join(sourcesFolderPath, p)) // sources 目录下所有 json 文件的绝对路径
for (let i = 0; i < JSON_FileList.length; i++) {
writeFile(JSON_FileList[i])
}
// 写入文件
function writeFile(fileName) {
// 处理文件内容
let fileContent = fs.readFileSync(fileName, 'utf-8')
fileContent = '[' + fileContent + ']'
fileContent = fileContent.replaceAll(/}\r\n/g, '},').replaceAll(/},]/g, '}]')
// console.log(fileContent);
let data = JSON.parse(fileContent)
// 解析的 json 词典名称
const basename = path.basename(fileName, '.json')
// 清空 results 目录下指定词典目录
const resultFolderPath = path.join(resultsFolderPath, basename)
emptyDir(resultFolderPath)
rmEmptyDir(resultFolderPath)
fs.mkdirSync(resultFolderPath)
// 单词列表(只含单词)
const headWords = data.map(w => w.headWord)
// console.log(headWords)
const checkString = `# ${basename}\n` + headWords.map((h, i) => {
if (i % 20 === 0) return `\n#### Chapter ${i / 20 + 1}\n\n` + `- [ ] ${h}\n`
else return `- [ ] ${h}\n`
}).join('')
fs.writeFileSync(path.join(resultFolderPath, `./${basename}.md`), checkString)
let chapterString;
let chapterNum = 0;
data.forEach((it, i) => {
if (i === data.length - 1) fs.writeFileSync(path.join(resultFolderPath, `./Chapter ${chapterNum}.md`), chapterString)
// 拼接章节 四级标题
if (i % 20 === 0) {
if (chapterNum !== 0) fs.writeFileSync(path.join(resultFolderPath, `./Chapter ${chapterNum}.md`), chapterString)
chapterNum++;
chapterString = '';
chapterString += `# Chapter ${chapterNum}`
chapterString += '\n\n'
// 拼接该 chapter 中的所有单词为 checkbox
chapterString += headWords.slice((chapterNum - 1) * 20, chapterNum * 20).map(h => `- [ ] ${h}\n`).join('')
chapterString += '\n'
}
// 拼接单词 五级标题
chapterString += `# ${i % 20 + 1}. ${it.headWord}`
chapterString += '\n\n'
const word = it.content.word.content
// 拼接发音
chapterString += `${SUB_TITLE.usphone}: [${word.usphone}]` + '\n'
chapterString += `${SUB_TITLE.ukphone}: [${word.ukphone}]` + '\n\n'
// 拼接词义
if (word.trans && word.trans.length > 0) {
const trans = word.trans
chapterString += `## ${SUB_TITLE.trans}` + '\n\n'
for (let i = 0; i < trans.length; i++) {
const t = trans[i]
if (t.pos && t.tranCn) chapterString += `${t.pos}. ${t.tranCn.replace(/\s/g, '')}` + '\n'
if (t.tranOther) chapterString += `\`${t.tranOther}\``
chapterString += '\n\n'
}
}
// 拼接同义词
if (word.syno && word.syno.synos && word.syno.synos.length > 0) {
const synos = word.syno.synos
chapterString += `## ${SUB_TITLE.syno}` + '\n\n'
for (let i = 0; i < synos.length; i++) {
const s = synos[i];
const w = s.hwds.map(h => `\`${h.w}\``).join(' ')
chapterString += `${s.pos}. ${s.tran}` + '\n' + w + '\n\n';
}
chapterString += '\n'
}
// 拼接短语
if (word.phrase && word.phrase.phrases) {
const phrase = word.phrase
const phrases = phrase.phrases
chapterString += `## ${phrase.desc}` + '\n\n'
phrases.forEach(p => {
chapterString += `- \`${p.pContent}\` ${p.pCn}` + '\n'
})
chapterString += '\n'
}
// 拼接例句
if (word.sentence && word.sentence.sentences) {
const sentence = word.sentence
const sentences = sentence.sentences
chapterString += `## ${sentence.desc}` + '\n\n'
sentences.forEach(s => {
chapterString += `\`${s.sContent}\`` + '\n' + s.sCn + '\n\n'
})
chapterString += '\n'
}
})
}
/**
* 删除所有的文件(将所有文件夹置空)
* @param {*} filePath
*/
function emptyDir(filePath) {
try {
const files = fs.readdirSync(filePath) // 读取该文件夹
files.forEach((file) => {
const nextFilePath = `${filePath}/${file}`
const states = fs.statSync(nextFilePath)
if (states.isDirectory()) {
emptyDir(nextFilePath)
} else {
fs.unlinkSync(nextFilePath)
// console.log(`删除文件 ${nextFilePath} 成功`)
}
})
} catch (error) {
// console.log(error)
return
}
}
/**
* 删除所有的空文件夹
* @param {*} filePath
*/
function rmEmptyDir(filePath) {
try {
const files = fs.readdirSync(filePath)
if (files.length === 0) {
fs.rmdirSync(filePath)
// console.log(`删除空文件夹 ${filePath} 成功`)
} else {
let tempFiles = 0
files.forEach((file) => {
tempFiles++
const nextFilePath = `${filePath}/${file}`
rmEmptyDir(nextFilePath)
})
//删除母文件夹下的所有字空文件夹后,将母文件夹也删除
if (tempFiles === files.length) {
fs.rmdirSync(filePath)
// console.log(`删除空文件夹 ${filePath} 成功`)
}
}
} catch (error) {
// console.log(error)
return
}
}
脚本逻辑直接看代码就好,可能还存在些许 bug,就现阶段自测下来,是没有问题的。若有问题,欢迎大家反馈,会找时间处理 bug 的。