知乎是中文平台最高质量内容的讨论平台,内容质量相当高,值得花大力气进行适配抓取;微信是国内最大的私域知识分发平台,最常用的沟通工具,是减少知识收集摩擦的最好通道;
从 URL 到知识资产:我的全自动内容抓取、转换与归档系统深度解析
本文详细拆解一套个人知识管理自动化体系的全链路设计——从手机一键提交链接,到浏览器插件安全抓取、AI 增强处理,最终沉淀至私有 NAS 形成结构化笔记。文章将逐一剖析 URL 提取、智能分流、高可靠性抓取、Markdown 转换、AI 增强、去重存储与跨组件同步等核心环节的工程考量与实现细节。
1. 背景与动机
在信息过载的时代,我们每天都在接触大量值得保存的文章——知乎回答、微信公众号推文、博客长文。传统的收藏方式存在诸多痛点:
-
平台绑定:知乎收藏夹可能因账号异常或内容删除而丢失;微信收藏没有导出的出口。
-
手动存档成本高:复制粘贴、整理格式、打标签、存到笔记软件,每一步都是摩擦。
-
跨设备壁垒:手机上看到的文章,要转到电脑上才能整理,流程断裂。
-
封闭平台反爬:知乎对非登录用户的限制极其严格,常规爬虫难以稳定抓取全文。
经过长期摸索和逐步迭代,我构建了一套以 Chrome 扩展 + Cloudflare Worker + Knowly 守护进程 为核心的个人知识管道。它能够:
-
手机一键提交:通过快捷指令将任意链接发送至中继 Worker。
-
智能分流:Worker 自动识别知乎链接与非知乎链接,放入不同队列。
-
安全抓取:Chrome 扩展利用浏览器的真实登录态抓取知乎正文;Knowly 守护进程负责微信公众号等通用网页。
-
内容转换:HTML 自动转换为 Markdown,保留图片、标题、加粗、链接等格式。
-
AI 增强:自动生成标签、摘要、质量评分,并整理内容结构。
-
自动归档:通过 SSH 同步至私有 NAS,按日期存储,三层去重保证无重复。
-
离线可靠:网络中断时暂存本地,恢复后自动重试,游标机制防止重复消费。
本文将深入剖析每个环节的技术方案、工程考量和优化历程,分享一条从 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,提取 content 或 url 字段;若解析失败,则直接当作纯文本处理。这种兼容性设计让终端不必关心格式细节。
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(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, "'");
const data = JSON.parse(jsonStr);
// 从 data.initialState.entities.answers 等路径中提取内容
}
这种双重抓取策略让扩展即便在 Cookie 部分失效或 API 限流时仍能工作。
4.3 HTML → Markdown 转换
知乎正文是 HTML 格式,扩展中实现了 htmlToMarkdown 函数进行转换,保留:
-
图片:优先取
data-actualsrc/data-src/src,转换为 -
标题:
<h1>~<h6>→#标记 -
加粗/斜体:
<strong>/<b>→**text**,<em>/<i>→*text* -
链接:
<a href="">→[text](href) -
代码:
<pre><code>→ 代码块,行内<code>→`code` -
引用:
<blockquote>→> text -
列表:
<li>→- item
转换时还对 HTML 实体( , <, — 等)进行了解码,确保输出为纯文本 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 转换过程需处理以下场景:
-
图片懒加载:知乎使用
data-actualsrc存放真实图片 URL,src往往是占位图。转换时优先匹配data-actualsrc,其次是data-src,最后才是src。 -
嵌套结构:如
<a><img></a>,先提取图片再处理链接。 -
代码块:知乎的代码块通常是
<pre><code>...</code></pre>,但行内代码是<code>直接包裹。需要区分处理。 -
引用块:知乎的
<blockquote>内可能包含多段落,转换时每行前加上>标记。
6.2 Knowly 的正文提取
对于通用网页,Knowly 的 extractContent 实现了逐级回退:
-
微信正文提取(专用正则)
-
语义化标签:
<article>→<main>→<div class="content"> -
回退到
<body> -
清除脚本、样式、注释,压缩空白
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 实现了纵深防御的三层去重:
-
内存级:Monitor 维护
lastHash,相同内容的连续复制直接过滤(读写锁保护)。 -
持久化级:进程重启后从
status.json恢复lastHash,防止重启后立即重复同步刚同步的内容。 -
远程索引级:每次写入前,检查 NAS 当天目录下的
.knowly_hashes索引文件。该文件使用grep -qxF进行 O(1) 精确匹配,比遍历目录快几个数量级。此检查现在被前置到 AI 处理之前,从而避免了无意义的 API 消耗。
此外,Result puller 拉取结果时也通过游标避免重复消费(见第 8 节)。
7.3 离线队列与重试
如果 SSH 连接中断导致同步失败,Knowly 会将内容存入本地 Outbox(JSONL 格式),并立即执行 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 扩展的功能仅仅是在知乎页面上添加一个复制按钮,把文章内容复制到剪贴板。但很快发现:
-
禁止转载限制:部分文章无法直接选中复制。
-
需手动操作:每次都要记得点按钮,不够自动化。
-
复制后仍需手动粘贴到笔记应用,无法直接归档。
于是逐步演进:
-
阅读模式:通过侧边栏渲染正文,绕过禁止复制限制,同时提供舒适的阅读环境。
-
收藏夹监控:定时检查收藏夹,自动推送新文章到 IMA。
-
任务 API 集成:通过 Worker 队列与 Knowly 联动,实现跨设备跨平台的内容自动同步。
-
智能分流:发现 Knowly 抓知乎不稳定后,引入双队列设计,让扩展主导知乎抓取。
-
游标持久化与去重前置:解决重启回溯和 AI 浪费问题,进一步打磨性能。
每一次迭代都在解决实际痛点,而不是凭空设计,最终形成今天这套高度自动化的系统。
11. 总结与展望
这篇文章详细拆解了一套个人知识自动归档系统的全链路实现,涵盖了从 URL 输入、智能分流、高可靠性抓取、内容转换增强,到去重存储和同步策略的完整技术细节。其核心价值在于:
-
高可靠性:利用浏览器原生认证状态抓取知乎,成功率堪比手动操作。
-
全自动化:手机提交后,全程无需任何人工干预,直至内容沉淀为带 AI 摘要的 Markdown 笔记。
-
隐私可控:数据完全私有,不依赖任何第三方存储。
-
可扩展性:双队列分流架构可轻易扩展至更多平台(如 B 站、GitHub Issue 等),只需在 Worker 和扩展中添加对应的识别和处理逻辑。
对于个人开发者而言,这套方案的最大启发或许是:充分利用你已有的基础设施(浏览器、NAS、Cloudflare Worker),通过巧妙的消息队列分拆和客户端专用化,可以构建出比许多商业产品更可靠、更自主的个人知识管理系统。
全文完