R语言实现通过epub/azw3格式电子书快速大批量制作Anki卡片——以《十天搞定考研词汇》为例

昨天是我背《十天搞定考研词汇》这本书的第十天。过去的十天里我每天都花大量的时间在单词上,虽然并没有像书名所说的“搞定”考研词汇,但也确实收获颇丰。十天过去了,往后我要怎么复习单词呢?我想到了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')

提取需要的代码片段:

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代码,这段代码不仅包含了音标、释义等内容,还包含了单词扩展的内容。如图:

输出到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”看一下:

为什么会出现这么多的“\&quot;”???最后的那个引号是什么时候出现的???

经过检查,发现是导入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

留下评论

电子邮件地址不会被公开。 必填项已用*标注