十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

RSS自动同步到Notion:用GitHub Actions搭建免费自动化内容库

RSS自动同步到Notion:用GitHub Actions搭建免费自动化内容库 做信息管理的人多少都有过类似的纠结RSS 订阅源散落在各种阅读器里每天明明有大量高质量内容在更新却很难沉淀成自己的知识库Notion 里建了各种数据库用起来很顺手但往里面填内容全靠手动。这个项目就是想打通这道墙——用 GitHub Actions 跑一个定时任务把 RSS 订阅里的新条目自动抓取、写入 Notion 数据库全程不需要自己的服务器也不需要手动干预。这篇内容适合被订阅源淹没、又想集中管理阅读记录的读者按文中的步骤操作你也能搭起一套完全免费的 RSS 自动化部署方案。1. 项目背景RSS 订阅源太多手动搬运到 Notion 太痛苦1.1 我的信息管理痛点我每天的阅读流程大概是这样的打开 RSS 阅读器刷一遍更新遇到有价值的内容点开细读然后复制链接、打开 Notion、新建一条记录、粘贴链接、填标题、选分类。这个过程看起来不复杂但一天发生十几次之后就非常消耗耐心而且很容易漏。尤其是出差或者忙起来几天没看阅读器积压的待读条目能到上百条手动整理根本不可能。后来我尝试过把 Notion 当作 RSS 阅读器来用就是把订阅源里的条目全部写入数据库然后隔一段时间集中浏览。这个思路本身没问题但卡在数据同步上。最开始我是每周手动导出一次 OPML 再批量导入太粗糙而且导入去重做得一塌糊涂。后来试过第三方自动化平台接入倒是快但免费额度少数据也要过一遍第三方服务器隐私上总觉得不太踏实。我需要的是一个自己能完全控制的同步方案定时跑、增量更新、不重复、不依赖额外平台。GitHub Actions 恰好满足所有条件。1.2 为什么选择 Notion 作为内容沉淀库市面上能做信息管理的工具不少但 Notion 在数据结构自由度高 API 完善这两点上特别适合做这件事。Notion 的数据库相当于一张灵活的表你可以自定义属性列比如标题、链接、来源、发布时间、阅读状态、标签。这比传统 RSS 阅读器里那种只能标记已读/未读的模式灵活得多。我可以按来源筛选按日期排序甚至可以建多个视图一套数据用表格看、用看板看、用日历看。另外 Notion 的 API 做得比较成熟官方提供了 REST API操作数据库的增删改查都支持文档也清晰。对开发者来说这意味着我能用代码直接往数据库里写数据不需要模拟浏览器操作。正是这一点让自动化成为可能。1.3 自动化部署要解决的核心问题从需求的角度看这个项目本质上是一个最小的 CI/CD 自动化部署流程数据源RSS有新的内容产生通过定时任务触发抓取经过加工处理后部署写入到目标平台Notion。我把问题拆成了三个核心点定时触发、增量抓取、可靠写入。定时触发最简单Linux 下的 cron、Windows 的计划任务、GitHub Actions 的 schedule 都能做到。增量抓取稍微麻烦一点因为 RSS 源本身不提供上次同步到哪里的指示你必须自己维护一个状态或者在写入之前去重。而可靠写入牵扯到 API 的认证、限流、错误重试这些细节决定了整个方案能不能长期稳定运行。2. 技术方案选型为什么是 GitHub Actions 而不是服务器定时任务2.1 三种主流方案对比在动手之前我对比了三种思路自己买服务器跑 cron、用第三方自动化平台、用 GitHub Actions。结论很明确就个人项目而言GitHub Actions 综合成本最低。自己买服务器跑 cron 最灵活但前提是你得有一台长期在线的机器。轻量云服务器一年几百块还得处理环境依赖、进程守护、日志轮转这些问题。如果只是跑一个每天几次的 Python 脚本纯粹是杀鸡用牛刀。第三方自动化平台接入快界面友好不用写代码。但免费层级限制明显一个月几百次的操作额度对高频 RSS 同步来说很容易耗尽。更关键的是你抓取的内容要经过它们的服务器从数据隐私角度讲不是一个理想选择。GitHub Actions 恰恰落在中间它是代码仓库自带的 CI/CD 平台支持定时触发免费额度对个人完全够用。写个 workflow 文件配置好 cron 表达式推送代码剩下的交给微软的服务器去跑。我不用维护任何基础设施也不用为计算资源付费。2.2 GitHub Actions 的工作机制GitHub Actions 的底层逻辑是事件驱动。你可以给仓库配置一个或多个工作流workflow每个工作流由若干个任务job组成任务里面是具体的步骤step。工作流的触发方式有几种代码推送push、拉取请求pull_request、发布版本release还有我们这次要用的定时触发schedule。schedule 用的是标准 cron 表达式比如*/30 * * * *表示每 30 分钟跑一次。每次触发后GitHub 会临时开一台虚拟机默认是 ubuntu-latest在里面按顺序执行你定义的步骤。这台机器是全新的装好了常用软件但没有你项目的历史状态。这既是优点也是坑环境干净、可复现但如果你想记录上次跑到哪了不能依赖机器上的文件因为任务结束后虚拟机就销毁了。2.3 整体架构设计与数据流整个流程可以概括为一条单向数据管道RSS 源 → 定时任务触发 → Python 脚本抓取 → 去重过滤 → 写入 Notion → 可选推送钉钉群。我画了一条很朴素的流程线来理清依赖关系RSS 源是上游Notion 是下游GitHub Actions 是中间的传输带。每个环节挂了都能单独排查源挂了抓取会返回空列表脚本挂了 Actions 会标红报错API 被限流了脚本会收到 HTTP 429 然后重试。坏就坏在中间环节静默失败所以脚本里我特意加了日志输出每一步都在标准输出里打出来。这样的设计保证了两件事第一是可观测打开 Actions 的运行记录就能看到每次执行的成功与否第二是易维护哪一环出问题就改哪一环不用推倒重来。3. 核心细节解析Notion API 和 RSS 解析的关键点3.1 Notion API 的认证与请求格式Notion API 的认证方式很简单就是 Bearer Token。你在 Notion 官网创建一个 Integration集成系统会生成一串以secret_开头的 token。调接口时在请求头里带上Authorization: Bearer token即可。比较容易被忽略的是Notion-Version请求头。这是 Notion API 的版本号机制官方要求每次请求都必须指定比如2022-06-28。很多人第一次调 API 报 400 错误多半就是版本号没写或者写错。还有一点创建的 Integration 必须和数据库建立连接。操作路径是打开 Notion 数据库页面右上角菜单里找到 Connections把刚创建的 Integration 添加进去。这一步漏掉的话API 请求会返回 404提示没有权限访问这个数据库。3.2 数据库设计和属性规划Notion 数据库的属性列设计直接决定了脚本写入的代码复杂度所以动手写代码前先把库建好。我建了一个名为 RSS Archive 的数据库属性列包括标题title 类型、链接url 类型、来源rich_text 类型、发布时间date 类型、已读checkbox 类型。其中标题列是 Notion 数据库的必填属性不能删。链接列用于去重也是脚本查询的主要依据。来源列存站点名称方便在数据库里按来源筛选。发布时间列用条目的 published 字段注意如果你的 RSS 源返回的时间格式不规范feedparser 会解析成 struct_time需要先格式化成 ISO 8601 再传给 API。数据库的图标可以顺手设置一下Notion 支持给每条记录单独加 emoji 图标。API 写入页面时在请求体里加一个icon参数就能指定比如{type: emoji, emoji: }。这个小细节能让数据视图看起来舒服很多也方便按图标快速辨别来源。3.3 去重逻辑如何避免重复写入去重是整个脚本里最关键的一环。如果不做每跑一次任务都会把旧条目重新写一遍数据库很快就会被垃圾数据塞满。我的做法是先查询数据库里已有的 url 属性集合再去新抓取到的条目里做过滤。具体实现上Notion API 的查询接口支持分页每次最多返回 100 条。我通过 filter 参数限定只查最近 30 天的数据减少每次请求的负担。然后遍历所有分页结果把 url 属性提取到一个集合里存进内存。接下来遍历 RSS 解析出来的 entry 列表如果 entry 的链接已经在集合里就跳过否则调用创建页面接口写入。这里有个细节要注意RSS 条目的 link 字段可能会有 URL 参数变化比如带 utm_source 追踪参数的地址同一篇文章会因为参数不同被判为两条。稳妥的做法是在比较之前先把 URL 里的查询参数去掉或者用 entry 的 id 字段来判断。如果抓取量大更高效的做法是把已经处理过的条目 ID 缓存到文件或数据库里。但在 GitHub Actions 环境下每次运行都是全新的虚拟机本地缓存文件不持久所以更可靠的方式始终是查询 Notion 数据库本身。数据量几千条的时候每次查询都在毫秒级完全够用。4. 实操过程从零搭建 RSS 自动化部署流水线4.1 第一步准备 Notion 数据库和 API Token先在 Notion 网页版里新建一个数据库页面表格视图即可。建好数据库后从浏览器地址栏里复制数据库 ID形如https://www.notion.so/workspace/8e1c...中间那串 32 位字符串就是 database_id。然后去 Notion 的 My Integrations 页面创建一个新的 Integration名称随意比如 rss-bot。创建完成后复制 token。注意 token 只会完整显示一次一定要保存好丢了只能重新创建。最后回到数据库页面右上角菜单里选择 Connections把 rss-bot 关联进去。到这里Notion 这边就准备完毕了。如果你注册 Notion 时选了学生身份也不用担心目前实测下来 API 功能没有差异。唯一要留意的是在设置 Integration 权限时有些 workspace 类型可能会隐藏部分选项如果遇到找不到入口的情况先到账号设置里确认一下 workspace 类型即可。4.2 第二步创建 GitHub 仓库并配置 Secrets在 GitHub 上新建一个私有仓库名字随便比如rss-to-notion。仓库是否私有不影响 Actions 功能但建议私有避免把抓取逻辑暴露给其他人。仓库创建好后进入 Settings → Secrets and variables → Actions添加两个 repository secretNOTION_TOKEN和NOTION_DATABASE_ID。secret 的名字是大小写敏感的后面在 workflow 文件里引用的名字必须和这里完全一致。接着把仓库克隆到本地初始化一个简单的项目结构。我用的文件不多requirements.txt放依赖fetch_rss.py放脚本.github/workflows/rss.yml放工作流定义。4.3 第三步编写 RSS 抓取脚本脚本用 Python 写依赖两个库requests负责调 HTTP 接口feedparser负责解析 RSS。这两个都是非常成熟的库安装没有坑。完整脚本的核心逻辑是读取 RSS 源列表逐个解析过滤掉已经存在数据库里的条目然后调用 Notion API 创建页面。下面是个简化版示例import os import feedparser import requests NOTION_TOKEN os.environ[NOTION_TOKEN] DATABASE_ID os.environ[NOTION_DATABASE_ID] NOTION_VERSION 2022-06-28 PAGE_API https://api.notion.com/v1/pages HEADERS { Authorization: fBearer {NOTION_TOKEN}, Notion-Version: NOTION_VERSION, Content-Type: application/json, } def get_existing_urls(): urls set() start_cursor None query { page_size: 100, filter: { property: 发布时间, date: {after: 2024-01-01} } } while True: if start_cursor: query[start_cursor] start_cursor resp requests.post( fhttps://api.notion.com/v1/databases/{DATABASE_ID}/query, headersHEADERS, jsonquery, ) data resp.json() for result in data.get(results, []): url_prop result[properties].get(链接, {}) urls.add(url_prop.get(url, )) if not data.get(has_more): break start_cursor data.get(next_cursor) return urls def add_page(title, url, source, pub_date): payload { parent: {database_id: DATABASE_ID}, icon: {type: emoji, emoji: }, properties: { 标题: {title: [{text: {content: title}}]}, 链接: {url: url}, 来源: {rich_text: [{text: {content: source}}]}, 发布时间: {date: {start: pub_date}}, 已读: {checkbox: False}, } } resp requests.post(PAGE_API, headersHEADERS, jsonpayload) resp.raise_for_status() def main(): rss_sources [ (示例博客, https://example.com/feed.xml), # 在这里添加你的订阅源 ] existing_urls get_existing_urls() for source_name, feed_url in rss_sources: feed feedparser.parse(feed_url) for entry in feed.entries: if entry.link in existing_urls: continue try: pub entry.get(published, ) add_page(entry.title, entry.link, source_name, pub) print(f已写入: {entry.title}) except Exception as e: print(f写入失败: {entry.link} - {e}) if __name__ __main__: main()这段代码足够跑通整个流程。有几个点我特别说明一下日期字段必须满足 Notion API 的格式要求必须是 ISO 8601 格式比如2024-12-01T08:30:00Z。feedparser 返回的published字段是类似Tue, 01 Dec 2024 08:30:00 GMT的格式需要注意转换。更稳妥的做法是直接用time.struct_time转换或者干脆只存日期部分。如果 RSS 源里的published字段缺失可以用updated兜底再不行就跳过该条避免 API 报错。这个细节我一开始没处理遇到过几次 400 错误才发现。4.4 第四步编写 GitHub Actions 工作流工作流文件放在.github/workflows/rss.yml内容如下name: rss-to-notion on: schedule: - cron: */30 * * * * workflow_dispatch: jobs: sync: runs-on: ubuntu-latest steps: - name: 检出代码 uses: actions/checkoutv4 - name: 配置 Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: 安装依赖 run: pip install -r requirements.txt - name: 运行 RSS 同步脚本 run: python fetch_rss.py env: NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}这个配置里有几个关键点。schedule的 cron 表达式用的是 UTC 时间不是本地时间。如果你希望每天北京时间早上 8 点跑cron 要写成0 0 * * *因为 UTC 比北京时间慢 8 小时。而且 GitHub Actions 的定时任务本身有延迟官方文档说过在高负载时段调度可能晚几分钟甚至更久所以对时效性要求高的任务建议把频率设得密一点比如每 30 分钟一次而不是每天一次。workflow_dispatch是为了方便手动触发。你可以在 GitHub 网页端 Actions 页面点击按钮手动跑一次工作流这在调试阶段特别有用不用干等定时调度。env 部分把 secrets 里的值传给脚本的环境变量。Python 脚本里通过os.environ[NOTION_TOKEN]读取。注意不要直接把 token 写在 yml 文件里否则等于公开了密钥。4.5 第五步部署验证与效果检查把代码推送到 GitHub 仓库后进入 Actions 页面会看到一个名为 rss-to-notion 的工作流。第一次建议点击右上角的 Run workflow 手动触发。运行结束后点进任务详情查看日志。正常情况会看到类似已写入: 某篇文章标题的输出说明脚本成功调用了 Notion API。如果没有写入任何条目先确认 RSS 源是否有新内容以及数据库里是否已经存在相同链接的数据。验证通过后打开 Notion 数据库页面按时间排序新条目应该已经出现在列表里。到这一步整个自动化部署流程就跑通了。我实测下来的情况是第一次手动触发写了 200 多条历史数据耗时约 40 秒之后每 30 分钟自动跑一次通常只有 0-3 条新增耗时稳定在 15 秒以内。GitHub Actions 的免费额度对个人项目来说绰绰有余。5. 常见问题与排查技巧实录5.1 认证与 API 相关报错整个流程里最容易出问题的地方就是 Notion API 的认证。我把常见的报错整理成了一张速查表排查起来会快很多报错现象可能原因解决办法401 unauthorizedtoken 写错或没传重新复制 token检查 secrets 配置和 env 传递400 validation_error缺少 Notion-Version 头或日期格式不对确认请求头带Notion-Version: 2022-06-28404 object_not_founddatabase_id 不对或 Integration 没关联数据库重新复制 ID到页面里确认 Connections 已添加409 conflict同一时间段请求过于频繁在脚本里加重试机制遇到 409 等待后重试值得注意的是404 的报错信息很容易误导人。它不一定是说数据库 ID 错了更多时候是权限问题——Integration 没有连接到这个数据库。所以遇到 404 时第一优先级是去数据库页面的 Connections 里检查。另外 GitHub Actions 的执行日志里不要打印环境变量的值尤其是 token。我一般在调试时打印len(token)而不是 token 本身既能确认变量传进来了又不会泄露敏感信息。5.2 定时任务不执行的坑定时任务在 GitHub Actions 上有一些隐性限制。首先是 cron 表达式按 UTC 解释这个前面提过。如果你按本地时间写了 cron观测到的执行时间会偏差好几个小时看起来就像定时任务不执行。其次是 GitHub Actions 的 schedule 事件有最小间隔限制。实测下来cron 最短能设到每 5 分钟一次但如果你设成每分钟一次部分调度会被系统自动跳过。这不是 bug而是平台为了控制负载的策略。个人 RSS 场景完全不需要这么高的频率每 30 分钟到每 6 小时都是合理区间。最后如果一个仓库超过 60 天没有任何活动GitHub 可能会暂停该仓库的定时任务。这个官方文档里没有明确写得那么直白但社区里有不少反馈。如果你发现部署好之后跑了几周就不再触发了检查一下仓库是否还有推送活动或者干脆加一个每周自动提交的小任务保持仓库活跃。5.3 RSS 源失效与第三方服务过期问题RSS 源本身也是需要维护的。有的博客关站了feed 地址直接 404有的源格式不规范feedparser 解析出来是空列表还有的源虽然能访问但内容更新极不规律可能一个月就更新几条。我的解决思路是在脚本里对每个源做单独的异常捕获单个源挂了不影响其他源。同时维护一个健康检查日志哪个源连续多次抓取结果为空就人工检查一下。这里还要提一点我最初尝试过把一些不支持 RSS 的网页通过第三方工具转成 RSS 源。这类第三方服务的免费档通常有缓存过期机制有的 24 小时就失效导致订阅源隔天就拉不到新内容。后来我改用 GitHub Actions 直连原始 feed凡是能直接访问的源都不再走第三方转换稳定性提升了一大截。如果你的订阅源里恰好有这种半死不活的转换源建议优先寻找原始 feed 地址或者干脆换一个支持 RSS 的替代信息源。5.4 时区与调度时间偏差时区问题其实在 5.2 已经说过了但值得单独拿出来强调因为它是所有初学者的噩梦。GitHub Actions 的 runner 默认时区是 UTC。cron 表达式按 UTC 计算。Python 脚本里如果用了datetime.now()拿到的也是 UTC 时间除非你在脚本里显式指定时区。我的做法是脚本里所有时间都按 UTC 处理只有在写入 Notion 的日期字段时才保持不变因为 ISO 8601 格式自带时区信息Notion 显示时会自动转换成本地时间。比如你写入2024-12-01T00:30:00ZNotion 网页版在国内时区会显示为当天的 08:30。这样反而省去了手动转换的麻烦。如果你需要严格按照本地时间判断哪一天的更新建议在脚本里用zoneinfo模块指定时区而不是依赖系统默认值。这样可以避免服务器时区和本地时区不一致导致的逻辑错误。6. 扩展玩法把新订阅推送到钉钉群6.1 钉钉 Webhook 接入方法Notion 适合做沉淀和检索但如果你希望第一时间知道有新内容更新推送通知必不可少。对接钉钉群机器人是最省事的方案原理就是往一个 webhook 地址 POST 一段 JSON。在钉钉群里添加一个自定义机器人拿到 webhook 地址。然后在脚本里加一个推送函数def notify_dingtalk(title, url, source): webhook os.environ.get(DINGTALK_WEBHOOK, ) if not webhook: return text f【{source}】新增订阅{title}\n{url} data {msgtype: text, text: {content: text}} requests.post(webhook, jsondata)GitHub Actions 的 secrets 里再加一个DINGTALK_WEBHOOK在 workflow 文件的 env 部分传入即可。脚本里放进notify_dingtalk调用后每次写入新条目就会实时推送到群聊。6.2 推送格式与频率控制推送本身很简单但推送频率控制是个细节问题。如果 RSS 源一下子更新了十几条逐条推送会刷屏。我的经验是做聚合脚本先跑完所有源的抓取和写入统计有新增的条目然后合成一条汇总消息推送。比如本次更新 5 条内容来自 3 个订阅源附上前几条的标题和链接。这样既不会漏信息也不会打扰群里的其他人。钉钉机器人还有一个安全设置可以加关键词校验。如果你在机器人配置里设置了一个关键词比如新增订阅推送消息必须包含这个关键词才能送达否则会被钉钉拦截。这个限制要在调试时特别留意避免出现脚本明明没报错但群里没有消息的情况。7. 踩坑心得与后续优化方向7.1 实操中印象最深的三个坑第一个坑是去重逻辑。我最初是只按标题去重结果不同站点转载同一篇文章时标题相同但链接不同导致重复写入。后来改成按链接去重问题就消失了。如果你也在做类似项目建议始终以 URL 作为唯一键而不是标题。第二个坑是 Notion API 的日期字段格式。feedparser 返回的时间格式五花八门有 RFC 822、RFC 3339还有纯日期。我一开始直接把这些字符串塞给 Notion API结果频繁报 400。最后统一用email.utils.parsedate_to_datetime做转换输出 ISO 8601 字符串才彻底解决。第三个坑是 Actions 定时任务的不稳定性。有时候你打开日志发现任务晚跑了 20 分钟别慌这是平台调度的正常现象。如果你对时间敏感可以在 workflow 里加一个超时设置或者把任务拆成更小的粒度。我在实际使用中体会到RSS 同步这个场景半小时的延迟完全无感不必过于纠结。7.2 后续可以扩展的方向这套架构跑通之后可以做很多衍生功能。比如把 Notion 数据库当作一个稍后读列表配合浏览器插件或手机快捷指令手动添加新条目进来两个数据源同时汇入同一个数据库。再比如给数据库加一个数据透视视图用公式列统计本周读了哪些来源的文章形成一份简单的阅读周报。还可以接入其他即时通讯工具比如飞书或 Telegram Bot推送方式大同小异改一下请求格式就行。如果你想把类似的自动化流程推广到其他场景这套GitHub Actions API的模式也能复用。比如定时抓取天气数据写入表格、监控网页价格变化发通知、定时生成日报摘要等等。本质上都是同一套骨架定时触发 → 脚本处理 → API 写入 → 通知反馈。我在实际使用中还发现GitHub Actions 版本的升级偶尔会影响 workflow 的语法比如actions/checkout从 v3 升到 v4 时老配置依然兼容但新项目建议直接用最新主版本。遇到工作流报错先看是否依赖了过时的 action 版本这一条能省掉不少排查时间。
返回列表