![da37da1899b69e583d76376e1f728dc4.png](https://i-blog.csdnimg.cn/blog_migrate/a31d9f50660cfcfb3ec2ae454c09451a.png)
昨天是我背《十天搞定考研词汇》这本书的第十天。过去的十天里我每天都花大量的时间在单词上,虽然并没有像书名所说的“搞定”考研词汇,但也确实收获颇丰。十天过去了,往后我要怎么复习单词呢?我想到了Anki。
(吃瓜群众请直接翻到文章最后)
以前我也尝试过Anki许多次,但说真的Anki并没有给我带来任何帮助。我认为原因有以下几个:1.制作卡片太浪费时间;2.用别人的卡片不合心意;3.背卡片总感觉不踏实,看不到希望、没有成就感。
这一次应该会好很多。
为什么用R语言。
前几天我才开始学R语言[1],因为论文可能要用它来处理数据。我其实更倾向于用Matlab,因为帮助文档很完善、例子也很多,用着也顺手,但用R可能会显得更“专业”一点 。我想正好这个问题用R也能处理,那就用R语言吧。但毕竟我对R语言的特性还不很了解,如果代码有可改进之处,欢迎友好指出。
关于epub/azw3电子书
这两种格式的电子书文件,不专业地说,你可以把它们看成zip、rar之类的压缩包(把.epub后缀改成.zip可以直接打开),压缩包里面是一些html格式的网页,书的内容就在这些网页文件里面。既然是网页,那如果你想获取里面的内容的话,是不是可以用“网络爬虫”来抓取呢?
大致思路
因此,通过epub/azw3格式的电子书制作卡片可以大致分成如下几个步骤:1.把电子书文件解压开,获取里面的html文件;2.用爬虫爬取里面需要的信息,并将信息整理汇总成一个“表格”(csv文件);3.将csv文件导入Anki;4.修理bug、调整细节、美化卡片。
开始
下面以《十天搞定考研词汇》这本书为例。这是一个完整的例子,我写这篇文章的时候也已经制作完成了,因此我想这篇文章对R语言初学者、有相同需求的人来说都是很有借鉴意义的。
接下来将《十天搞定考研词汇》简称为《十天》。
观察这本书
可以看到,这本书一共有20个单词列表,从list1到list20,背单词的任务分成了10天,每天背两个列表,因此,具体到这本书,我们的任务是:1.将电子书文件中的所有单词都导入到anki中;2.在anki中创建一个总记忆库、再创建20个子记忆库,每个子记忆库保存一个列表的单词;3.单词卡片正面是单词,背面是单词的音标、释义和扩展;4.需要把书中高亮的单词也给标注出来。
步骤1:获取html网页文件、观察结构
凭空变出《十天》的epub或azw3格式的电子书文件。我这里用的是azw3格式的。导入Calibre中,在这本书上点鼠标右键选“Edit book”,在打开的页面中可观察到,每一个列表的单词被单独存放在一个html文件中,选择需要的html文件导出。
观察导出的html文件。这里选取一部分:
<h3 class="chapter-three">List 1</h3>
<p class="bodytext"><span class="jiacu">intellect</span> <span class="yinbiao">[ˈɪntəlekt] n.</span> <span class="blue-title">智力;</span>理解力;[总称] <span class="blue-title">知识分子</span></p>
<p class="content-yinyong"><span class="juli">派</span>intellectual <span class="yinbiao">[ˌɪntəˈlektʃuəl] adj.</span> <span class="blue-title">智力的;</span>理智的;<span class="blue-title">聪明</span><span class="blue-title">的</span></p>
<p class="content-yinyong"><span class="juli0">考点搭配</span>intellectual enquiry 知识探索,知识探求</p>
<p class="content-yinyong">intellectual achievement 知识成就,智力成果</p>
<p class="content-yinyong">intellectualize <span class="yinbiao">[ˌɪntəˈlektʃuəlaɪz] vt.</span> <span class="blue-title">使…理智化;对…做理性探讨</span></p>
<p class="bodytext"><span class="jiacu">contempt</span> <span class="yinbiao">[kənˈtempt] n.</span> <span class="blue-title">轻视,轻蔑</span></p>
<p class="content-yinyong"><span class="juli">派</span> contemptible <span class="yinbiao">[kənˈtemptəbl] adj.</span> 可鄙的;可轻视的</p>
<p class="content-yinyong">contemptuous <span class="yinbiao">[kənˈtemptʃuəs] adj.</span> <span class="blue-title">轻视的,蔑视的</span></p>
<p class="bodytext"><span class="jiacu">ultimate</span> <span class="yinbiao">[ˈʌltɪmət] adj.</span> <span class="blue-title">最后的,最终的</span></p>
<p class="bodytext"><span class="jiacu">yield</span> <span class="yinbiao">[jiːld] n.</span> <span class="blue-title">产量,</span>收获量;收益 <span class="yinbiao">v.</span> <span class="blue-title">出产;屈服</span></p>
<p class="bodytext"><span class="jiacu">contend</span> <span class="yinbiao">[kənˈtend] vi.</span> <span class="blue-title">竞争,</span>争夺 <span class="yinbiao">vt.</span> 坚决主张,<span class="blue-title">声称</span></p>
如上所示,每一个词条均被包括在一个p标签中,“主单词”的p标签的class属性值为bodytext,“单词扩展”的class属性为content-yinyong。至于p标签内部,可以看到需要修改css样式的部分都被包括在不同的span标签中,也都有不同的class属性,据此我们可以对它们应用css样式,这放到后面anki卡片样式美化那里再说。
因此我们需要做的有:
1.在R中创建一个数据框,一列是“单词”(字符串)、另一列是“单词释义”(对应的html代码,也是字符串)
2.用爬虫提取所需信息放到上述数据框中。
需要注意的是,一个“主单词”对应的不仅有后面的音标和解释,还可能有下面的“单词扩展”,虽然在书的html代码中二者没有包含关系(是同级并列的),但这两部分内容是要放在一起的,都要放到“主单词”对应的“单词释义”里面。
步骤2:爬虫爬取内容
我们用到的是R的rvest包。
install.packages('rvest')#安装,这句代码只需使用一次
library('rvest')#载入包
载入网页代码[2]:
url = '/Users/aoyu/Desktop/10dayshtml/list20.html'
web = url%>%read_html('UTF-8')
提取需要的html代码片段:
md = web%>%html_nodes(xpath='//p')
上面我们通过观察电子书的html代码已知道,我们所需的单词信息都是包括在p标签中的,p标签中都是我们需要的内容,而且我们在这一步不需要区分“主单词”和“单词扩展”,所以我们只使用p标签来筛选即可。
md的类型是xml_nodeset,它内部是一个个的节点,就像html的标签树一样,我们用md[i]可获取它内部第i个节点的代码的简略信息,如执行md[2]:
> md[2]
{xml_nodeset (1)}
[1] <p class="bodytext"><span class="jiacu">effect</span> <span class="yinbiao">[ɪˈfekt] n ...
新建一个新的空数据框:
mylist1 <- data.frame(word=character(0), meaning=character(0))
变量word我们打算用来保存“主单词”字符串,变量meaning对应的是主单词的释义和单词扩展的html代码串(也是字符串)。
将主单词和单词扩展合并,保存到数据框中:
包裹主单词内容的p标签的class属性值是bodytext,包括单词扩展内容的p标签的class属性值是content-yinyong,据此我们可以把主单词和单词扩展区分开。
遍历变量md中保存的每一条代码段(下面称为节点),如果一个节点的class属性值为bodytext就说明它是一个“主单词”,这时我们从代码段中提取出这个单词的字符串保存到word变量中,并把代码段保存到meaning变量中(作为“单词释义”),二者作为一个“观测”保存到数据框mylist1中;如果一个节点的class属性值为content-yinyong,说明它是前面与它相临的“主单词”的“单词扩展”,就把它的内容合并到前面与它相临的“主单词”的内容中(meaning变量中)。
怎样把xml_nodeset类型的变量中的html代码输出到一个数据框中呢,这个问题着实困扰了我好久,用as.character()函数即可[3]。
#将附加单词内容与主单词内容合并
for (i in 1:length(md)) {
if (md[i]%>%html_attr('class')=="bodytext") {
word <- md[i]%>%html_nodes(".jiacu")%>%html_text()
meaning <- md[i] %>% as.character
mylist1 <- rbind(mylist1,c(word,meaning))
} else if (md[i]%>%html_attr('class')=="content-yinyong") {
mylist1[length(mylist1[,1]),2] <- paste(mylist1[length(mylist1[,1]),2],md[i] %>% as.character)
}
}
这样一个过程完成后,我们就得到了一个两列的数据框,数据框左边一列是单词,右边一列是html代码,这段代码不仅包含了音标、释义等内容,还包含了单词扩展的内容。如图:
![d2e0e89b6fbd7dc09d7de5e64d7e3ef4.png](https://i-blog.csdnimg.cn/blog_migrate/9ebdb6088882c6b48e2567ad42dd07be.jpeg)
输出到csv文件:
write.table(mylist1, file = "/Users/aoyu/Desktop/10dayscsv/mylist1.csv", row.names=FALSE,col.names=FALSE, sep=",")
上面代码的意思是输出数据框mylist1中的数据到mylist1.csv文件中,不包含row.names行名和col.names列名,内容以英文逗号分隔。
步骤3:将csv文件导入anki
在得到csv文件后,我用excel打开发现中文乱码,其他软件正常、但密密麻麻的很难看,知道是excel的问题、csv文件没问题就好,索性就不细细检查了(为后面的bug埋下伏笔,之后我安装了LibreOffice)。
打开anki,在菜单中选择“工具”——“导入”,选择刚才得到的csv文件。接下来的设置,我是这样做的:
![24c9d250598423b552994609191e03a1.png](https://i-blog.csdnimg.cn/blog_migrate/fd16d6d70b7e9a526b9182f1ac6b66f1.jpeg)
注意选中“允许在字段中使用HTML”。
![2815dc2ac8c7fc99e1dbb79418ab8074.png](https://i-blog.csdnimg.cn/blog_migrate/3c927ba0f97e876b279ed4b9e3820bd9.jpeg)
导入完成后试一下,卡片没有美化,不过内容显示“好像”是正常的,似乎我们离成功已经很近了。但卡片内容后那一个小小的引号提醒我们,事情并没有我们想象的这么美好。
步骤4:修理bug、调整细节、美化卡片
修理bug1
既然“看起来”一切正常,那剩下要做的就是美化卡片了。
但我在修改卡片css的时候发现,添加的css样式似乎不起作用。打开编辑框,选择“编辑HTML”看一下:
![0004fae599da5cde316b71267c4babf2.png](https://i-blog.csdnimg.cn/blog_migrate/51cdfa463544afc246f8a89baf2910fd.jpeg)
为什么会出现这么多的“"”???最后的那个引号是什么时候出现的???
经过检查,发现是导入anki时出的问题,准确地说是anki的bug,anki不能正确识别csv文件内容中的双引号。又过了不知道多长时间,半个小时?我想到了解决方法,就是给输出csv文件的write.table()函数加个参数qmethod,帮助文档中对这个参数的解释如下:
a character string specifying how to deal with embedded double quote characters when quoting strings.
上面输出csv文件的代码应修改为:
write.table(mylist1, file = "/Users/aoyu/Desktop/10dayscsv/mylist1.csv", row.names=FALSE,col.names=FALSE, sep=",",qmethod = "double")
这样在csv文件中,对于内容中的引号,不再是以”的方式转义,而是以””的形式转义。
再次将csv文件导入anki,一切正常。
美化卡片
修理了bug后,这样看起来似乎一切又美好起来了。那接下来开始美化卡片吧。我是模仿《十天》纸质书来修改卡片样式的,这里不多讲,我对CSS已经很生疏了。效果如下:
![58bf66ed968fe23d693cee58e8dc6c02.png](https://i-blog.csdnimg.cn/blog_migrate/130c3bbcc4be717fd1ff6b293ed71207.jpeg)
看起来好像还不错。但因为源文件的限制,不能做的和纸质书一模一样。
CSS代码如下:
.card {
font-family: serif;
font-size: 20px;
text-align: center;
background-color: white;
}
p {
text-align: justify;
}
p.bodytext {
border-top: 2px solid #87CEFA;
}
p.bodytext .jiacu {
background-color: #87CEFA;
font-weight: bold;
font-size: 1.25em;
}
p .blue-title {
color: #00A1E9;
}
p .juli {
color: white;
background-color: grey;
margin-right: 16px;
}
p.content-yinyong {
font-size: 0.85em;
text-indent: 40px;
}
p .juli0 {
margin-right: 16px;
border: 1px solid grey;
}
好像一切都很美好。
修理bug2
昨天是我背《十天》的第10天,晚上还要复习6个列表的单词,索性我就用Anki来复习了。
背着背着我就发现,出bug了。如图:
![c54e0da4591ef9a11834688e437c58a4.png](https://i-blog.csdnimg.cn/blog_migrate/45b36ebd5e0a87f7363905163487fe2c.jpeg)
怎么只显示了音标、释义和扩展,唯独没有“主单词”?
瞬间我就明白过来。上面我们看到,在电子书的html代码中,“主单词”那个词条被class属性值为bodytext的p标签包裹着,而“单词扩展”那个词条被class属性值为content-yinyong的p标签包裹着,而我忽略的一点是,很少一部分的单词有两种发音、对应两个含义,在html代码中,分别用不同的p标签包裹着,且class属性值都是bodytext,这样它们就被当成了两个单词。
怎么修改呢,我们需要修改“将主单词和单词扩展合并,保存到数据框中”这部分的代码。我的修改如下:
#将附加单词内容与主单词内容合并
for (i in 1:length(md)) {
if (md[i]%>%html_attr('class')=="bodytext" & length(md[i]%>%html_nodes(".jiacu")%>%html_text())) {#修bug,增加条件,判断span.jiacu里面有没有内容
word <- md[i]%>%html_nodes(".jiacu")%>%html_text()
meaning <- md[i] %>% as.character
mylist1 <- rbind(mylist1,c(word,meaning,paste("list",j,sep="")))#有补充,添加标签到每个anki卡片
} else { #去掉else if判断条件
mylist1[length(mylist1[,1]),2] <- paste(mylist1[length(mylist1[,1]),2],md[i] %>% as.character)
}
}
遍历到一个节点时,不仅看它的class属性,而且看它内部有没有一个class属性为jiacu的span标签,两者结合起来判断这个节点是否为“主单词”节点。不再判断一个节点是否为“单词扩展”节点,不满足条件的统统按“其他”处理。
好像世界又变得美好了。
调整细节
在《十天》这本书里,我们要处理的一共有20个列表,也就是20个html文件,如果手动一个一个转换的话,未免不够“优雅”,也很浪费时间,这个过程不如也交给程序来做。
这里插入一个小细节,在RStudio中,“清屏”的快捷键是Ctrl+L;清除环境变量需要在控制台输入rm(list=ls())。在Matlab中,一个是clc,一个是clear,感觉还是Matlab更顺手。
如果导出20个csv文件,那么在anki中就要再导入20次,十分麻烦,不如在R程序中,只导出1个csv文件,而通过“加标签”的方式区分不同列表的单词,也就是给数据框再增加一列,这一列保存每个单词所处的列表。
汇总
综上,R程序如下(总):
#install.packages('rvest')
#library('rvest')
rm(list=ls())
#url = 'https://xiake.me/usr/uploads/2020/07/3715819721.html'
for (j in 1:20) {
url = paste('/Users/aoyu/Desktop/10dayshtml/list',j,'.html',sep="")
web = url%>%read_html('UTF-8')
#md = web%>%html_nodes(css = 'p.list1')
#md = web%>%html_nodes(xpath='//p[@class = "bodytext list1"] | //p[@class = "content-yinyong list1"]')
md = web%>%html_nodes(xpath='//p')
#md1 = md %>% as.character #将xml_nodeset变成字符
#新建一个空数据框
mylist1 <- data.frame(word=character(0), meaning=character(0), taghao=character(0))
#mylist1 <- edit(mylist1)
#将附加单词内容与主单词内容合并
for (i in 1:length(md)) {
if (md[i]%>%html_attr('class')=="bodytext" & length(md[i]%>%html_nodes(".jiacu")%>%html_text())) {#修bug,增加条件,判断span.jiacu里面有没有内容
word <- md[i]%>%html_nodes(".jiacu")%>%html_text()
meaning <- md[i] %>% as.character
mylist1 <- rbind(mylist1,c(word,meaning,paste("list",j,sep="")))#有补充,添加标签到每个anki卡片
} else { #去掉else if判断条件
mylist1[length(mylist1[,1]),2] <- paste(mylist1[length(mylist1[,1]),2],md[i] %>% as.character)
}
}
write.table(mylist1, file = paste("/Users/aoyu/Desktop/10dayscsv/mylist",j,".csv",sep=""), row.names=FALSE,col.names=FALSE, sep=",",qmethod = "double")
write.table(mylist1, file = paste("/Users/aoyu/Desktop/10dayscsv/mylist",".csv",sep=""), row.names=FALSE,col.names=FALSE, sep=",",qmethod = "double",append=TRUE)
}
代码中有一些被我注释掉的语句,我贴上来的时候没有去掉是因为觉得可以帮助理解。
在最后
远离知乎好几年,我把知乎昵称都改成了“那就离开吧”,知乎的所有评论、私信我一概没有回复。
先说声抱歉,以后还会继续远离知乎。
发这篇文章是因为我觉得这个程序或者说这种思路对有需要的人确实帮助挺大,只写在我的个人博客里未免有些明珠蒙尘,而且我在网上也没看到有这样将R和Anki联系起来的文章,我背这本单词书、写这个程序和制作卡片的过程也的确很有成就感,很有和小伙伴们分享的欲望。
2021考研,一起加油!
虽然代码很短,但我写的时候遇到了很多困难,完成这个程序让我感觉我对R处理问题的逻辑有了进一步的了解。我受C语言的影响还是蛮大的,从上面的代码中还能看到C语言的影子。
管中窥豹,感觉R社区的交流氛围应该是挺好的,不过这也不能掩盖帮助文档做的太不人性化的事实,我看过的软件、程序的帮助文档,还就属Matlab的最好。
我已经很尽力地去准确还原我的思考过程了。
在文章开头我说,以前Anki没有给我带来任何帮助但这一次应该会好很多。对应的,下面我也给出几点原因:
1.制作卡片不再需要很多时间。虽然《十天》这本书结构很简单,但我写的这个程序稍加修改就是可以应用到其他卡片制作当中的。
2.既然自己制作卡片这么省事,那也就不用考虑用别人的卡片不合心意的问题了,自己做就好。
3.因为我是先用纸质书背了10天的《十天》,书中的全篇内容实际上我都已经读了好几遍了,再看到书中一个单词的时候,可能我不知道它的含义,但一定知道它在书上出现过。用anki背卡片感觉不踏实无非是对自己的记忆效果没有一个准确的定位,但先用纸质书背过之后,就相当于给自己打了个底,知道自己用anki之后总不可能比之前更差,而且卡片内容是自己已经过了几遍的,也不会说看着几千张卡片手足无措、感觉黑暗一眼望不到头,至于说成就感,我过去10天里把纸质书过了几遍就已经很有成就感了。
至于做好的卡片,我不知道分享出来是否有侵权的嫌疑,就不分享了。认真思考一下,用我上面的代码自己也能做出来。
参考资料
[1] R语言实战(第2版)
[2] “简单粗暴”的R语言爬虫·其一 https://zhuanlan.zhihu.com/p/77777024
[3] R – xmlnodeset output into dataframe or table https://stackoverflow.com/questions/37960580/r-xmlnodeset-output-into-dataframe-or-table
[4] JavaScript实现文章隔字挖空及与Anki结合使用 - 侠客