☰
微信读书书摘同步到Notion:Python脚本与API自动化实战
2026/10/28 5:23:30 网站建设 项目流程

把微信读书里的书摘和书架同步到 Notion,这个事我琢磨了不少时间。微信读书的书摘做得不错,划线、笔记、想法都挺顺手,但数据一直被关在 App 里,想按主题回顾、跟自己的知识库打通,基本只能手动复制粘贴。Notion 这边则是另一个极端,数据库、公式、双向链接、看板视图全都给你,但没有现成的微信读书导入入口。这个项目就是干一件事:让微信读书里的阅读数据自动流进 Notion,变成一份能持续更新、能统计、能检索的个人阅读数据中心。如果你正在用 Notion 管理阅读笔记,或者想学 Python 脚本加 API 自动化,这篇内容可以直接照着做。

我不打算写一套“云端全家桶”的重量级方案,就聚焦在两条路径的组合上:模拟微信读书 Web 端登录态抓取用户自己的书架与书摘数据,再用 Notion 官方 API 写入指定数据库。整个过程我会拆成六块:思路选型、基础准备、核心代码、自动化部署、数据库模板、问题排查。每一块都会包含我实测过的东西和踩过坑之后才明白的细节。

1. 项目思路与方案选型

1.1 为什么要把微信读书的书摘搬到 Notion

先说需求。微信读书的书摘功能对“读的时候顺手划线”这件事很友好,但对“读完之后回头看”很不友好。你在 App 里能看到某一本书的所有划线,也能看到所有笔记,但当你跨书搜索,比如想找所有关于“注意力”相关的内容,就只能一本一本翻,效率很低。而且微信读书的导出能力有限,自带的导出格式比较粗糙,无法保留章节层级,也很难接入其他知识管理工具。

Notion 的价值在于它是一个“数据容器”。书籍可以是一条条数据库记录,书摘可以是子记录或子块,每本书可以关联多篇书摘,每篇书摘可以被贴标签,整个库还可以用视图切换成表格、看板、画廊甚至日历。这意味着你读过的所有内容可以在一个统一结构里被检索、被关联、被二次加工。把微信读书数据放进去之后,我还可以给每本书打上“领域”标签,比如产品、心理、编程,然后按标签聚合所有划线内容。这种能力在读书 App 里基本做不到。

我自己的实际情况是:书架上有几百本书,划线笔记累计几千条。最初几年靠人工整理,整理了不到十分之一就放弃了。后来试过各种导出工具,有的输出格式混乱,有的停更许久,有的只支持单一平台。这个项目最终锁定的目标很简单:实现对个人微信读书数据的安全、自动、幂等同步,每天一次,跑完就结束,错误能通知,不会重复写入。

1.2 技术路线对比:为什么选“接口抓取 + API 写入”

动手之前,我先列了三条技术路线对比了一下:

方案优点缺点结论
官方导出 + 手动整理零开发导出格式有限,无法自动化,费时只适合一次性迁移
完整爬虫抓取微信读书页面数据全解析复杂,依赖页面结构,维护成本高,风险偏高不推荐
登录态下调用微信读书 Web 接口抓取本人数据 + Notion API 写入数据可控、可定时、可增量依赖 Cookie 和接口结构,需要更新 Cookie平衡性好,最终采用

核心判断点在于“自动化”和“稳定性”。我需要的是一个能每天跑、不用我每天盯着的流程。手动导出的方案再完美,只要需要人类介入,就注定很难长期坚持。完整爬虫方案虽然能拿到更多页面级数据,但代价是要把前端渲染出的 HTML 结构变化也纳入维护范围,前端一改版,爬虫就崩,这是最让人疲惫的维护场景。相比之下,Web 接口通常更稳定,返回 JSON 结构也相对清晰,配合定时任务和重试机制,能实现“一次授权、定时执行”的长期运行。

这条路线还有个容易被忽视的好处:因为是登录态下的接口请求,拿到的数据天然就是用户自己的,不需要处理复杂的跨用户授权逻辑,也不涉及任何平台限制绕过问题。要注意的是,抓取频率不能太高,毕竟任何平台都不希望自己的服务被高频轮询。我把同步频率控制在每天一次,实测下来稳定,也没有触发过任何限制。

2. 核心基础准备

2.1 微信读书数据获取机制

微信读书的 Web 端登录态通常保存在 Cookie 中。你只需要在浏览器里完成一次登录,把登录后产生的有效 Cookie 提取出来,后续脚本请求就会以你的身份读取数据。整个过程可以理解为“使用你自己的账号,读取你自己的书架与书摘”。

书架数据主要来自书架接口,返回内容一般包含书籍 ID、书名、作者、封面 URL、阅读状态等字段。书摘数据则按书籍维度获取,每本书需要单独请求,返回内容里通常包含划线内容、章节标题、划线时间、笔记类型等信息。由于书摘可能较多,接口大概率会有分页参数,比如基于 cursor 或页码的翻页逻辑,因此脚本里需要做循环拉取。

提示:微信读书接口路径会随版本迭代变动。我在代码注释里保留的是“书架接口与书摘接口”的语义命名,具体路径建议实现时先用浏览器的开发者工具观察一次真实请求,再填入脚本。不要直接照着网上的旧教程死抄。

一个关键细节是:书摘类型有区别。有的是纯划线,有的是划线加笔记,有的是书籍想法。这几种数据在接口里的字段名、内容形式、甚至优先级都可能不同。我把“笔记”和“划线”分开处理:纯划线只摘录原文,划线加笔记则额外保存我的想法,书籍想法单独作为一条记录。这样做的好处是,在 Notion 里我可以区分“这本书哪里引起了我的注意”和“我对此有什么思考”。

2.2 Notion API 接入三板斧

Notion API 的使用门槛不高,但第一次接都会卡在权限和页面发现上。整个过程只需要三步:

第一步,在 Notion 里创建一个 Internal Integration。打开 Notion 的 Settings,进入 Connections,点击 New integration,输入名称,选择关联的工作空间,提交后就能看到一串 Internal Integration Token。这串 Token 就是脚本调用 API 的身份凭证。

第二步,把集成连接到目标页面。这一步最容易漏。你光有 Token 不够,还要在 Notion 页面的右上角菜单里,通过 Connections 搜索并选中刚才创建的集成。如果不做这一步,API 会返回 403,提示没有权限访问该页面。其实可以理解为:Token 只是身份,具体能看哪些页面,由页面所有者显式授权。

第三步,拿到数据库 ID。在 Notion 里打开你的目标数据库页面,观察浏览器 URL。URL 的形如https://www.notion.so/你的工作区/数据库ID?v=...,中间那段 32 位字符串就是数据库 ID。脚本里查询和写入都需要它。

请求头里还需要带Notion-Version,我一直在用2022-06-28这个版本,目前没有遇到兼容问题。所有调用都通过官方 API 进行,这是 Notion 提供的能力,合规且稳定。

2.3 环境准备与依赖

我的运行环境是 Python 3.10,包管理用的 pip。核心依赖只有两个:requests用于发 HTTP 请求,retry用于失败重试。如果你想把数据发给推送服务,可以额外装requests就够了,不需要引入大型框架。

敏感信息不要写死在代码里。我会把三样东西放进环境变量或配置文件:微信读书 Cookie、Notion Token、Notion 数据库 ID。跑定时任务时,这些值放进 GitHub Secrets,本地调试时用.env文件加载,但这个文件绝不提交到 Git 仓库。

import os import requests WE_READ_COOKIE = os.getenv("WE_READ_COOKIE") NOTION_TOKEN = os.getenv("NOTION_TOKEN") DATABASE_ID = os.getenv("NOTION_DATABASE_ID") HEADERS_NOTION = { "Authorization": f"Bearer {NOTION_TOKEN}", "Notion-Version": "2022-06-28", "Content-Type": "application/json", }

这段代码虽然简单,但它把所有关键凭证都集中到了环境变量里,后续部署到 GitHub Actions 时只需要在 Secrets 里配置同名变量即可。

3. 核心实现:从抓取数据到写入 Notion

3.1 抓取书架与书摘数据

整个抓取过程分成两个阶段:先拿书架,再逐本拿书摘。我以“书架接口 + 书摘接口”的语义来写示例代码,实际路径请自行观察确认。

书架请求的核心是带上 Cookie 发送 GET 请求,然后解析响应中的书列表。书摘请求则通常需要传入书籍 ID 和分页游标,循环直到所有书摘拉完。

import json import requests def get_shelf(cookie): # 书架接口:返回我书架上的书籍列表 url = "https://weread.qq.com/web/book/shelf/books" resp = requests.get(url, headers={"Cookie": cookie}) data = resp.json() books = [] for obj in data.get("books", []): book_info = obj.get("bookInfo", {}) books.append({ "book_id": book_info.get("bookId"), "title": book_info.get("title"), "author": book_info.get("author"), "cover": book_info.get("cover"), "status": obj.get("readingStatus", 0), }) return books def get_book_highlights(cookie, book_id): # 书摘接口:需要按书籍 ID 循环分页拉取 highlights = [] cursor = "0" while True: url = "https://weread.qq.com/web/book/highlights" resp = requests.get( url, params={"bookId": book_id, "cursor": cursor}, headers={"Cookie": cookie}, ) data = resp.json() for item in data.get("updatedHighlights", []): highlights.append({ "highlight_id": item.get("highlightId"), "chapter": item.get("chapterTitle", ""), "mark_text": item.get("markText", ""), "note": item.get("note", ""), "create_time": item.get("createTime", 0), }) if data.get("hasMore"): cursor = data.get("nextCursor", cursor) else: break return highlights

核心思路是“先拿到书,再逐本拿书摘”,这样方便做增量。书太多的时候,你不需要每次全量抓所有书摘,只需记录同步时间戳,只请求新产生的书摘即可。我通常把最近 7 天的书摘作为增量同步窗口,既保证不会漏,也避免浪费接口请求。

注意:书摘接口返回的createTime通常是毫秒级时间戳。写入 Notion 时,我需要把它转成YYYY-MM-DD格式,这样 Notion 的日期属性才能正确识别。

3.2 构建 Notion 数据库与写入页面

在写入之前,数据库设计要先想清楚。我用的数据库字段如下:

属性名类型用途
书名Title主标题,书籍名称
作者Rich text作者信息
内容Rich text书摘原文
我的想法Rich text如果有笔记,存入这里
章节Select书摘所属章节
划线时间Date书摘创建时间
书籍链接URL微信读书的书籍链接
状态Select想读 / 在读 / 读完
领域Multi-select标签,用于后续聚合检索
同步批次Number用于调试去重

其中“同步批次”这个字段可以直接看出哪些记录是哪次同步写入的,排查重复时特别有用。

写入使用 Notion 的 create page 接口。每个书摘单独创建一个页面,页面属性按上面的字段填充,页面内容用 children blocks 放入原文和相关说明。需要注意,每个 create page 请求的 children blocks 上限是 100 个。书摘就是单条文本,所以问题不大,但如果你想把整本书的所有书摘一次写入一个页面,就会碰到 100 个子块的硬限制。我的解法是:一本书对应一个独立的 Notion 页面,书摘作为子块分批写入,每批 90 个以内,循环追加。

import time import requests def create_page(headers, database_id, payload): url = "https://api.notion.com/v1/pages" resp = requests.post(url, headers=headers, json={"parent": {"database_id": database_id}, **payload}) if resp.status_code == 429: retry_after = int(resp.headers.get("Retry-After", 2)) time.sleep(retry_after) resp = requests.post(url, headers=headers, json={"parent": {"database_id": database_id}, **payload}) if resp.status_code >= 400: print(f"create page failed: {resp.status_code} {resp.text}") return None return resp.json()

我在实际运行时发现,Notion API 对并发和频率有一定限制,触发 429 后按响应头中的Retry-After等待即可。追加子块也可以用类似的方式处理,先构建 blocks 列表,再每次切出 90 个调用 append 接口。

3.3 幂等同步:避免每次同步生成重复内容

幂等是这套系统能不能长期跑的关键。如果每次运行都全量写入,第二次运行就会出现一整批重复页面。解决思路很简单:给每条书摘算出一个稳定 ID,在写入前先查询 Notion 数据库里是否已经存在这个 ID。

最直接的做法是,往数据库里加一个唯一ID属性,类型用 Rich text,值由book_id + highlight_id + create_time拼接生成。每次同步前,用 Notion API 的 query 接口找出所有已有唯一ID,放进一个 Set,然后遍历新书摘时先检查 Set。如果命中就跳过,没命中才写入,写入成功后把这个 ID 加入 Set。

def is_duplicate(existing_ids, unique_id): return unique_id in existing_ids def build_unique_id(book_id, highlight): return f"{book_id}:{highlight.get('highlight_id')}:{highlight.get('create_time')}"

这个方案比维护本地数据库简单得多,因为状态全部存在 Notion 自身,所有去重逻辑都基于“目标系统里有什么”来决策。即使同步脚本崩溃重跑,也不会产生新的重复数据。

经验:第一次跑全量同步时一定要先做小范围测试。我当时是从最近 50 条书摘开始测试的,确认字段映射和写入格式都正确后,再放开跑全书架。否则中途发现书名和作者字段错位,清理起来就是大工程。

3.4 同步时间线优化

同步速度除了受到 Notion API 限制影响,还会被书摘接口的逐本请求拉慢。几百本书逐本去请求书摘,即使每本只需要 1 秒,也要好几分钟。更别提有些书有上千条书摘,需要多轮回圈。

优化思路是“快照 + 增量”结合。书架数据每天同步一次,书籍状态变化不用太频繁;书摘只同步新产生的部分。具体做法是:脚本启动时读取上次同步时间戳,如果距离上次不到 24 小时,就只请求更新时间窗口内的书摘;如果超过 24 小时,就做一次全量增量同步。另外,可以在书架接口返回的书籍列表里过滤掉状态为“想读”或者没有阅读进度的书,这些书通常没有新书摘,可以跳过,节省大量请求。

时间窗口之外,还需要解决“接口数据乱序”的问题。书摘接口返回时不一定按时间排序,如果只取前 N 条,很容易漏掉最新记录。我建议先完整拉取某本书的书摘再本地过滤时间,不要依赖接口的排序。虽然请求次数多一点,但准确性最重要。

4. 部署为自动化工作流

4.1 用 GitHub Actions 安排定时任务

本地跑通之后,我把它部署到了 GitHub Actions,用定时触发实现全自动同步。核心就是一个 workflow YAML 文件,配置schedule里填 cron 表达式。

name: sync-weread-to-notion on: schedule: - cron: "0 8 * * *" jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.10" - run: pip install -r requirements.txt - run: python sync.py env: WE_READ_COOKIE: ${{ secrets.WE_READ_COOKIE }} NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}

这里有两个容易踩的坑。第一个,GitHub Actions 的定时任务默认是 UTC 时区,0 8 * * *代表 UTC 时间早上 8 点,对应北京时间下午 4 点。你要是想在北京时间早上 8 点跑,cron 要写成0 0 * * *,也就是 UTC 零点。第二个,secrets 里的 Cookie 如果换行或包含特殊字符,直接粘贴很容易导致 JSON 解析失败。建议在本地先把 Cookie 值用引号包裹,复制到 Secrets 后再加一句打印检查逻辑,确认长度和格式。

4.2 抓取频率与请求频控

在定时任务里,频率控制对长期稳定运行非常重要。我建议每天最多跑 1 到 2 次,不要设置成每小时甚至更密集。高频请求不仅可能给平台带来压力,还会导致你的账号被临时限制,反而造成同步中断。

如果你确实需要更高的实时性,可以考虑在脚本里加入“本地时钟 + 上次同步时间”的双重限制,同一小时内只允许运行一次。有些时候 GitHub Actions 会因为重试或手动触发而产生重复执行,频率检查能兜底。我的脚本里有这样一段保护:

import os import time from pathlib import Path state_file = Path("last_sync.txt") last_ts = float(state_file.read_text()) if state_file.exists() else 0 if time.time() - last_ts < 3600: print("skip: sync too frequent") return

这个机制很简单,但能有效避免因为误触或任务重跑导致的重复请求。

4.3 敏感信息保护与失效处理

Cookie 是这套方案里最需要关注的生命周期问题。微信读书的登录态一般能维持几天到几周,过期后脚本会返回异常或空数据。如果你在脚本里把这种情况当作“无书摘”处理,那么过期几天后你就会发现 Notion 里的数据静默断更。

我的处理方式是:在同步脚本开头先做一次验证请求,如果响应中包含登录态失效的标志,就立刻发送通知,不要继续跑后续逻辑。通知可以用微信推送、邮件或任意 webhook。最少要做到把错误打印在 Actions 日志里,结合 GitHub Actions 的失败通知功能,出问题至少能收到邮件。

Notion Token 相对稳定,不太会失效,但它能访问的页面取决于集成授权。如果你重建了集成,旧的 Token 会立即失效,记得同步更新 Secrets。我建议所有凭证统一放在环境变量里,不要散落在代码和配置文件中。这样换机器部署时,只要复制环境变量,不用改一行代码。

5. 数据库模板:阅读知识库的高级玩法

5.1 用 Notion 公式做阅读统计

当数据持续流入后,Notion 的数据库公式能力能让这份数据从“记录”升级成“统计”。我的数据库里有一个日期属性叫“开始阅读日期”,一个状态属性叫“状态”。利用公式可以算出到目前为止的阅读天数。

dateBetween(now(), prop("开始阅读日期"), "days")

这个公式每天都会自动变化,不需要手动更新。类似地,我还能算出一本书的划线密度,比如“划线数量”属性除以“章节数量”属性,用公式一列就能显示。这里的核心价值是:Notion 公式不是一次性计算,而是随数据库数据动态更新,你只管同步书摘数据,统计结果自动生成。

另一个我觉得很实用的公式是“长草警告”。如果一本书的最近划线时间距离今天超过 30 天,状态还是“在读”,公式就该提醒你了。

if(and(prop("状态") == "在读", dateBetween(now(), prop("划线时间"), "days") > 30), "警惕长草", "正常")

这种公式让阅读管理变得有“主动反馈”的意味,而不再是一堆静态数据。

5.2 用视图看板管理读书节奏

Notion 数据库的视图切换值得用好。我建了三种视图:

第一种是表格视图,展示全部书摘,按划线时间倒序,快速浏览最近读了什么、画了什么。 第二种是看板视图,按书籍状态分组,每本书是一个卡片,卡片内显示最近一条书摘摘要。 第三种是画廊视图,每张卡片用封面图打底,下面挂书名和领域标签,适合做读书墙。

视图之间切换不需要改变底层数据,所以我可以根据场景自由切换。最让我满意的是把“最近划线”单独做成一个筛选视图,筛选条件是最近 7 天的书摘。这样每次打开 Notion,第一眼看到的就是本周摘录过的内容,很适合周回顾。

与 Workflowy 这类纯大纲工具不同,Notion 的数据库可以按属性组合筛选和排序。比如我只想看“产品方向 + 在读 + 最近一周有划线”的书籍,三个筛选条件叠加,一次就能定位到重点。这个能力是微信读书内部怎么都比不了的。

5.3 性能注意

Notion 数据库在数据量大了以后需要注意性能,但我的实测结果是:只要不把几千条书摘塞进单个数据库页面,整体速度都能接受。每一本书是独立页面,书摘作为子块最多每批 90 个,这样单页面的内容块不会膨胀到影响加载。

属性数量也要控制。每个数据库的属性数量并不是无限膨胀的,公式属性数量过多会导致页面加载变慢。我见过一些人给阅读数据库塞了三十多个属性,打开要转好几秒。我的建议是属性数量控制在 15 个以内,公式属性尤其要克制,只保留真正会在日常视图中用到的几个。

如果你确实需要更多统计维度,更好的方案是把“书籍数据库”和“书摘数据库”分开,用关联字段绑定,而不是把所有信息塞进一个表。我目前的架构就是两者分离,便于各自优化。

6. 常见问题与排查实录

6.1 Notion API 403 与 429

403 错误是接入期最常见的。它基本可以判定为“集成没连到页面”或“Token 不属于当前页面所属工作空间”。排查顺序是:先在 Notion 页面右上角 Connections 里确认集成已添加;再确认 Token 是 Internal Integration Token,不是 OAuth 专用;最后确认数据库 ID 确实属于该页面。如果这三步都做了还是 403,大概率是数据库 URL 里选的页面和实际授权页面不一致。

429 错误则是运行期最常见的。它的特征很明确,响应头里有Retry-After。处理策略就是尊重这个时间,等它结束再继续。我自己写的重试逻辑不单是定时重试,还会读取响应头里的时间,因为这个时间是动态的、比固定等待更准确。

import time from retry import retry @retry(tries=3, delay=2, backoff=2) def call_notion_api(url, headers, payload): resp = requests.post(url, headers=headers, json=payload) if resp.status_code == 429: wait = int(resp.headers.get("Retry-After", 5)) time.sleep(wait) raise Exception("rate limited") resp.raise_for_status() return resp.json()

这个写法会在触发 429 时自动按服务端建议的等待时间暂停,然后再试。三次尝试后仍失败,就整个失败退出,交给 GitHub Actions 的通知机制处理。

6.2 微信读书 Cookie 过期与接口字段变动

Cookie 过期后,书摘接口响应往往不是空数组,而是结构异常或者直接报错。不要把这个当成“没有书摘”。我的经验是:每次请求后都检查响应体是否包含业务数据标志,如果缺失,立即终止同步并打印错误。

接口字段变动是更隐蔽的问题。微信读书的接口返回结构并非完全稳定,某些字段在 A 版本是markText,在 B 版本可能嵌套到markText.xxx里。建议你在写解析代码之前,先用浏览器开发者工具看真实返回结构,再用代码解析。如果发现字段名不对,不要硬猜,直接在控制台检查 JSON 树。

另外,书摘接口的分页逻辑也变过。有些版本用cursor,有些用page加pageSize。我建议用“无限循环 + hasMore 标志”的方式,这样无论分页字段怎么变,都能自然适配。

6.3 书摘重复与乱序

重复问题在第一次全量同步时最容易暴露。因为你可能之前手动导入过一批,再跑自动同步时就会看到重复。唯一的根治办法就是“唯一ID + 先查重再写入”,这个我在前面已经详细说过,这里再强调一次:同步脚本一定要保持幂等。

乱序问题则来自接口返回顺序和真实划线时间不一致。如果你用“先取前 N 条”的方式做增量,很容易漏掉最新书摘。正确做法是取完这本书的所有书摘后,再在本地按时间筛选。不要尝试在接口层做假设性的限制,因为那条边界很微妙,不值得赌。

6.4 时区与运行时间

时区问题是很多定时任务都绕不开的坑。GitHub Actions 的 cron 调度默认是 UTC,本地跑脚本则可能按系统时区计算。为了让同步脚本的行为保持一致,我在脚本里统一使用 UTC 时间戳作为增量窗口基准,只在使用datetime展示时才转换成本地时间。

运行时间的设置也要考虑你的实际需求。我自己是把同步放在北京时间的早晨,这样每天一醒来就能看到头一天所有新划线的汇总。如果你更看重收盘后的阅读记录,那就安排到晚上。定时任务最大的意义是从“记起来才同步”变成“每天都自动更新”,它是这套数据同步系统的最后一环。

在实际维护这个项目半年之后,我的体感是:前期所有复杂度都集中在第一轮全量同步的字段对齐和去重策略上,一旦跑通,之后每天就是静默执行,极少需要人工介入。如果你只是想把数据从微信读书一点点搬出来,手动复制也能用;但你如果想要一个长期自动更新的个人阅读数据库,这条“接口抓取 + API 写入 + 幂等同步 + 定时运行”的路径,是目前我认为性价比最高的一条。

另外再分享两个小技巧。第一个,在 Notion 首页做一个“最近划线”的嵌入式视图,这样你打开 Notion 的第一眼就能看到本周的摘录,而不是非得进数据库。第二个,在同步脚本里加一个失败心跳检测,连续两天同步失败就往推送里发一条警告消息,能避免“数据静默断更”这种最磨人的情况。方法都不复杂,但长期用下来,它们才是让这套系统真正“可靠”的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询