# 用 Shortcuts 将豆瓣上的书籍信息保存到 Notion 和 CSV

> 本文公开全文。

原文：https://utgd.net/article/20509

吾生也有涯，而知也无涯。

好书太多了，不知道读哪本收益最高，最符合自己的需求。好在豆瓣网站已经整合了无数读者的点评和建议，为我们提供了一个直观的平台来预览书籍信息。通过豆瓣读书，我们可以看到每本书的综合评分、读者评论、及相关介绍。

第二个问题随之而来，待读书单里已有 100 多本书了，一个个翻网页去比对这也太麻烦了吧？如果能将书籍信息汇总到一个表格里，进行筛选和排序就很方便了。Notion Database 擅长做这个事，通过创建个性化的数据库，辅以筛选和排序功能，轻松地对待读书单进行管理。

那么，如何将豆瓣上的书籍信息方便的录入到 Notion 中呢？Shortcuts 可以将二者一拍即合。

用 Shortcuts 从豆瓣抓取书籍信息，然后通过 API 保存到 Notion 中。只需要几次点击，就可以建立自己的图书馆。

早期我是将所有信息存在 CSV 中，每次打开总要在 Numbers 中操作一番显示格式和过滤，不如 Notion 方便。所以现在调整为直接保存到 Notion，留一份备份在 CSV 文件中，线上线下兼得。

得益于 Apple 的全平台特性，本套方案无需任何修改即可在 Mac 上使用：

在 Safari 中打开豆瓣书籍页面——分享——Shortcuts——运行获取豆瓣书籍信息的 Shortcuts 即可。不管在哪里看到好书，随时抓取到自己的书库里~

⏬ Shortcuts 下载

安装后填入自己的 Notion API key 和 Database id。

## 从豆瓣获取书籍信息

### 创建 Database

在 Notion 中新建一个 Full page Database，比对着豆瓣网页上提供的书籍信息，建立自己希望保存的属性。并为属性选择合适的数据类型，稍后我们将书籍信息保存到这个 Notion Database。

打开 Database 模板 链接，点击右上角的 “Duplicate” 可以直接复制我的模板到自己的 Notion 中。

“状态”和“种类”是我自己添加的 Database 属性，用来帮助筛选书籍。

“状态”是一个标签，用来表示是否打算阅读一本书，只有一种状态就是“看”。目前想看的书我会标记上“看”，进行初步筛选。

关于如何使用标签，Minja 在《3\-7 标签作为一种管理工具：指示性标签》有详细的说明。

> 再如阅读文章，我就只有一个维度：看，没有什么稍后看、过几天看以后再看、放进愿望清单这种无穷倒退式的标签分类。

UNTAG 会员文章《会员讨论：星标、旗标、优先级》也有过相关话题的讨论。

“种类”用来根据自己情况进行书籍分类，也是用来筛选书籍的一种手段。例如想读小说时，肯定要把编程类书籍过滤掉。

### 获取信息

#### 1\. 书名、评分、评价人数

建立好 Database 后，先从第一个 Database 属性——书名，开始从豆瓣网页上获取数据。

以《也许你该找个人聊聊》这本书的详细页面为例：

在书名上右键——Inspect——在高亮的 HTML 代码上右键——Copy——Copy JS path，会得到如下的代码

```
document.querySelector("#wrapper > h1 > span”)
```

在这行代码的后面加上一个 \.textContent，可以获取上面查找到的 HTML 元素的文本内容。

把组合后的代码输入到 Console，按下回车，你会得到书名：

关于如何定位元素、什么是 JS path、什么是 document，见旧文《保存网页时一键裁切不必要的部分》有详细说明，本文不做详细介绍。

接下来如法炮制，获取评分、评价人数：

代码如下：

- 评分：document\.querySelector\("\#interest\_sectl \> div \> div\.rating\_self\.clearfix \> strong"\)\.textContent

- 评价人数：document\.querySelector\("\#interest\_sectl \> div \> div\.rating\_self\.clearfix \> div \> div\.rating\_sum \> span \> a \> span"\)\.textContent

这个时候只要在 Shortcuts 中简单组装一下上面的代码，就拥有了简易版的获取豆瓣书籍评分工具

```
// 获取书名
bookTitle = document.querySelector("#wrapper > h1 > span").textContent;
// 获取评分
rating = document.querySelector("#interest_sectl > div > div.rating_self.clearfix > strong").textContent
// 获取评价人数
reviewCount = document.querySelector("#interest_sectl > div > div.rating_self.clearfix > div > div.rating_sum > span > a > span").textContent

// 输出书籍信息
let book = `${bookTitle},${rating},${reviewCount}
// 调用Completion以完成
completion(book);
```

通过 Shortcuts 在网页上运行 JavaScript 若要返回数据，必须在 JavaScript 中调用完成处理程序，即代码中最后一行的 completion\(book\)。

有关 “在网页上运行 JavaScript 动作” 的一些技术细节可以阅读苹果官方文档。

注意：使用时先点击右上角绿色计算机图标，将页面从手机端切换为电脑端，再运行 Shortcuts。手机端访问时默认呈现的页面缺少了很多信息。

简易版豆瓣书籍评分 Shortcuts 下载

对整体流程有了初步的了解之后，我们继续获取其他信息。

#### 2\. 书籍信息：作者、出版社、出版年、页数、ISBN

还是按照前面选择元素的方法，分别在作者、出版社、出版年上右键\.\.\.（省略后续步骤），可以发现这些信息都属于 info 这个元素：

那能不能直接获取 info 中的文本然后再想办法拆分呢？

用 info 的 JS path 测试了一下是可以的：

获取了全部文本，如何提取为结构化的内容呢？这种困难的工作就交给 GPT 老师好了。

经过几次调试和经过获取 100 多本书信息的准确性验证后，这是最终的代码：

```
// 获取书籍信息
let info = document.querySelector("#info").textContent;
// 用于存储提取的键值对信息
let infoJson = {};
// 正则表达式，用于匹配“key: value”这样的字符串
// (.+): 捕获一或多个任意字符（代表键），后面跟着一个冒号
// \s* 匹配零个或多个空白字符
// (.+) 捕获一或多个任意字符（代表值）
// g 表示在文本中找到所有的匹配项
let regex = /(.+):\s*(.+)/g;
// 用于存储每次正则表达式匹配的结果
let match;
// 使用 while 循环重复地在 info 文本内容中查找与正则表达式匹配的项
while ((match = regex.exec(info)) !== null) {
  // 从匹配的结果获取第一个捕获组（即键），使用trim()删除前后的空白字符
  let key = match[1].trim();
  // 从匹配的结果中获取第二个捕获组（即值），删除前后的空白字符
  let value = match[2].trim();
  // 将键和值添加到 infoJson 字典中
  infoJson[key] = value;
}

// 在 Console 输出 infoJson
console.log(infoJson);
```

输入到 Console 中试试。

感谢 GPT 老师。

#### 3\. 内容简介

接着往下看，这次是取内容简介。

仍然按照：右键——Inspect\.\.\.的流程。注意要取 \<div class="indent" id="link\-report"\> 部分的 JS path，下方的 \<div class="intro"\> 有些书籍会没有这部分。

之前从查找到的 HTML 元素获取文本内容使用的是 \.textContent，这里稍做变化，改为 \.innerText。不知道什么原因，豆瓣网站在“内容简介”中使用 CSS 代码隐藏了一部分文本，直接使用 \.textContent 会取得多余文本。innerText 与 textContent 稍有不同，它返回元素的“渲染”文本——即页面上实际显示给用户的文本，而不包括那些隐藏的文本。

还有一点细节要注意，内容简介中可能会有 " 号，在保存为 CSV 文件时会影响格式，需要将 " 号替换为 ' 单引号。

总结以上流程的代码为：

```
// 获取内容简介的“渲染”文本
let bookDescription = document.querySelector("#link-report").innerText
// 使用 replace 正则表达式替换 bookDescription 中的所有双引号（"）为单引号（'）
bookDescription = bookDescription.replace(/"/g, "'");

// 在 Console 输出 bookDescription
console.log(bookDescription);
```

搞定。

#### 4\. 热门短评

来到获取数据的最后一步了，热门短评。

照例在热门短评上右键，Inspect\.\.\.

替换其中的 " 号为 ' 单引号。

```
// 获取热门短评的“渲染”文本
let hotShortComments = document.querySelector("#comment-list-wrapper").innerText;
// 替换所有双引号（"）为单引号（'）
hotShortComments = hotShortComments.replace(/"/g, "'");
console.log(hotShortComments);
```

搞定，进入下一环节。

#### 代码整理

这里只说明必要环节，完整代码在下面部分给出。

Database 中还有一个条目没有获取，就是当前页面的链接，这个非常简单，只要一行 JavaScript 代码就可以

```
// 获取当前页面的链接
link = window.location.href;
console.log(link);
```

将所有抓取的信息组合为一个 JSON 字典方便在 Shortcuts 中根据通过键名调用。

```
// 将所有抓取的信息组合为一个 JSON 字典
let book = {
  bookTitle: bookTitle,
  rating: rating,
  reviewCount: reviewCount,
  pageCount: infoJson["页数"],
  description: bookDescription,
  hotComments: hotShortComments,
  ISBN: infoJson["ISBN"],
  author: infoJson["作者"],
  publicationYear: infoJson["出版年"],
  publisher: infoJson["出版社"],
  link: link,
  csvRow: `"${bookTitle}","${rating}","${reviewCount}","${infoJson["页数"]}","状态","种类","${bookDescription}","${hotShortComments}","${infoJson["ISBN"]}","${infoJson["作者"]}","${infoJson["出版年"]}","${infoJson["出版社"]}","${link}"`,
};
```

在整个 book 字典中，我额外添加了一个包含所有书籍信息的字符串 csvRow，其中都是用逗号分割的文本，利用这个长字符串可以直接将书籍信息保存为 CSV 文件。

在 console 中测试一下

成功获取到所有信息。

截止这部分的完整代码见 Github

#### 处理异常

在获取网页上的信息时，经常会遇到各种意外情况，可能是元素没有成功加载，也可能是网页上没有这个元素，所以我们要进行一些异常处理，来确保当意外发生时不影响代码的正常运行。

这个非常简单，只要通过 try\.\.\.catch 语句包围原本的代码即可。try 部分尝试执行操作，如果这个操作成功了，那么一切正常，bookTitle 会存储元素的文本内容。但是，如果在尝试执行代码时发生了错误，则 catch 块中的代码会被执行。在 catch 块中，bookTitle 被赋予了一个默认值"N/A"。这样，即使出错，bookTitle 以一个默认值继续运行程序。

还有一点要注意的是，要在 try\{\} 外面声明变量 bookTitle。这是因为 JavaScript 的作用域规则。在 try 块里面声明变量会导致这个变量只能在 try 块里面访问得到，也就是说它的作用域被限制在了 try 块内。在 catch 块或者 try 块之后的代码中就无法访问到 bookTitle 变量了。

增加处理异常后的完整代码见 Github

获取完所有信息后，就是保存到 Notion 了。

## 保存书籍信息到 Notion

### 创建 Notion 集成（integration）

Notion 通过集成来实现 API 自动化操作。在 Notion My integrations 创建一个新的集成。默认在当前工作区创建集成，随便填一个名字，点击 Submit，保存生成的 API 密钥。

进入之前创建的待读书单 Database 页面，点击右上角 \.\.\. 向下滚动到 \+ Add Connections，搜索刚才创建的集成并选择它。

### 了解 Notion API

浏览 Notion API 文档，Create a page 看起来像是我们需要的。

简单浏览了一下，了解到创建作数据库的子级的新页面需要使用 POST 方法。 API 请求必须提供 database\_id、properties 和 Notion\-Version 参数，请求成功时返回 200 状态码。

在文档中的另一处 Working with databases 部分，说明了如何获取 database\_id\:

> 在 Notion 中将数据库作为整页打开。使用 Share 菜单来 Copy link 。现在将链接粘贴到文本编辑器中，以便您可以仔细查看。URL 使用以下格式： https\://www\.notion\.so/\{workspace\_name\}/\{database\_id\}?v=\{view\_id\} 在您粘贴的 URL 中找到与 \{database\_id\} 对应的部分。它是一个 36 个字符长的字符串。该值是您的数据库 ID。

了解到必要的信息后，看一下右侧的 API 请求示例。这个示例有些繁琐，提供了很多不必要的信息，先将它精简一下，便于我们学习如何使用。

Content\-Type、icon、cover、children 这几个参数都是不需要的。

文档中还有一句很重要的话：“ 如果新页面是现有数据库的子级，则 properties 对象主体参数的键必须与父数据库的属性匹配。”也就是说，在提交数据时要同时提交数据库属性的数据类型。

这句话在右侧的例子中的应用是：Price 中要先写 number 然后才是数值 2\.5，不能直接写 2\.5。

根据以上获得的信息可以制作出一个最简单的 Shortcuts 用来测试 API 请求：

在 Shortcuts 中构造 Notion API 请求是个手工活儿~ 需仔仔细细的正确选择每个选项。

成功向 Notion 发送数据后，会返回一个 JSON 响应。通过这个响应可以大概了解向 Notion 发送了什么数据。

### 向 Notion Database 发送书籍信息

先从网页的返回值中获取字典（Shortcuts 中动作名字是“从输入中获取词典”），然后将字典中的每个值保存为变量，以便在 API 请求中使用。

截图中仅展示了部分，未截取完整截图。

在我们的 Database 中一共有 13 个字段，将其按照类型分类如下：

- title 类型 1 个：书名

- text 类型 5 个：内容简介、热门短评、作者、出版年、出版社

- number 类型 4 个：评分、评价人数、页数、ISBN

- url 类型 1 个：链接

- 不需要填写 2 个：状态、种类

每一种类型对应一种 API 写法，一共需要 4 种，这 4 种也是比较常用的 Database 属性。

根据类型来构造 API 请求，红框部分依次对应 title、text、number、url 类型该如何在 Shortcuts 中构造请求，仔细地的填入正确的选项即可。

在下方可以加上一个“显示结果”动作，方便了解 Shortcuts 运行结果。

### 加一道保险

为了避免碰上 Notion 无法访问的情况，可以在请求 API 之前，先将数据保存一份 CSV 到本地。

想进一步了解 Shortcuts 和 CSV 可阅读《Shortcuts 结合 CSV，在本地创建、读取和修改表格》

---

[前往标准阅读页](https://utgd.net/article/20509?audience=web)
