Kindle 笔记整理 - Shitao Wu

Kindle 笔记整理 - Shitao Wu

加载包

导入My Clippings.txt文件

数据操作

数据皆可tibble()

识别“笔记块”

向整洁数据进军

写出到文件

结果

更新 3.0 版本(2023-02-01)

更新 2.0 版本(2022-09-12)

Kindle要停运了,对于用kindle看电子书的小伙伴影响估计不小。之前买过一年亚马逊书店会员,书籍的高亮内容可以导出到邮箱,还算方便。后来自己通过calibre往kindle里边导入电子书,没有再购买过会员,现在都是飞行模式。

自己导入的电子书,不支持自动导出笔记,Kindle会把划线的笔记全部记录在kindle\documents\My Clippings.txt这个文件中。由于它不区分书籍,只是不断地把划线的内容添加到文件最后,相当于是增量备份,导致它非常杂乱1。对于喜欢把书摘按书籍分类的小伙伴,有大活要干。

Figure 1: My Clipping

作为致力于给自己贴上Data Scientist标签的俺,用R写了个脚本,目标是将My Clippings.txt文件中的所有笔记根据书名和笔记在书中的位置先后进行整理,同时剔除重复笔记2,最后导出每本书的笔记文件。

数据处理的流程如下。

加载包

library(tidyverse)

library(lubridate)

tidyverse是我在数据整理中绕不开的包,每用一次,都要感谢一下Hadly Wickham;lubridate包用于处理时间数据。

导入My Clippings.txt文件

text <- readLines(con = "My Clippings.txt")

按行读取文本文件。

数据操作

数据皆可tibble()

tibble是tidyverse版本的数据框,优点包括:

方便打印预览

标题可以是字符开头

……

text %>% as_tibble()

## # A tibble: 4,650 × 1

## value

##

## 1 "爱的艺术 (〔美〕弗洛姆)"

## 2 "- 您在第 2 页(位置 #22-25)的标注 | 添加于 2022年1月17日星期一 下午11:04:0…

## 3 ""

## 4 "爱不是一种只需投入身心就可获得的感情,如果不努力发展自己的全部人格并以此达…

## 5 "=========="

## 6 "爱的艺术 (〔美〕弗洛姆)"

## 7 "- 您在第 3 页(位置 #37-37)的标注 | 添加于 2022年1月17日星期一 下午11:05:2…

## 8 ""

## 9 "爱情不是一种与人的成熟程度无关,只需要投入身心的感情"

## 10 "=========="

## # … with 4,640 more rows

识别“笔记块”

转为tibble后可以看到每5行是一条完整记录,且称呼为“笔记块”吧。

“笔记块”中依次分别是:

书名

笔记信息(位置、时间)

空行

笔记内容

分割线

前4条各自都不同,但是分割线都是一样的,所以从分割线切入,添加新列,每条分割线所在行填充唯一的值3,分割线以上直至新的分割线中间行填充分割线的值。

text2 <- text %>%

as_tibble() %>%

mutate(group = ifelse(value == "==========",

row_number(value), NA)) %>%

fill(group, .direction = "up")

text2

## # A tibble: 4,650 × 2

## value group

##

## 1 "爱的艺术 (〔美〕弗洛姆)" 1878

## 2 "- 您在第 2 页(位置 #22-25)的标注 | 添加于 2022年1月17日星期一 下午1… 1878

## 3 "" 1878

## 4 "爱不是一种只需投入身心就可获得的感情,如果不努力发展自己的全部人格并… 1878

## 5 "==========" 1878

## 6 "爱的艺术 (〔美〕弗洛姆)" 1879

## 7 "- 您在第 3 页(位置 #37-37)的标注 | 添加于 2022年1月17日星期一 下午1… 1879

## 8 "" 1879

## 9 "爱情不是一种与人的成熟程度无关,只需要投入身心的感情" 1879

## 10 "==========" 1879

## # … with 4,640 more rows

向整洁数据进军

整洁数据(Wickham 2014)是数据清洗的目标,整洁数据的特点是:

每个变量构成一列

每个观测构成一行

每个观测的每个变量值构成一个单元格

接下来添加type列,然后通过pivot_wider()拉宽数据框。

text3 <- text2 %>%

add_column(type = rep(

c("title", "information", "empty", "text", "line"),

nrow(text2) / 5)) %>%

pivot_wider(names_from = "type",

values_from = "value")

text3

## # A tibble: 930 × 6

## group title information empty text line

##

## 1 1878 爱的艺术 (〔美〕弗洛姆) - 您在第 2 页(位置 #22-25)… "" 爱不… ====…

## 2 1879 爱的艺术 (〔美〕弗洛姆) - 您在第 3 页(位置 #37-37)… "" 爱情… ====…

## 3 1880 爱的艺术 (〔美〕弗洛姆) - 您在第 3 页(位置 #37-37)… "" 爱情… ====…

## 4 1881 爱的艺术 (〔美〕弗洛姆) - 您在第 3 页(位置 #37-40)… "" 这本… ====…

## 5 1882 爱的艺术 (〔美〕弗洛姆) - 您在第 6 页(位置 #86-86)… "" 大多… ====…

## 6 1883 爱的艺术 (〔美〕弗洛姆) - 您在第 6 页(位置 #90-91)… "" 事实… ====…

## 7 1884 爱的艺术 (〔美〕弗洛姆) - 您在第 8 页(位置 #109-110… "" 事实… ====…

## 8 1885 爱的艺术 (〔美〕弗洛姆) - 您在第 8 页(位置 #109-110… "" 人与… ====…

## 9 1886 爱的艺术 (〔美〕弗洛姆) - 您在第 8 页(位置 #109-110… "" 人与… ====…

## 10 1887 爱的艺术 (〔美〕弗洛姆) - 您在第 8 页(位置 #118-119… "" 人们… ====…

## # … with 920 more rows

通过正则表达式从information中提取日期、时间、笔记起始位置。过程中发现kindle的日期是不是24小时制的,分上下午。所以也利用正则提取了是否为上下午的信息:如果是下午,则时间都加上12小时;如果不是下午却又是上午12时(凌晨0点),那就减12小时;其余保持不变。

根据书名分组,并按照笔记起始位置排序,重复笔记只保留最后一次修改。中间用到了数据库的合并操作。

delete <- text3 %>%

mutate(date = str_extract(information,

pattern = "\\d*年\\d*月\\d*日"),

time = str_extract(information,

pattern = "\\d*:\\d*:\\d*"),

datetime = ymd_hms(paste(date, time)),

afternoon = ifelse(str_detect(information, "下午"),

1, 0),

datetime = case_when(

afternoon == 1 ~ datetime + hours(12),

afternoon == 0 & hour(datetime) == 12 ~ datetime - hours(12),

TRUE ~ datetime

),

location = str_extract(information,

pattern = "(?<=#)\\d*(?=\\-)") %>%

as.numeric()) %>%

group_by(title) %>%

arrange(location, datetime, .by_group = TRUE) %>%

filter((location == lead(location) &

substr(text, 1, 5) == substr(lead(text), 1, 5)))

text4 <- text3 %>%

mutate(date = str_extract(information,

pattern = "\\d*年\\d*月\\d*日"),

time = str_extract(information,

pattern = "\\d*:\\d*:\\d*"),

datetime = ymd_hms(paste(date, time)),

afternoon = ifelse(str_detect(information, "下午"),

1, 0),

datetime = case_when(

afternoon == 1 ~ datetime + hours(12),

afternoon == 0 & hour(datetime) == 12 ~ datetime - hours(12),

TRUE ~ datetime

),

location = str_extract(information,

pattern = "(?<=#)\\d*(?=\\-)") %>%

as.numeric()) %>%

group_by(title) %>%

arrange(location, datetime, .by_group = TRUE) %>%

anti_join(delete) %>%

ungroup() %>%

mutate(id = row_number(),

line = str_replace_all(line, "=", "-"))

text4

## # A tibble: 718 × 12

## group title infor…¹ empty text line date time datetime after…²

##

## 1 1878 爱的… - 您在… "" 爱不… ----… 2022… 11:0… 2022-01-17 23:04:00 1

## 2 2004 25《… - 您在… "" 生命… ----… 2022… 11:0… 2022-02-07 11:01:22 0

## 3 2005 25《… - 您在… "" 人们… ----… 2022… 11:0… 2022-02-07 11:08:25 0

## 4 2006 25《… - 您在… "" 忘了… ----… 2022… 11:1… 2022-02-07 11:10:17 0

## 5 2007 25《… - 您在… "" 相信… ----… 2022… 11:1… 2022-02-07 11:16:44 0

## 6 2008 25《… - 您在… "" 拖延… ----… 2022… 1:04… 2022-02-11 01:04:03 0

## 7 2010 25《… - 您在… "" 那些… ----… 2022… 1:13… 2022-02-11 01:13:00 0

## 8 2011 25《… - 您在… "" 在所… ----… 2022… 1:16… 2022-02-11 01:16:17 0

## 9 2012 25《… - 您在… "" 、每… ----… 2022… 1:21… 2022-02-11 01:21:22 0

## 10 2014 25《… - 您在… "" 个、… ----… 2022… 1:21… 2022-02-11 01:21:32 0

## # … with 708 more rows, 2 more variables: location , id , and

## # abbreviated variable names ¹​information, ²​afternoon

写出到文件

在text4数据框中存放了所有的笔记,现在要按照书名分别写出到对应的文本文件中,这里选择的是Markdown文件,用write.table()写出一列的数据框就可以。

这里遇到的难题是:目前text4处于整洁数据的形态,如何在按照书名分组的情况下,将其整理成一列的数据框,且笔记块的信息对应。

因为Markdown语法需要空一行才能实现换行,所以写出去的每个新笔记块需要由6个部分组成:

时间

空行(用于换行)

笔记内容

空行(用于换行)

分割线

空行(用于换行)

后来想到了类似Excel辅助列的方法。在数据框行合并的同时,给每个笔记块中的行都添加id,其中时间为整数,然后下面的每一行id都加0.1,这样就可以用排序达成目标,效果是这样的:

## # A tibble: 4,308 × 2

## value id

##

## 1 "2022-01-17 23:04:00" 1

## 2 "" 1.1

## 3 "爱不是一种只需投入身心就可获得的感情,如果不努力发展自己的全部人格并… 1.2

## 4 "" 1.3

## 5 "----------" 1.4

## 6 "" 1.5

## 7 "2022-02-07 11:01:22" 2

## 8 "" 2.1

## 9 "生命并非短促,而是我们荒废太多。一生足够漫长,如能悉心投入,足以创造… 2.2

## 10 "" 2.3

## # … with 4,298 more rows

根据书名分组的任务,原本打算用循环了

学艺不精,后来在苦苦寻觅下找到了group_split()函数,柳暗花明又一村。这是个生命周期还处于实验阶段的函数,反正先拿来用了。

dfs <- text4 %>%

mutate(value = as.character(datetime)) %>%

select(title, value, id) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .1)) %>%

rbind(tibble(

title = text4$title,

value = text4$text,

id = text4$id + .2)) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .3)) %>%

rbind(tibble(

title = text4$title,

value = text4$line,

id = text4$id + .4)) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .5)) %>%

arrange(id) %>%

select(title, value) %>%

group_split(title, .keep = FALSE)

最后得到的是一个列表,列表中是以书名分组的笔记数据框,一本书一个数据框,将每一个数据框按书名写出就完成了。

这步我会,请出懒猫purrr:

先从text4中提取出书名index,并形成文件名files。我的kindle书名比较乱,有的带了特殊字符会导致写入报错,所以用正则做了一定的调整。我以后肯定重命名好电子书再导入kindle才怪,看心情。

index <- text4 %>%

distinct(title) %>%

mutate(title = str_extract(title,

pattern = "[^\\s]+"),

title = str_replace_all(title, ":", "_"),

title = str_replace_all(title, "\\?", "_"))

tail(index, 10)

## # A tibble: 10 × 1

## title

##

## 1 百年孤独(根据马尔克斯指定版本翻译,未做任何增删)

## 2 社会学的想象力

## 3 穿透:像社会学家一样思考

## 4 精英的傲慢:好的社会该如何定义成功?(每个关心社会公平的人,请读一读这本书。…

## 5 蒙田随笔全集(许渊冲推荐译本,季羡林,周国平导读,穿越四百多年的人生智慧与生活哲…

## 6 西方现代思想讲义

## 7 观念的力量

## 8 认识世界:古代与中世纪哲学【陈嘉映、刘擎、许知远推荐,“现象级”公共哲学家、畅…

## 9 遥远的救世主:根据本书改编的电视剧《天道》正在全国掀起极大反响

## 10 金瓶梅(崇祯本)

利用字符串函数str_c()形成文件名files。这里我新建了一个files文件夹,把笔记导入到里边,方便打包带走。

files <- str_c("files/", index$title, ".md")

tail(files, 10)

## [1] "files/百年孤独(根据马尔克斯指定版本翻译,未做任何增删).md"

## [2] "files/社会学的想象力.md"

## [3] "files/穿透:像社会学家一样思考.md"

## [4] "files/精英的傲慢:好的社会该如何定义成功?(每个关心社会公平的人,请读一读这本书。哈佛大学教授、《公正》作者迈克尔·桑德尔重磅新作,反思美国梦的破碎,揭示教育的目的、工作的意义).md"

## [5] "files/蒙田随笔全集(许渊冲推荐译本,季羡林,周国平导读,穿越四百多年的人生智慧与生活哲学,开随笔式写作之先河!).md"

## [ reached getOption("max.print") -- omitted 5 entries ]

接下来用purrr中的walk2()函数进行批量写出。

walk2(dfs, files, write.table,

row.names = FALSE,

quote = FALSE,

col.names = FALSE)

结果

Figure 2: 写出结果

Figure 3: 文件内容

要手动改下文件名美美收工咯!

全部代码如下:

text <- readLines(con = "My Clippings.txt")

library(tidyverse)

library(lubridate)

# 文本变成数据框

text2 <- text %>%

as_tibble() %>%

mutate(group = ifelse(value == "==========",

row_number(value), NA)) %>%

fill(group, .direction = "up")

# 数据框清洗,提取标题、信息和文本

text3 <- text2 %>%

add_column(type = rep(

c("title", "information", "empty", "text", "line"),

nrow(text2) / 5)) %>%

pivot_wider(names_from = "type",

values_from = "value")

# 从信息中提取日期、时间、位置并排序,重复笔记只保留最后一次修改

delete <- text3 %>%

mutate(date = str_extract(information,

pattern = "\\d*年\\d*月\\d*日"),

time = str_extract(information,

pattern = "\\d*:\\d*:\\d*"),

datetime = ymd_hms(paste(date, time)),

afternoon = ifelse(str_detect(information, "下午"),

1, 0),

datetime = case_when(

afternoon == 1 ~ datetime + hours(12),

afternoon == 0 & hour(datetime) == 12 ~ datetime - hours(12),

TRUE ~ datetime

),

location = str_extract(information,

pattern = "(?<=#)\\d*(?=\\-)") %>%

as.numeric()) %>%

group_by(title) %>%

arrange(location, datetime, .by_group = TRUE) %>%

filter((location == lead(location) &

substr(text, 1, 5) == substr(lead(text), 1, 5)))

text4 <- text3 %>%

mutate(date = str_extract(information,

pattern = "\\d*年\\d*月\\d*日"),

time = str_extract(information,

pattern = "\\d*:\\d*:\\d*"),

datetime = ymd_hms(paste(date, time)),

afternoon = ifelse(str_detect(information, "下午"),

1, 0),

datetime = case_when(

afternoon == 1 ~ datetime + hours(12),

afternoon == 0 & hour(datetime) == 12 ~ datetime - hours(12),

TRUE ~ datetime

),

location = str_extract(information,

pattern = "(?<=#)\\d*(?=\\-)") %>%

as.numeric()) %>%

group_by(title) %>%

arrange(location, datetime, .by_group = TRUE) %>%

anti_join(delete) %>%

ungroup() %>%

mutate(id = row_number(),

line = str_replace_all(line, "=", "-"))

# 写出到文件

dfs <- text4 %>%

mutate(value = as.character(datetime)) %>%

select(title, value, id) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .1)) %>%

rbind(tibble(

title = text4$title,

value = text4$text,

id = text4$id + .2)) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .3)) %>%

rbind(tibble(

title = text4$title,

value = text4$line,

id = text4$id + .4)) %>%

rbind(tibble(

title = text4$title,

value = text4$empty,

id = text4$id + .5)) %>%

arrange(id) %>%

select(title, value) %>%

group_split(title, .keep = FALSE)

index <- text4 %>%

distinct(title) %>%

mutate(title = str_extract(title,

pattern = "[^\\s]+"),

title = str_replace_all(title, ":", "_"),

title = str_replace_all(title, "\\?", "_"))

files <- str_c("files/", index$title, ".md")

walk2(dfs, files, write.table,

row.names = FALSE,

quote = FALSE,

col.names = FALSE)

Wickham, Hadley. 2014. “Tidy Data.” The Journal of Statistical Software 59. http://www.jstatsoft.org/v59/i10/.

比如:笔记的位置是根据做笔记的时间而不是笔记在书中的位置来的。 ↩︎

把笔记起始位置相同且开头5个字一样的笔记认定为重复笔记,只保留最新的那一条。 ↩︎

这是经过排序的行号,不存在重复。 ↩︎

← 读《北野武的小酒馆》

毕业季 →