昨天是我背《十天搞定考研词汇》这本书的第十天。过去的十天里我每天都花大量的时间在单词上,虽然并没有像书名所说的“搞定”考研词汇,但也确实收获颇丰。十天过去了,往后我要怎么复习单词呢?我想到了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文件。这里选取一部分:
### List 1
intellect [ˈɪntəlekt] n. 智力;理解力;[总称] 知识分子
派intellectual [ˌɪntəˈlektʃuəl] adj. 智力的;理智的;聪明的
考点搭配intellectual enquiry 知识探索,知识探求
intellectual achievement 知识成就,智力成果
intellectualize [ˌɪntəˈlektʃuəlaɪz] vt. 使…理智化;对…做理性探讨
contempt [kənˈtempt] n. 轻视,轻蔑
派 contemptible [kənˈtemptəbl] adj. 可鄙的;可轻视的
contemptuous [kənˈtemptʃuəs] adj. 轻视的,蔑视的
ultimate [ˈʌltɪmət] adj. 最后的,最终的
yield [jiːld] n. 产量,收获量;收益 v. 出产;屈服
contend [kənˈtend] vi. 竞争,争夺 vt. 坚决主张,声称
如上所示,每一个词条均被包括在一个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')
提取需要的代码片段:
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] effect [ɪˈ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代码,这段代码不仅包含了音标、释义等内容,还包含了单词扩展的内容。如图:
输出到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文件。接下来的设置,我是这样做的:
注意选中“允许在字段中使用HTML”。
导入完成后试一下,卡片没有美化,不过内容显示“好像”是正常的,似乎我们离成功已经很近了。但卡片内容后那一个小小的引号提醒我们,事情并没有我们想象的这么美好。
步骤4:修理bug、调整细节、美化卡片
修理bug1
既然“看起来”一切正常,那剩下要做的就是美化卡片了。
但我在修改卡片css的时候发现,添加的css样式似乎不起作用。打开编辑框,选择“编辑HTML”看一下:
为什么会出现这么多的“"”???最后的那个引号是什么时候出现的???
经过检查,发现是导入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已经很生疏了。效果如下:
看起来好像还不错。但因为源文件的限制,不能做的和纸质书一模一样。
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了。如图:
怎么只显示了音标、释义和扩展,唯独没有“主单词”?
瞬间我就明白过来。上面我们看到,在电子书的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处理问题的逻辑有了进一步的了解。我受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