从 URL 到知识资产:我的全自动内容抓取、转换与归档系统深度解析

知乎是中文平台最高质量内容的讨论平台,内容质量相当高,值得花大力气进行适配抓取;微信是国内最大的私域知识分发平台,最常用的沟通工具,是减少知识收集摩擦的最好通道;

从 URL 到知识资产:我的全自动内容抓取、转换与归档系统深度解析

本文详细拆解一套个人知识管理自动化体系的全链路设计——从手机一键提交链接,到浏览器插件安全抓取、AI 增强处理,最终沉淀至私有 NAS 形成结构化笔记。文章将逐一剖析 URL 提取、智能分流、高可靠性抓取、Markdown 转换、AI 增强、去重存储与跨组件同步等核心环节的工程考量与实现细节。


1. 背景与动机

在信息过载的时代,我们每天都在接触大量值得保存的文章——知乎回答、微信公众号推文、博客长文。传统的收藏方式存在诸多痛点:

  • 平台绑定:知乎收藏夹可能因账号异常或内容删除而丢失;微信收藏没有导出的出口。

  • 手动存档成本高:复制粘贴、整理格式、打标签、存到笔记软件,每一步都是摩擦。

  • 跨设备壁垒:手机上看到的文章,要转到电脑上才能整理,流程断裂。

  • 封闭平台反爬:知乎对非登录用户的限制极其严格,常规爬虫难以稳定抓取全文。

经过长期摸索和逐步迭代,我构建了一套以 Chrome 扩展 + Cloudflare Worker + Knowly 守护进程 为核心的个人知识管道。它能够:

  1. 手机一键提交:通过快捷指令将任意链接发送至中继 Worker。

  2. 智能分流:Worker 自动识别知乎链接与非知乎链接,放入不同队列。

  3. 安全抓取:Chrome 扩展利用浏览器的真实登录态抓取知乎正文;Knowly 守护进程负责微信公众号等通用网页。

  4. 内容转换:HTML 自动转换为 Markdown,保留图片、标题、加粗、链接等格式。

  5. AI 增强:自动生成标签、摘要、质量评分,并整理内容结构。

  6. 自动归档:通过 SSH 同步至私有 NAS,按日期存储,三层去重保证无重复。

  7. 离线可靠:网络中断时暂存本地,恢复后自动重试,游标机制防止重复消费。

本文将深入剖析每个环节的技术方案、工程考量和优化历程,分享一条从 URL 到知识资产的完整自动化路径。


2. 系统全景:三大组件与数据流向

2.1 架构概览

系统由三个独立组件协作完成:

  • Cloudflare Worker (knasync):作为消息中继,提供队列服务。

  • Chrome 扩展 (zhihu-assistant):运行在用户浏览器侧,负责知乎文章的抓取。

  • Knowly 守护进程:运行在 Mac 本地,负责剪贴板监听、通用网页抓取、AI 处理及 NAS 归档。

整体数据流可概括为:

  
手机(快捷指令) → Worker(/submit) → 自动分流 → 知乎队列 / 通用队列
  
                                            ↓                   ↓
  
                                     Chrome 扩展(pull)   Knowly Relay(pull)
  
                                            ↓                   ↓
  
                                     抓取知乎正文          抓取通用网页
  
                                            ↓                   ↓
  
                                     格式化为 Markdown      格式化为 Markdown
  
                                            ↓                   ↓
  
                                        push 结果至 Worker 的 results 队列
  
                                            ↓
  
                         Knowly ResultPuller(pull results,游标增量)
  
                                            ↓
  
                                    去重检查 → AI 标签/摘要 → SSH 归档至 NAS
  

2.2 组件职责边界

| 组件 | 核心职责 | 关键能力 |

|------|---------|---------|

| Worker | 消息队列、内容分流 | 双队列设计、自动识别知乎 URL、结果广播、游标分页 |

| Chrome 扩展 | 知乎内容抓取 | 复用浏览器 Cookie、支持 API 和页面解析两种方式、HTML→MD 转换 |

| Knowly | 通用抓取、AI 增强、归档 | 多源抓取(HTTP/无头)、三层去重、SSH 同步、Outbox 离线队列 |

这种分工使得每个组件的逻辑高度内聚,且各自的优劣势得到互补:Chrome 扩展有登录态但无法处理非知乎链接;Knowly 可处理任何 URL 但受限于无状态环境。Worker 的中继作用完美解决了两者的竞争问题。


3. URL 提取:从手机到队列

3.1 手机端提交

手机端使用 iOS 快捷指令,支持从分享菜单直接发送链接。快捷指令构造一个 POST 请求:

  
POST /submit
  
Host: ***.workers.dev
  
X-Auth-Key: <secret>
  
Content-Type: application/json
  

  
{"content": "https://www.zhihu.com/question/12345678/answer/987654321"}
  

Worker 的 /submit 接口会先尝试解析 JSON,提取 contenturl 字段;若解析失败,则直接当作纯文本处理。这种兼容性设计让终端不必关心格式细节。

3.2 Worker 端自动分流

Worker 收到内容后,调用 isZhihuUrl(content) 进行判断:

  
function isZhihuUrl(content) {
  
    const trimmed = content.trim();
  
    if (!/^https?:\/\//i.test(trimmed)) return false;
  
    if (!/^https?:\/\/([\w-]+\.)?zhihu\.com\//i.test(trimmed)) return false;
  
    const validPatterns = [
  
        /zhihu\.com\/question\/\d+\/answer\/[\w-]+/,
  
        /zhihu\.com\/p\/\d+/,
  
        /zhuanlan\.zhihu\.com\/p\/\d+/,
  
    ];
  
    return validPatterns.some(pattern => pattern.test(trimmed));
  
}
  
  • 若是知乎链接 → 写入 KV 键 queue_zhihu(消费者:Chrome 扩展)

  • 否则 → 写入 queue_general(消费者:Knowly)

队列存储格式为 JSON 数组,每个元素包含 { t: timestamp, c: content }。队列大小受 MAX_QUEUE_SIZE = 50 限制,并设置 expirationTtl: 600(10 分钟),防止异常堆积。

3.3 消费者拉取

两个消费者分别以不同参数请求 /pull

  • Chrome 扩展:GET /pull?queue=queue_zhihu

  • Knowly:GET /pull?queue=queue_general

Worker 采用 竞争消费模型:每次拉取后立即删除队列(await env.CLIPBOARD_STORE.delete(targetKey)),确保每条消息只被一个消费者处理。这种方式牺牲了“可靠投递”的保证(若消费者在处理前崩溃,消息会丢失),但在个人场景下可接受,且避免了复杂的 ACK 机制。


4. 知乎文章高可靠性抓取

4.1 为什么扩展抓取知乎最可靠?

知乎对未登录用户的限制极其严格:未登录时只能查看少量内容,频繁请求会触发验证码。传统爬虫方案如 Selenium、Puppeteer 需要模拟登录,面临账号风控、Cookie 过期等问题。

而 Chrome 扩展运行在用户真实的浏览器环境中,可以通过 chrome.cookies.getAll({ domain: '.zhihu.com' }) 直接获取当前登录态的所有 Cookie。这些 Cookie 是知乎服务器主动下发的,扩展只是“借用”了这一有效凭证,因此请求不会触发任何风控,成功率接近 100%。

4.2 抓取流程:API 优先 + 页面回退

扩展的 fetchZhihuContent 函数实现了双通道抓取:

步骤一:API 优先

  • 回答类:调用知乎内部 API /api/v4/questions/{questionId}/answers/{answerId}

  • 文章类:调用 /api/v4/articles/{articleId}

这些 API 返回 JSON 数据,包含完整的标题、正文 HTML、作者信息。使用 API 的优势是数据干净、无需解析 DOM。

步骤二:页面回退

如果 API 失败(例如未登录时某些 API 会拒绝访问),扩展会抓取完整 HTML 页面,并尝试从中提取内嵌的 initialData__INITIAL_STATE__。知乎将页面初始状态以 JSON 形式嵌入在 <script> 标签中:

  
let match = html.match(/<script id="js-initialData"[^>]*>(.+?)<\/script>/);
  
if (match) {
  
    const jsonStr = match[1]
  
        .replace(/&lt;/g, '<')
  
        .replace(/&gt;/g, '>')
  
        .replace(/&amp;/g, '&')
  
        .replace(/&quot;/g, '"')
  
        .replace(/&#39;/g, "'");
  
    const data = JSON.parse(jsonStr);
  
    // 从 data.initialState.entities.answers 等路径中提取内容
  
}
  

这种双重抓取策略让扩展即便在 Cookie 部分失效或 API 限流时仍能工作。

4.3 HTML → Markdown 转换

知乎正文是 HTML 格式,扩展中实现了 htmlToMarkdown 函数进行转换,保留:

  • 图片:优先取 data-actualsrc / data-src / src,转换为 ![alt](url)

  • 标题<h1> ~ <h6># 标记

  • 加粗/斜体<strong>/<b>**text**<em>/<i>*text*

  • 链接<a href="">[text](href)

  • 代码<pre><code> → 代码块,行内 <code>`code`

  • 引用<blockquote>> text

  • 列表<li>- item

转换时还对 HTML 实体(&nbsp;, &lt;, &mdash; 等)进行了解码,确保输出为纯文本 Markdown。

4.4 结果推送

处理完成后,扩展将 Markdown 正文通过 pushTaskResult POST 到 Worker 的 /push 接口,存入 results 广播队列。请求体示例:

  
{
  
    "content": "# 文章标题\n\n> 作者:xxx\n> 来源:https://...\n\n正文 Markdown..."
  
}
  

results 队列同样受 MAX_QUEUE_SIZE 限制,并设置 expirationTtl: 3600(1 小时自动过期)。

4.5 推送频率控制

为避免触发 IMA 等下游 API 的频率限制,扩展在连续推送之间设置了可配置的间隔(默认 8 秒)。如果遇到 403 错误,还会实行指数退避重试(最多 3 次)。连续失败 2 篇后,自动暂停 2 小时,防止账号被风控。


5. 通用网页抓取与 Knowly 的增强

5.1 Knowly 的 Relay Puller

Knowly 守护进程内部的 relay.Puller 定时从 queue_general 拉取内容。拉取器在初始化时指定了 ?queue=queue_general,因此它只会获取非知乎链接(包括微信公众号文章、普通博客、纯文本等)。

5.2 通用网页抓取:fetcher.FetchPage

对于 URL,Knowly 调用 fetcher.FetchPage 抓取页面标题和正文。该函数通过标准 HTTP 客户端发送请求,使用精心设置的一组请求头(User-Agent, Accept, Accept-Language 等)来模拟真实浏览器访问。针对微信文章,还实现了专门的解密逻辑(识别 JsDecode 中的 \xNN 转义内容)。

同时,Knowly 支持知乎链接的后门入口(尽管现已通过队列分流屏蔽):如果 Relaya 拉取到了知乎链接,它会尝试通过 智谱 web_reader MCP 接口 抓取,但该接口有额度限制且不稳定。这也是我们要将知乎完全移交给扩展的原因。

5.3 剪贴板直通

除了从 Relaya 队列拉取,Knowly 的核心功能之一是剪贴板监听。当用户在电脑上复制任何文本或图片时,Knowly 会立即捕获并启动同步。对于剪贴板中的 URL,它会异步抓取网页标题,增强内容。

5.4 URL 增强与内容格式化

无论是 Relaya 拉取还是剪贴板捕获,如果内容是 URL,Knowly 都会尝试抓取页面标题:

  
if fetcher.IsURL(content) {
  
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  
    info, err := fetcher.FetchPage(ctx, urlStr)
  
    if err == nil && info != nil {
  
        enhanced = content + "\n\n# " + info.Title + "\n\n" + info.Content
  
    }
  
}
  

这使最终保存到 NAS 的文件不再是孤零零的链接,而是附带了标题和正文摘要的 Markdown 文档。对于本次优化后的架构,通用链接的正文抓取就由 Knowly 独立完成

5.5 AI 增强

如果启用了 AI 模块,Knowly 会在同步前对文本进行结构化处理:

  • 标签:生成 3~5 个关键词

  • 摘要:50 字以内的中文摘要

  • 评分:内容质量打分(0~10 分),系统日志等低质量内容自动识别

  • 整理:将原文重新组织为清晰的 Markdown 格式

AI 调用通过 OpenAI 兼容 API 进行,支持自定义提示词模板(通用、代码、学术、极简模式)。处理成功后,结果会嵌入到归档文件的 YAML Front Matter 中:

  
---
  
sync_time: 2026-04-27 08:36:05
  
source: clipboard
  
content_hash: a1b2c3d4e5f6...
  
tags: [AGI, 语言学, Transformer]
  
summary: "从符号学角度论证中文更利于大模型权重收敛"
  
score: 9
  
---
  

一个关键优化是:去重检查前置于 AI 处理。Knowly 在调用 AI 之前先通过 ExistsByHash 检查 NAS 上是否已有相同内容,若命中则直接跳过,避免浪费 AI Token。这在 Result puller 拉取大量历史结果时尤其重要。


6. 转换细节:从 HTML 碎片到高质量 Markdown

6.1 知乎正文的 HTML 清洗

知乎正文 HTML 包含大量样式类、<figure> 容器、懒加载图片等。扩展的 htmlToMarkdown 转换过程需处理以下场景:

  1. 图片懒加载:知乎使用 data-actualsrc 存放真实图片 URL,src 往往是占位图。转换时优先匹配 data-actualsrc,其次是 data-src,最后才是 src

  2. 嵌套结构:如 <a><img></a>,先提取图片再处理链接。

  3. 代码块:知乎的代码块通常是 <pre><code>...</code></pre>,但行内代码是 <code> 直接包裹。需要区分处理。

  4. 引用块:知乎的 <blockquote> 内可能包含多段落,转换时每行前加上 > 标记。

6.2 Knowly 的正文提取

对于通用网页,Knowly 的 extractContent 实现了逐级回退:

  1. 微信正文提取(专用正则)

  2. 语义化标签:<article><main><div class="content">

  3. 回退到 <body>

  4. 清除脚本、样式、注释,压缩空白

6.3 Markdown 格式标准化

无论来自扩展还是 Knowly,最终入库的 Markdown 都遵循统一格式:

  
# 标题
  

  
> 作者:xxx
  
> 来源:URL
  

  
正文内容...
  

  
---
  

  
*自动同步于 2026-04-27 08:36:05*
  

Knowly 在写入 NAS 时还会添加 YAML Front Matter 元数据(同步时间、来源、哈希值、AI 标签/摘要/评分),便于未来使用静态站点生成器(如 Hugo)直接构建知识库。


7. 保存与同步:三层去重与离线可靠性

7.1 目标存储

所有内容最终通过 SSH 同步至 NAS(或任何 Linux 服务器),按日期组织目录结构:

  
~/knowly_archive/
  
├── 2026/
  
│   ├── 04/
  
│   │   ├── 27/
  
│   │   │   ├── 083558_0427_083444_INFO_RelayResul.md
  
│   │   │   ├── 083142_自己发明的梗被广为流传是种怎样的体验.md
  
│   │   │   └── ...
  

文件名由时间戳(HHMMSS)+ 内容前缀构成,方便人类浏览。内容前缀提取时优先使用 Markdown 的一级标题,若无则取原文前 20 个字符并清理特殊符号。

7.2 三层去重策略

为避免重复同步,Knowly 实现了纵深防御的三层去重:

  1. 内存级:Monitor 维护 lastHash,相同内容的连续复制直接过滤(读写锁保护)。

  2. 持久化级:进程重启后从 status.json 恢复 lastHash,防止重启后立即重复同步刚同步的内容。

  3. 远程索引级:每次写入前,检查 NAS 当天目录下的 .knowly_hashes 索引文件。该文件使用 grep -qxF 进行 O(1) 精确匹配,比遍历目录快几个数量级。此检查现在被前置到 AI 处理之前,从而避免了无意义的 API 消耗。

此外,Result puller 拉取结果时也通过游标避免重复消费(见第 8 节)。

7.3 离线队列与重试

如果 SSH 连接中断导致同步失败,Knowly 会将内容存入本地 OutboxJSONL 格式),并立即执行 client.ForceReset() 断开僵死连接。

守护进程启动后及每 5 分钟,会自动执行 drainOutbox(),尝试重新同步积压条目。成功则删除,失败则保留并记录错误。Outbox 中不仅存储原始内容,还保留 AI 元数据,确保重试成功后归档文件仍包含完整的 AI 信息。

7.4 历史记录

每次成功同步(或去重跳过),Knowly 会将条目记录在本地 history.jsonl 中,包括 ID、预览、类型、NAS 路径和 AI 标签。该文件采用惰性计数与阈值压缩策略,保持文件大小可控(超过 2 倍上限时压缩至最新 1000 条),并通过逆序块读取优化查询性能。


8. 同步策略:避免重复消费与信息丢失

8.1 队列消费模式

Worker 的输入队列(queue_zhihu / queue_general)采用“读完即删”的竞争消费。这种模式天然保证了一个 URL 只被一个消费者处理,避免了两个 Knowly 实例或扩展与 Knowly 同时处理同一链接。

输出队列(results)则设计为广播模式:允许多个消费者(如手机查看结果、Knowly 归档)都可以通过游标增量拉取,数据不会因为第一个消费者拉走就消失。两种模式的结合恰到好处地平衡了资源竞争与结果共享的需求。

8.2 游标持久化防止重启回溯

Knowly 的 ResultPuller 使用 result_cursor.txt 文件持久化最后一次拉取的游标(results 队列中条目的时间戳)。启动时从文件恢复游标,后续拉取只请求 since 参数大于此游标的新条目。

  
rp.cursorFile = filepath.Join(config.GetConfigDir(), "result_cursor.txt")
  
// 启动时加载游标
  
if data, err := os.ReadFile(rp.cursorFile); err == nil {
  
    fmt.Sscanf(string(data), "%d", &rp.cursor)
  
}
  
// 拉取后更新
  
if len(data.Items) > 0 {
  
    rp.cursor = data.Cursor
  
    os.WriteFile(rp.cursorFile, []byte(strconv.FormatInt(data.Cursor, 10)), 0644)
  
}
  

这一机制彻底解决了早期版本中每次重启 Knowly 都会拉取全部历史结果、重复 AI 处理和去重检查的问题。

8.3 去重前置

syncText 函数中,远程去重检查被提前到 AI 处理之前。如果计算出的内容哈希在 NAS 当天目录的 .knowly_hashes 中已存在,则直接返回,整个 syncText 流程提前结束。这对于 Result puller 尤其重要,因为 results 队列中的很多内容可能之前已通过剪贴板同步过。

8.4 背压保护

在 Monitor 剪贴板轮询中,如果同步速度跟不上复制速度(例如 SSH 网络拥塞),itemChan 缓冲区(默认大小 10)满时,新的 Payload 会被丢弃并记录警告。这确保了 Monitor 永远不会阻塞用户操作。

同样,Chrome 扩展在处理知乎收藏夹批量推送时,通过可配置的推送间隔(默认 8 秒/篇)和错误暂停机制(连续失败 2 篇暂停 2 小时)实现流量控制,避免触发 API 风控。


9. 安全与隐私考量

9.1 认证与授权

  • Worker 的 /submit/pull/push/results 接口均通过 X-Auth-Key 进行基于 safeCompare 的时序安全认证。

  • Knowly 通过 SSH 密钥与 NAS 通信,私钥不离开本机。

  • Chrome 扩展使用的 Cookie 仅限本机浏览器,不传输至任何第三方服务器。

9.2 数据主权

所有抓取的内容直接存储于用户的私有 NAS,不经过任何云存储服务。AI 处理虽然调用外部 API,但可配置为本地模型(如 Ollama)或私有代理,确保数据不外泄。

9.3 Shell 注入防护

Knowly 在通过 SSH 执行远程命令时,所有路径参数均经过严格的 Shell 转义(单引号包裹,内部单引号转义为 '\''),彻底杜绝了因文件名特殊字符导致的注入风险。


10. 优化历程回顾:从简单复制到全自动管道

最初,Chrome 扩展的功能仅仅是在知乎页面上添加一个复制按钮,把文章内容复制到剪贴板。但很快发现:

  1. 禁止转载限制:部分文章无法直接选中复制。

  2. 需手动操作:每次都要记得点按钮,不够自动化。

  3. 复制后仍需手动粘贴到笔记应用,无法直接归档。

于是逐步演进:

  • 阅读模式:通过侧边栏渲染正文,绕过禁止复制限制,同时提供舒适的阅读环境。

  • 收藏夹监控:定时检查收藏夹,自动推送新文章到 IMA。

  • 任务 API 集成:通过 Worker 队列与 Knowly 联动,实现跨设备跨平台的内容自动同步。

  • 智能分流:发现 Knowly 抓知乎不稳定后,引入双队列设计,让扩展主导知乎抓取。

  • 游标持久化与去重前置:解决重启回溯和 AI 浪费问题,进一步打磨性能。

每一次迭代都在解决实际痛点,而不是凭空设计,最终形成今天这套高度自动化的系统。


11. 总结与展望

这篇文章详细拆解了一套个人知识自动归档系统的全链路实现,涵盖了从 URL 输入、智能分流、高可靠性抓取、内容转换增强,到去重存储和同步策略的完整技术细节。其核心价值在于:

  • 高可靠性:利用浏览器原生认证状态抓取知乎,成功率堪比手动操作。

  • 全自动化:手机提交后,全程无需任何人工干预,直至内容沉淀为带 AI 摘要的 Markdown 笔记。

  • 隐私可控:数据完全私有,不依赖任何第三方存储。

  • 可扩展性:双队列分流架构可轻易扩展至更多平台(如 B 站、GitHub Issue 等),只需在 Worker 和扩展中添加对应的识别和处理逻辑。

对于个人开发者而言,这套方案的最大启发或许是:充分利用你已有的基础设施(浏览器、NAS、Cloudflare Worker),通过巧妙的消息队列分拆和客户端专用化,可以构建出比许多商业产品更可靠、更自主的个人知识管理系统。


全文完