【35】Chrome 书签批量导入 Notion

Notion

Notion 是一款万能的内容工具。关于 Notion 的深入介绍可以看少数派上的超多介绍文章,这是其中一篇:Notion:重新定义数字笔记 - 少数派

为什么使用 Notion?

Notion 的哲学是万物皆块(Block)。有相当多的数据形式,他们皆是以块的形式展现,可以在其他的块(页、数据库等)里以任意形式排列组合。丰富的数据类型加上自由的组合方式,使得 Notion 成为了一个相当强大的内容记录/管理工具,你可以随心所欲的为你的需求打造对应的记录方式。

还有一个很重要的点,Notion 好看。这一点也让 Notion 可以直接成为一个博客,或者是某种公开的展示页,而省的还要另外去搭网站之类。

Notion 也有很棒的团队协作功能,不过现在作为单机玩家还没机会体会这一点。

Notion 的缺点?

  1. 连接速度慢。毕竟是境外网站,不过好在咱能科学上网。
  2. 移动端 app 性能差,桌面端 app 就是个套壳。网页端才是本体,幸亏足够好用。
  3. 功能并不够完善,且没有提供官方 API。足够自由的形式固然好,但是没有配套的快捷操作/批量操作等,再自由都是白搭。想自己实现自定义功能,又没有 API 能用。期待早日开放 API 吧。
  4. 不支持 Markdown。虽然它支持“类 Markdown”快捷键,但是终究不是 Markdown。

这里是 Notion 的更新记录:What’s New?

这些缺点使 Notion 成为了一个瘸腿皇帝。我把一些不需要经常查看的数据(订阅管理、书单等)迁移到了 Notion 上,但是 TODO、快速笔记等还是保留在了更快速更轻量的 Google Keep 上,笔记还是单独使用 Markdown 文件线下管理。

目标

我在 Chrome 的收藏夹里有将近 500 个书签。这些书签里有相当多有意思的文章、项目等等,但是仅靠收藏夹的文件夹很难细致的把这些书签分门别类进行整理归类。

因此目标已经相当明确:把 Chrome 收藏夹里的书签导入 Notion,打造一个知识库。

方法

Notion 的数据库支持使用 csv 文件直接导入,因此把书签转为 csv 格式就可以一键导入了。

首先需要从 Chrome 导出书签。在书签管理器的右上角三点菜单中,可以找到导出书签的选项。导出后就会拿到一个命名格式类似 bookmarks_YYYY_M_D.html 的 HTML 文件,点开可以看到里面放着我们所有的书签。

查看这个文件的源代码,可以看到每个书签都有名称、链接、时间、缩略图四个信息,知道这一点就可以写个 Python 脚本提取了。不过奇怪的是,这个文件中有一部分标签没有正确闭合,这给之后的解析带来很多麻烦。为了方便起见,把这个 HTML 文件用 Chrome 打开,Ctrl-S 再把这个网页另存为一个新的文件。重新保存以后,该闭合的标签都正确闭合了,可以用脚本解析了。

我使用了 bs4 做文档树的解析。源码中可以看到链接的 <a> 标签都是放在嵌套的 <dl><dt> 列表中,一个简单的递归就可以完成对文件夹结构的提取。脚本代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import csv
import time
from bs4 import BeautifulSoup  # type: ignore
from typing import List

with open("Bookmarks.html", "r", encoding="utf-8") as f:
    soup = BeautifulSoup(f, features="html.parser")

link_list: List[List[str]] = list()

def parse_folder(folders: BeautifulSoup, tags: List[str]) -> None:
    contents = folders.find("dl", recursive=False).find_all("dt", recursive=False)
    for content in contents:
        h3 = content.find("h3")
        if h3 is not None:
            parse_folder(content, tags + [h3.string])
            continue
        a = content.find("a", recursive=True)

        # 0. Name
        link_name = a.string

        # 1. Link
        link_href = a["href"]

        # 2. Created time
        time_local = time.localtime(int(a["add_date"]))
        link_date = time.strftime("%Y-%m-%dT%H:%M:%S+08:00", time_local)  # ISO-8601

        # # 3. Cover picture
        # try:
        #     link_cover = a["icon"]
        # except KeyError:
        #     link_cover = ""

        link = [link_name, link_href, ",".join(tags), link_date]
        link_list.append(link)

parse_folder(soup.body, [])

with open("bookmarks.csv", "w", newline='', encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["Name", "Link", "Tags", "Created"])
    writer.writerows(link_list)

缩略图是 base64 的 png 数据,由于 Notion 尚不支持这种数据的导入,所以就没有放进 csv。csv 文件包括了四列,分别是名称,链接,标签和创建时间。其中,标签包括了这一书签所属的所有母文件夹的名称。创建时间使用了 ISO-8601 标准的格式,Notion 在导入的时候会自动识别为时间类型。

生成好 csv 文件,到 Notion 中导入,就得到了一个整齐的数据库!

遗憾

首先是时间类型的处理。我在 csv 中使用 ISO-8601 的标准让 Notion 能够自动识别为时间类型,然而 Notion 在导入时间后会把所有的时间转化为 GMT 时间,并且目前没有办法批量更改为 GMT+8(批量选中后更改时区会让选中的所有时间变成选中的第一个时间)。

另外,Notion 并不支持更改一个块的“创建时间”属性。Notion 在导入时间类型的时候使用的是“Date”格式,而创建时间是另一个单独的类型“Created Time”,这个属性目前是不支持更改的(估计以后也不会让你修改)。如果只是用这么一个 Date 类型的时间,后续往数据库里加入新书签的时候又需要手动设置时间,多了一步麻烦。总结起来,如果保留 Chrome 提供的创建时间数据,在之后要么麻烦,要么丑。

光一个时间就这么多麻烦事,所以大概还是删掉这个属性比较好了。

还有一个遗憾在于链接预览。Notion 有两个很酷的东西:一个是 bookmark 类型的块,还有一个是 Web Clipper 插件。bookmark 类型的块可以填入一个链接,然后生成对应的预览图和简介文字。可惜的是,这一效果只能手动选择 bookmark 块,手动输入链接后才行,而不能对数据库里的链接批量生成。Web Clipper 插件则是可以把正在浏览的网页内容保留格式的复制到 Notion 中,然而同样的,这没有办法批量生成。目前唯一可行的方法,大概就是手动去把我这几百条书签一条条生成类似的东西。(一点也不可行啊喂)

目前 Notion 还有一个比较烦人的 bug(feature?)。从一个数据库里批量把一堆项目选中拖到另一个数据库里以后,数据的顺序竟然会变…?而且是一个无法预测的随机变化… 我想要把一个大数据库手动拆到几个小数据库的想法直接就泡汤了。(书签的顺序对我还是蛮重要的)

就像之前说的,Notion 并没有特别完善的功能,也没有开放 API,这使得大量数据的批量操作比较艰难。倒也不是完全不能用,但是就是在细节上差那么一点点,让完美主义者和强迫症心有不甘。也做不了什么,只能期待 API 早日开放吧!有完整 API 的 Notion,我是真的不敢想象能够强大到什么程度。

Update:Notion 的客户反馈是真人而且态度超级棒!在工作区右下角那个问号图标里选 Send us a message 就可以和他们的团队成员直接交流了!一般他们都会在一天内给回复。我已经反馈了数据库顺序的问题和自动生成书签预览的建议,等他们更新咯。


Update 2:为了解决从一个数据库拖到另一个数据库顺序会变的问题,我进一步魔改了代码,让这个脚本能够按照顶层文件夹分别生成独立的 csv 文件。代码主体基本没有变化,主要是修改了存储所有链接的 link_list 结构。代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import csv
import time
from bs4 import BeautifulSoup  # type: ignore
from typing import List, Dict

with open("Bookmarks.html", "r", encoding="utf-8") as f:
    soup = BeautifulSoup(f, features="html.parser")

link_list: Dict[str, List[List[str]]] = dict()

def parse_folder(folders: BeautifulSoup, tags: List[str]) -> None:
    contents = folders.find("dl", recursive=False).find_all("dt", recursive=False)
    for content in contents:
        h3 = content.find("h3")
        if h3 is not None:
            if not tags:
                link_list[h3.string] = list()
            parse_folder(content, tags + [h3.string])
            continue
        a = content.find("a", recursive=True)

        # 0. Name
        link_name = a.string

        # 1. Link
        link_href = a["href"]

        # 2. Created time
        time_local = time.localtime(int(a["add_date"]))
        link_date = time.strftime("%Y-%m-%dT%H:%M:%S+08:00", time_local)  # ISO-8601

        # # 3. Cover picture
        # try:
        #     link_cover = a["icon"]
        # except KeyError:
        #     link_cover = ""

        link = [link_name, link_href, ",".join(tags), link_date]
        link_list[tags[0]].append(link)

parse_folder(soup.body, [])

keys = link_list.keys()
for key in keys:
    with open(f"./Bookmarks/{key.replace('/', '')}.csv", "w", newline='', encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["Name", "Link", "Tags", "Created Time"])
        writer.writerows(link_list[key][::-1])

本文阅读量
本站访客量