视频

B站视频链接:

视频内容概要

如果把想要分享的内容全部都介绍完,估计时间会很长,所以计划拆分为俩视频来介绍,每个视频把想要说明的核心要点给讲清楚就好。

  • 视频 1:讲清楚 qwerty learner 是个啥?
    • 上期视频简介:若不熟悉打字指法,建议回看一下上期视频的后半部分 —— 打字指法教程
    • 介绍 qwerty learner 这款工具
    • 简单说明第二个视频的内容
  • 视频 2:
    • 上期视频简介
    • 介绍数据的获取方式
    • 展示解析后的数据的结构
    • 介绍 qwerty learner 词典数据源
    • 介绍 qwerty learner 桌面版的获取方式
    • 脚本使用 & 功能介绍
    • 如何将数据导出为 PDF、word

视频源文件 第一期视频源文件: Screen-2023-02-17-下午105809.mp4 (140.1MB) 第二期视频源文件: QwertyLearner Part II.mp4 (334.68MB)第二期字幕.srt.zip

References

词典目录下载解析后的词典数据

数据来源:https://github.com/kajweb/dict

一键下载所有资源:results.zip

封面 备注
新东方四级词汇
- 单词数量:2607
- 词典源数据下载:CET4_3.zip
- 解析后的 markdown 下载:[markdown]CET4_3.zip

image.png | | 新东方六级词汇 |
- 单词数量:2345
- 词典源数据下载:CET6_3.zip
- 解析后的 markdown 下载:[markdown]CET6_3.zip
image.png | | 新东方考研词汇 |
- 单词数量:3728
- 词典源数据下载:KaoYan_3.zip
- 解析后的 markdown 下载:[markdown]KaoYan_3.zip
image.png | | …… | …… |

Qwerty Learner 功能介绍?

对于 Qwerty Learner 工具的相关功能,可以通过 👇🏻👇🏻👇🏻 下面这个视频快速了解。
Qwerty Learner 练习打字 の 免费工具 —— part I_哔哩哔哩_bilibili

练习结束后的反馈面板?

image.png

当我们练习完一个 chapter 的内容之后,会弹出该面板。

  • 面板左侧可以看到【正确率】【章节耗时】【码字速度】
  • 面板右侧展示的内容主要是本次练习中出现错误的一些词汇,只需要将鼠标悬停在对应的词汇上即可查看该单词对应的中文意思
  • 面板的底部的三个按钮【默写本章节shift + 回车键】【重复本章节 空格键】【下一章节 回车键】都有对应的快捷键。(若在敲完后,不想把放在键盘上的手移开去摸鼠标,完全可以通过对应快捷键来选择自己想要的功能)

如何给 Qwerty Learner 项目提建议 or 问题?

直接给项目提 Issues(建议 or 问题)即可。

🎙 功能与建议

目前项目处于开发初期,新功能正在持续添加中,如果你对软件有任何功能与建议,欢迎在 Issues 中提出 项目的进展与未来计划在 Issue 中详细介绍,内部也包含对未来功能的意见征询等,如果对 Qwerty Learner 的未来感兴趣,欢迎参与讨论。 如果你也喜欢本软件的设计思想,欢迎提交 pr,非常感谢你对我们的支持! image.png

🏄‍♂️ 贡献指南

如果您对本项目感兴趣,我们非常欢迎参与到项目的贡献中,我们会尽可能地提供帮助 在贡献前,希望您阅读 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

安装包:

提取单词数据的脚本介绍?脚本

这涉及到一部分编程相关的知识,暂时还没能力跟大家介绍清楚原理。所以就简单说明一下步骤,和实现逻辑。

步骤:

  1. 安装 node 环境
  2. 随便找个地方,新建两个文件夹 sourcesresults
  3. 下载词典源数据到 sources 目录中,并解压
  4. 执行脚本

    具体步骤呢,如果你感兴趣的话,可以回看第二期视频的后半部分。 这里大家听一乐就好,毕竟所有的词典数据源都已经趴下来了……

需要具备的编程知识主要有以下两点:

  • 文件的 I O 操作
  • 字符串拼接
  1. const fs = require('fs')
  2. const path = require("path")
  3. // const {
  4. // EOL
  5. // } = require('os')
  6. /**
  7. * 源目录名
  8. */
  9. const SOURCE_FOLDER_NAME = 'sources'
  10. /**
  11. * 目标目录名
  12. */
  13. const RESULT_FOLDER_NAME = 'results'
  14. /**
  15. * 一个单词下边的子标题
  16. */
  17. const SUB_TITLE = {
  18. sentence: '例句',
  19. phrase: '短语',
  20. trans: '词义',
  21. relWord: '关联词汇',
  22. ukphone: 'UK',
  23. usphone: 'US',
  24. syno: '同义词'
  25. }
  26. let sourcesFolderPath = path.join(__dirname, SOURCE_FOLDER_NAME); // sources 目录的绝对路径
  27. let resultsFolderPath = path.join(__dirname, RESULT_FOLDER_NAME); // results 目录的绝对路径
  28. const JSON_FileList = fs.readdirSync(sourcesFolderPath)
  29. .filter(p => p.includes('.json'))
  30. .map(p => path.join(sourcesFolderPath, p)) // sources 目录下所有 json 文件的绝对路径
  31. for (let i = 0; i < JSON_FileList.length; i++) {
  32. writeFile(JSON_FileList[i])
  33. }
  34. // 写入文件
  35. function writeFile(fileName) {
  36. // 处理文件内容
  37. let fileContent = fs.readFileSync(fileName, 'utf-8')
  38. fileContent = '[' + fileContent + ']'
  39. fileContent = fileContent.replaceAll(/}\r\n/g, '},').replaceAll(/},]/g, '}]')
  40. // console.log(fileContent);
  41. let data = JSON.parse(fileContent)
  42. // 解析的 json 词典名称
  43. const basename = path.basename(fileName, '.json')
  44. // 清空 results 目录下指定词典目录
  45. const resultFolderPath = path.join(resultsFolderPath, basename)
  46. emptyDir(resultFolderPath)
  47. rmEmptyDir(resultFolderPath)
  48. fs.mkdirSync(resultFolderPath)
  49. // 单词列表(只含单词)
  50. const headWords = data.map(w => w.headWord)
  51. // console.log(headWords)
  52. const checkString = `# ${basename}\n` + headWords.map((h, i) => {
  53. if (i % 20 === 0) return `\n#### Chapter ${i / 20 + 1}\n\n` + `- [ ] ${h}\n`
  54. else return `- [ ] ${h}\n`
  55. }).join('')
  56. fs.writeFileSync(path.join(resultFolderPath, `./${basename}.md`), checkString)
  57. let chapterString;
  58. let chapterNum = 0;
  59. data.forEach((it, i) => {
  60. if (i === data.length - 1) fs.writeFileSync(path.join(resultFolderPath, `./Chapter ${chapterNum}.md`), chapterString)
  61. // 拼接章节 四级标题
  62. if (i % 20 === 0) {
  63. if (chapterNum !== 0) fs.writeFileSync(path.join(resultFolderPath, `./Chapter ${chapterNum}.md`), chapterString)
  64. chapterNum++;
  65. chapterString = '';
  66. chapterString += `# Chapter ${chapterNum}`
  67. chapterString += '\n\n'
  68. // 拼接该 chapter 中的所有单词为 checkbox
  69. chapterString += headWords.slice((chapterNum - 1) * 20, chapterNum * 20).map(h => `- [ ] ${h}\n`).join('')
  70. chapterString += '\n'
  71. }
  72. // 拼接单词 五级标题
  73. chapterString += `# ${i % 20 + 1}. ${it.headWord}`
  74. chapterString += '\n\n'
  75. const word = it.content.word.content
  76. // 拼接发音
  77. chapterString += `${SUB_TITLE.usphone}: [${word.usphone}]` + '\n'
  78. chapterString += `${SUB_TITLE.ukphone}: [${word.ukphone}]` + '\n\n'
  79. // 拼接词义
  80. if (word.trans && word.trans.length > 0) {
  81. const trans = word.trans
  82. chapterString += `## ${SUB_TITLE.trans}` + '\n\n'
  83. for (let i = 0; i < trans.length; i++) {
  84. const t = trans[i]
  85. if (t.pos && t.tranCn) chapterString += `${t.pos}. ${t.tranCn.replace(/\s/g, '')}` + '\n'
  86. if (t.tranOther) chapterString += `\`${t.tranOther}\``
  87. chapterString += '\n\n'
  88. }
  89. }
  90. // 拼接同义词
  91. if (word.syno && word.syno.synos && word.syno.synos.length > 0) {
  92. const synos = word.syno.synos
  93. chapterString += `## ${SUB_TITLE.syno}` + '\n\n'
  94. for (let i = 0; i < synos.length; i++) {
  95. const s = synos[i];
  96. const w = s.hwds.map(h => `\`${h.w}\``).join(' ')
  97. chapterString += `${s.pos}. ${s.tran}` + '\n' + w + '\n\n';
  98. }
  99. chapterString += '\n'
  100. }
  101. // 拼接短语
  102. if (word.phrase && word.phrase.phrases) {
  103. const phrase = word.phrase
  104. const phrases = phrase.phrases
  105. chapterString += `## ${phrase.desc}` + '\n\n'
  106. phrases.forEach(p => {
  107. chapterString += `- \`${p.pContent}\` ${p.pCn}` + '\n'
  108. })
  109. chapterString += '\n'
  110. }
  111. // 拼接例句
  112. if (word.sentence && word.sentence.sentences) {
  113. const sentence = word.sentence
  114. const sentences = sentence.sentences
  115. chapterString += `## ${sentence.desc}` + '\n\n'
  116. sentences.forEach(s => {
  117. chapterString += `\`${s.sContent}\`` + '\n' + s.sCn + '\n\n'
  118. })
  119. chapterString += '\n'
  120. }
  121. })
  122. }
  123. /**
  124. * 删除所有的文件(将所有文件夹置空)
  125. * @param {*} filePath
  126. */
  127. function emptyDir(filePath) {
  128. try {
  129. const files = fs.readdirSync(filePath) // 读取该文件夹
  130. files.forEach((file) => {
  131. const nextFilePath = `${filePath}/${file}`
  132. const states = fs.statSync(nextFilePath)
  133. if (states.isDirectory()) {
  134. emptyDir(nextFilePath)
  135. } else {
  136. fs.unlinkSync(nextFilePath)
  137. // console.log(`删除文件 ${nextFilePath} 成功`)
  138. }
  139. })
  140. } catch (error) {
  141. // console.log(error)
  142. return
  143. }
  144. }
  145. /**
  146. * 删除所有的空文件夹
  147. * @param {*} filePath
  148. */
  149. function rmEmptyDir(filePath) {
  150. try {
  151. const files = fs.readdirSync(filePath)
  152. if (files.length === 0) {
  153. fs.rmdirSync(filePath)
  154. // console.log(`删除空文件夹 ${filePath} 成功`)
  155. } else {
  156. let tempFiles = 0
  157. files.forEach((file) => {
  158. tempFiles++
  159. const nextFilePath = `${filePath}/${file}`
  160. rmEmptyDir(nextFilePath)
  161. })
  162. //删除母文件夹下的所有字空文件夹后,将母文件夹也删除
  163. if (tempFiles === files.length) {
  164. fs.rmdirSync(filePath)
  165. // console.log(`删除空文件夹 ${filePath} 成功`)
  166. }
  167. }
  168. } catch (error) {
  169. // console.log(error)
  170. return
  171. }
  172. }

脚本逻辑直接看代码就好,可能还存在些许 bug,就现阶段自测下来,是没有问题的。若有问题,欢迎大家反馈,会找时间处理 bug 的。