
打开我的笔记文件夹那一刻我自己都愣住了一千两百多条整整齐齐躺在那里标题五花八门内容从 Python 爬虫教程到咖啡手冲水温从产品方法论到一句不知道在哪抄来的鸡汤。收藏的时候每个都觉得“以后用得上”结果收藏完就再也没打开过。这就是典型的“落灰笔记”。我花了大概一个周末写了一套完全自用的整理流程用爬虫把分散在各个平台、浏览器收藏夹、本地草稿箱里的内容统一同步到一个本地数据库再通过 AI 批量分拣自动打标签、写摘要、判断哪些值得重读、哪些可以直接归档。整个过程跑完之后一千多条笔记变成了一张干净的知识地图。这套做法不复杂核心就是“爬虫同步 AI 分拣”今天把完整思路、代码和提示词都整理出来给同样被笔记淹没的朋友一个参考。1. 先搞清楚“落灰笔记”到底从哪里来1.1 落灰笔记的三类典型来源我做了一次全量盘点发现落灰笔记基本有三个来源。第一类是平台收藏夹比如刷到一篇好文章点了收藏、看到一条有价值的帖子存了链接这类占比最大问题也最明显平台一改版、链接一失效内容就再也找不回来。第二类是浏览器书签和临时保存的网页存的时候觉得“回头细看”实际上回头的时间永远不会来。第三类是本地随手记的片段可能是备忘录里的一段话、聊天里转给自己的链接、文档里留的一个想法。这三类内容的共同特点是零散、无结构、缺少统一的元数据。你甚至说不出自己到底“存过什么”更别说“存在哪”。所谓的落灰本质上是数据散落在太多容器里每个容器都太浅浅到装不下你的注意力。1.2 手动整理的困境如果你只有几十条笔记手动整理完全可以接受无非花一个下午逐条归个类。但一旦超过几百条手动整理的边际成本会急剧上升每条笔记要打开、阅读、判断分类、决定去留一分钟一条一千条就是十多个小时而且整理完你还是只能得到一个“分类后的文件夹”并没有真正理解这些笔记之间的关系。更麻烦的是可持续性。手动的整理动作和收藏动作是分离的你收藏时很爽整理时很痛苦两者之间的时间差越长整理动力越低。所以我当时的判断是必须把“采集”和“整理”两个动作自动化让整理成为一个随着收藏实时发生、几乎不用人工干预的后台流程。1.3 自动化方案的总体思路整体方案分两层。第一层叫“同步”用爬虫把分散在外部平台的内容抓下来统一存进一个本地数据库这一步解决的是“东西在哪儿”的问题。第二层叫“分拣”把数据库里每条笔记的标题和正文喂给大模型让 AI 输出分类、标签、摘要和行动建议这一步解决的是“这玩意儿到底讲了啥、值不值得再看”的问题。两层中间只需要一个非常薄的调度层定时触发爬虫增量抓取抓完后把新内容批量丢给 AIAI 的结果再写回数据库。整个链路清晰到可以画在餐巾纸上源头 - 采集 - 清洗 - 存储 - AI 分拣 - 入库 - 查询。这也是我比较推荐的个人知识管理架构逻辑简单每一环都可以独立替换。2. 技术选型不追求炫技只求稳定可控2.1 采集层三种接入方式怎么选很多人一听到“爬虫”就想到 requests 模拟请求、解析 HTML其实接入一个数据源之前优先级应该是官方 API 平台导出 页面抓取。官方 API 最稳定比如你用的笔记软件如果支持开放接口那直接调接口拿数据字段干净、没有反爬风险。平台导出是第二选择很多平台支持备份全部收藏数据导出 HTML 或 JSON 文件再解析一次性能拿到全量数据。页面抓取是兜底方案只在没有 API、也不支持导出时才用。我这套流程里三种方式都用到了浏览器书签走的是浏览器导出的 HTML 文件某个平台收藏夹走的是官方 API还有几个零散网页走的是 requests BeautifulSoup 解析。实际跑下来体感很明显API 和导出文件一次就能同步干净页面抓取则要经常处理结构变动。所以我建议你在落地时先盘点手上的数据源能走 API 就走 API能导出就别写抓取逻辑把爬虫留给真正绕不开的场景。2.2 存储层SQLite 三张表加 JSONL 存档存储层我选了 SQLite没有上 MySQL 或 Postgres原因很简单个人笔记量级最多几万条SQLite 单文件、零运维、备份方便一个 db 文件拷走就是全部数据。核心三张表notes存笔记主信息包括 id、来源、URL、标题、正文、抓取时间ai_tags存 AI 分拣结果包括分类、标签、摘要、行动建议sync_log存每次同步的批次信息和抓取状态。正文除了存进 notes 表我还同时以 JSONL 格式追加到一个目录里每条记录一行纯文本存档。这么做的好处是调试和重新分拣都方便——如果 AI 提示词改了我可以直接从 JSONL 重新跑一遍不用再抓一次网页。2.3 分拣层为什么用 LLM 做分拣分类和打标签这件事传统的做法是用规则或关键词匹配比如标题里含“Python”就归到技术类。但笔记内容太杂规则永远追不上真实语义一篇讲“如何用 Python 自动整理周报”的文章关键词在“周报”和“自动整理”上但本质上是效率工具类规则很难判断。大模型擅长的是语义理解给它一段正文它可以判断主题、抽取标签、写摘要甚至判断这条笔记未来会不会真的有用。我承认 LLM 有幻觉问题但在“分拣笔记”这个场景下输出是低风险的标签不对最多影响搜索摘要写偏了也不至于造成什么严重后果。只要提示词约束好输出格式再对 JSON 结果做一层校验整体准确性可以做到很高。2.4 运行载体一台常开的电脑或小服务器这套流程不需要很强的算力LLM 走的是 API 调用本地只跑爬虫、清洗和调度。我放在一台常年开机的迷你主机上系统是 Linux用 cron 做定时触发。如果你手头只有一个普通的 Windows 电脑开机时常开着也行用任务计划程序跑 Python 脚本效果没区别。3. 爬虫同步的实操细节3.1 通用采集脚本骨架我用 Python 写了一个比较通用的采集脚本思路是给每个数据源写一个fetcher统一返回清洗后的笔记对象。以最简单的网页抓取为例requests 拿 HTMLBeautifulSoup 解析标题和正文核心代码大概长这样import requests from bs4 import BeautifulSoup from urllib.parse import urlparse def fetch_url(url): headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout15) resp.raise_for_status() # 先用 meta 拿标题失败再取 h1 soup BeautifulSoup(resp.text, html.parser) title if soup.title and soup.title.string: title soup.title.string.strip() h1 soup.find(h1) if h1 and h1.get_text(stripTrue): title h1.get_text(stripTrue) # 取 body 文本按空行合并 body soup.find(body) text \n.join( line.strip() for line in body.get_text(\n).split(\n) if line.strip() ) if body else return { url: url, title: title[:200], content: text[:8000], source: urlparse(url).netloc, created_at: datetime.now().isoformat() }这段代码不复杂但很实用。注意几个点请求必须带 User-Agent不然很多站点直接拒超时设置不能省有些页面会卡住整个任务正文截断到 8000 字符就够了AI 分拣不需要全文截断还能省 token。3.2 HTML 正文清洗的坑清洗是爬虫链路里最容易被低估的一环。如果你直接把get_text()的原始结果喂给 AI会得到一堆导航文字、广告文案、页脚版权信息分拣效果会很差。我前后踩了几个坑总结出最简单有效的清理顺序第一优先提取article或main标签很多站点的正文都包在语义化标签里第二删除script、style、nav、footer、aside这些非正文区块第三把连续空白符合并成单个换行避免正文变成一个超长字符串第四去掉明显的“上一篇”“下一篇”“相关推荐”这类导航残留。清洗完的质量直接决定 AI 分拣的准确率这一步值得多花时间调试。我最初抓下来的很多页面清洗前正文里 40% 是噪音清洗后能降到 5% 以内。3.3 增量同步与去重指纹与 URL 归一化增量同步最怕两个问题重复入库和反复抓取。我用的去重方案是 URL 归一化加内容指纹双重校验。URL 归一化处理常见的情况去掉utm_*跟踪参数、去掉#锚点、统一http和https、统一末尾斜杠。归一化之后同一个链接的不同形态会被识别成同一条。内容指纹我用hashlib.sha256对清洗后的正文取前 16 位十六进制入库前先查指纹是否已存在存在就跳过。这两个字段我还加了数据库唯一索引双保险。import hashlib def normalized_url(url): from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse parsed urlparse(url.lower()) query [(k, v) for k, v in parse_qsl(parsed.query) if not k.startswith(utm_)] return urlunparse(parsed._replace(queryurlencode(query), fragment)) def content_fingerprint(text): return hashlib.sha256(text.encode(utf-8)).hexdigest()[:16]指纹去重有个好处同一个 URL 内容更新了指纹会变化你可以选择更新原记录而不是重复插入这样一条笔记始终只有一条记录。3.4 定时任务与失败重试同步任务我挂在 cron 里每天早上跑一次。但爬虫最怕的是某次请求卡住导致整个任务挂死所以脚本里做了三层防护requests 超时、每个数据源独立 try/except、失败数据写入sync_log等待下次重试。另外要加一个简单的“重试上限”同一个 URL 连续失败三次就标记为 dead不再反复抓。因为很多链接收藏时就注定了会 404没必要浪费请求。“死链”我单独打了一个状态后续搜索时可以直接过滤掉界面清爽很多。4. AI 分拣把大模型变成你的图书管理员4.1 分拣任务定义在写提示词之前我先明确了 AI 需要输出什么也就是分拣结果的结构。我的核心字段有五个分类、标签、摘要、是否需要行动、重读理由。分类用于粗粒度归档我限定了一个候选集合避免 AI 自由发挥出十个互相重叠的类目。标签用于细粒度检索每篇 3 到 5 个中文短词为主。摘要是一句话概括控制在一定字数内。是否需要行动这个字段很关键它把笔记分成“存档即可”和“待办后续”两类很多收藏夹里其实藏着大量待办事项比如“周末试试这个方案”“给同事推荐这篇文章”不拆出来就会继续落灰。最后的重读理由是给值得再看一遍的笔记准备的AI 会告诉你“这篇值得重读是因为它提供了可操作的分步指南”这条理由会显示在整理后的视图里。4.2 批量调用 LLM 的工程细节批量分拣时要考虑 token 成本和稳定性。我的做法是每批只处理 5 条笔记用一个异步循环并发调用并发数控制在 3 左右避免触发接口限制。每条笔记的正文只取前 2000 字标题单独给到因为标题往往是最浓缩的信息。调用前先做一轮本地规则预筛长度小于 200 字的纯链接、明显是购物订单确认页、已经被标记为死链的都不进 AI 队列。这样可以省掉一大笔没必要花的 token。调用后对返回结果做 JSON 解析解析失败就把这条笔记放进重试队列连续失败两次后标记为“待人工处理”。import asyncio import json from openai import AsyncOpenAI client AsyncOpenAI() SYSTEM_PROMPT 你是一名资深知识管理编辑。你会收到用户保存的笔记请判断内容主题并输出结构化 JSON。 async def classify_one(note): content f标题{note[title]}\n正文{note[content][:2000]} resp await client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f{content}\n\n{USER_PROMPT}} ], temperature0.2, response_format{type: json_object} ) return json.loads(resp.choices[0].message.content)我建议把temperature调低一点比如 0.2分拣场景不需要创意越稳定越好。另外如果你用的模型支持 JSON 输出模式一定打开能省掉大量解析报错。4.3 提示词设计要点提示词设计的核心是四个词角色、任务、约束、格式。先给 AI 一个角色设定让它知道自己在干嘛再明确任务步骤然后给出约束条件包括分类候选集、标签数量、摘要字数最后指定输出格式最好强制 JSON。还有一个容易被忽略的点是给示例。模型对“输出 3 到 5 个标签”的理解和你的理解可能有偏差。我在提示词里塞了两个 few-shot 示例AI 就会严格按照示例的风格来。这也是我迭代了好几版之后总结出的经验与其描述一百遍“标签要短”不如给它看两个具体例子。4.4 可直接抄的提示词模板这个提示词模板是我目前自用版本贴出来可以直接复制。它包含角色设定、任务描述、候选分类、few-shot 示例和 JSON 格式约束完整跑下来效果比较稳定。你是一名资深知识管理编辑负责把用户保存的零散笔记整理成结构化的知识卡片。 请阅读下面的笔记内容完成以下任务 1. 判断内容主题从以下候选中选择一个最合适的分类 技术开发、效率工具、产品设计、职场成长、商业思考、生活健康、阅读笔记、待办事项、其他。 2. 提取3到5个中文标签每个标签不超过6个汉字。 3. 用一句话概括这条笔记的核心内容不超过35个字。 4. 判断这条笔记是否需要后续行动只需要回答 true 或 false。 需要行动的情况包括文章里提到可执行的方案你打算实践、有值得推荐给别人的内容、有时间需要安排阅读、有需要回复或跟进的信息。 5. 如果这条笔记值得重读用一句话说明重读理由如果不值得重读理由写空字符串。 输出严格 JSON不要输出任何多余文字 {category: ..., tags: [...], summary: ..., needs_action: true, replay_reason: ...} 示例1 输入标题如何用 Python 批量压缩图片 输入正文本文介绍用 Pillow 库批量压缩图片的步骤包括安装依赖、循环处理、参数调优...... 输出{category: 技术开发, tags: [Python, 图片处理, 自动化], summary: 用 Pillow 批量压缩图片的完整方案, needs_action: true, replay_reason: 提供了可落地的代码方案适合实践} 示例2 输入标题咖啡萃取参数记录 输入正文今天试了 92 度水温研磨度 15粉水比 1:15萃取时间 2 分 30 秒口感偏酸...... 输出{category: 生活健康, tags: [咖啡, 手冲, 参数记录], summary: 手冲咖啡参数与口感记录, needs_action: false, replay_reason: } 现在开始处理新的笔记 标题{title} 正文{content}这套提示词最大的特点是“把话说死”分类给候选集标签给数量上下限摘要给字数限制输出给格式约束还配了示例。AI 生成的格式基本稳定JSON 解析成功率我实测在 95% 以上。5. 常见问题与排查技巧实录5.1 抓取被限制怎么办这是爬虫同步绕不开的问题。我遇到过的限制包括请求被 403、请求频率过高触发临时封禁、页面返回验证码而不是正文。解决思路分三步。第一步改请求头加上完整的浏览器 User-Agent必要时加 Accept-Language。第二步加限速每个请求之间 sleep 1 到 3 秒这个策略比较土但足够有效。第三步做重试退避遇到 403 或 429 就等 5 分钟再试连续失败就跳过。我还会主动过滤一些明显不支持抓取的站点比如某些需要登录才能看全文的平台这种就直接放弃不值得耗时间。5.2 正文清洗后质量太差最常见的表现是抓下来的正文第一行是“点击上方蓝字关注我们”最后一行是“本内容来源于网络侵删”中间还夹着大量无关推荐。这就是清洗规则没覆盖到位。我的排查方法是随机抽几十条入库记录直接看清洗后的正文开头和结尾如果反复出现同一批噪音文本就把对应的节点或关键词加进黑名单。比如很多站点都有“相关阅读”“热门推荐”这类区块可以直接在清洗函数里按标签和 class 名过滤。5.3 AI 输出不稳定输出不稳定的表现包括偶尔返回的不是 JSON、分类不在候选集里、标签数量忽多忽少。问题一大半出在提示词约束不够一小半出在模型本身。我的应对是三层校验第一层用response_format强制 JSON 对象第二层在代码里做字段校验缺失就补默认值第三层对分类做白名单检查不在候选集里就归为“其他”。标签数量我会在代码里截断到 5 个。这套组合下来分拣流程基本不会因为个别解析失败而中断。5.4 同步重复与循环增量同步跑久了容易发现同一条笔记被软去的 URL 重新抓回来。比如平台把旧链接跳转到新地址或者收藏时保存的是短链接抓取后跳转成了长链接。我处理的办法是入库前先记录最终跳转后的 URL下一次同步时用这个最终地址做归一化短链接归一化后和长链接保持一致就自然不会重复入库。还有一个高频坑是浏览器书签导出文件里带了大量重复记录同一篇文章被收藏了三四次。内容指纹去重会在这一层把重复彻底拦截掉所以指纹字段一定是入库前的最后一道闸。最后分享一点我的真实体会。这套流程跑通之后我最大的收获不是“笔记整理完了”而是我重新敢收藏东西了。以前收藏意味着负担现在收藏意味着有条流水线在后面兜底。你不需要等系统很完善再动手先抓下一百条笔记跑一遍流程哪怕 AI 分拣质量一般也比一千条堆在那里强。后续要扩展的话可以给每个标签生成一个“知识地图”也可以按“是否需要行动”筛选出真正值得消费的内容这些都是建立在统一存储和结构化数据之上的事。