书签这个东西有点像家里的储物间。刚开始往里扔的时候都觉得“以后肯定用得上”等到某天真正想找一份东西翻半天翻不到才发现里面塞满了三年前的快递盒和不知道哪来的塑料袋。我一个做前端的朋友上个月换了台机器导出的书签文件有 1.8MB列表拉下来四千多条他自己都懵了——里面有三套重复的文档站、七八个已经打不开的旧项目地址还有大量只存了域名、连标题都是“无标题”的条目。这就是我后来把书签批量导入、批量去重、自动备份这套流程做成固定动作的直接原因收藏这件事的成本太低了而维护的成本一直没人愿意付。这里说的“首页替代”不是让你换个浏览器而是把浏览器默认的新标签页换成一个自己完全掌控的入口页。默认首页推荐位是给别人看的你自己每天高频访问的可能就那二十个站点加上几组按项目分好的链接。把这套入口做成一个静态页面数据源就是你自己清洗过的书签库改一次数据全端同步还不用忍受推荐流的干扰。整篇内容会从数据格式、导入预处理、去重算法、备份策略一直讲到首页自建适合三类人看书签已经乱到不敢打开的重度用户、需要给团队做统一导航页的开发者、以及单纯想搞明白“去重到底怎么判”的技术爱好者。难度上前半部分照着做就行后半部分的自建首页需要一点 HTML 和脚本基础我会把关键代码贴全。1. 先想清楚书签管理到底在管什么1.1 书签乱掉的三个真实原因大部分人的书签不是一天乱的是三个机制叠加出来的结果。第一个是零成本收藏点一下星标就完事大脑在这一刻获得了“我已经掌握了这个知识”的虚假满足感但实际上只是把信息转移到了另一个地方。第二个是多端并行写入办公室的台式机存一批、家里的笔记本存一批、手机浏览器再存一批三份数据互相不知道对方存在同步机制一旦没开全重复就是必然的。第三个是缺乏退出机制书签只增不减没人会定期清理一年下来增速是线性的混乱度却是指数级的。我在统计自己那批数据时做过一个粗略分类四千多条里大致是这样的分布类型占比典型特征重复条目约 22%同一 URL 不同标题、同一内容不同域名已失效链接约 8%域名过期、路径变更、需要登录一次性查阅约 45%存完之后再没打开过真正高频约 5%每周至少访问一次分类模糊约 20%不知道该归到哪个文件夹这张表最有价值的地方不是数字本身而是它告诉你一件事去重能解决的是那 22%而真正让首页变得好用的是那 5%。很多人一上来就想做精美的导航页结果把四千条原封不动搬上去做出来的东西比默认首页还难用。所以顺序一定是先清洗、再筛选、最后才呈现。1.2 整套方案的设计思路我把这套流程拆成五个阶段每个阶段解决一个独立问题前后有明确的输入输出边界这样任何一环出问题都不会污染下一环。第一阶段是归集把所有设备、所有浏览器、所有账号下的书签导出成统一格式的 HTML 文件全部丢进一个待处理目录。第二阶段是解析与规范化把 HTML 解析成结构化记录统一字段、统一编码、统一 URL 形态。第三阶段是去重与清洗按 URL 指纹和标题相似度双维度合并同时标记可疑失效链接。第四阶段是备份固化把清洗后的数据落库并建立定时快照机制。第五阶段是呈现从库里生成一份精简的 JSON喂给自建的首页。这个流水线式设计的核心考量是可重入。你不可能一次就把数据洗得干干净净总会发现新的重复模式。如果整个流程是一坨脚本跑到底改一次规则就得全量重跑心理负担很重。拆成阶段之后中间产物都落盘我只改第三阶段的判定逻辑前两个阶段的结果直接复用迭代成本降到几乎为零。这也是我在做数据清洗类工作时的通用习惯——中间结果一定要能落盘复用不要迷信“一条命令搞定”。1.3 工具选型的取舍逻辑市面上解决书签问题的路径大致三条我分别试过说说体会。第一条是装现成的书签管理扩展。优点是零门槛安装完点几下就能查重、能分标签、能换新标签页样式。缺点是数据在别人的容器里扩展一旦停止维护或者权限策略收紧你攒的分类体系可能一夜归零而且这类扩展的去重逻辑通常是“URL 完全相等”对?utm_sourcexxx这种尾巴基本无感实际查重率大打折扣。第二条是纯手工整理。我试过一个周末整理了 600 条左右就崩溃了因为手工判断“这两个链接是不是同一个东西”需要打开页面确认单条成本大约 20 秒四千条就是 22 小时。这条路对超过 500 条的数据量是不成立的。第三条是脚本 静态首页自建。前期投入大概两三个小时写脚本之后每次新增书签只需要跑一次增量处理成本趋近于零。数据完全在本地格式是纯文本十年后随便什么工具都能读。我最终选的是第三条但不是因为它“更高级”而是因为它的失败模式更温和。扩展挂了你会慌脚本挂了你看一眼报错改两行就接着跑。对于这种要长期维护的个人基础设施可控性比便利性重要得多。当然如果你只有两三百条书签装个扩展认真整理一遍其实更划算不必为了技术而技术。2. 数据格式与导入前的准备工作2.1 标准 HTML 书签文件的结构拆解主流浏览器导出的书签文件格式基本都遵循一套很老的约定业内通常叫它标准 HTML 书签格式。它的本质就是一个嵌套的DL列表外层DL代表文件夹层级每个条目是一个DT里面包着A标签HREF是地址标签之间的文本是标题ADD_DATE和ICON这类属性存的是添加时间戳和图标。一段最小化的结构长这样DLp DTH3 ADD_DATE1700000000 PERSONAL_TOOLBAR_FOLDERtrue书签栏/H3 DLp DTA HREFhttps://example.com/docs ADD_DATE1700000001 ICONdata:image/png;base64,...示例文档站/A DTH3 ADD_DATE1700000002子文件夹/H3 DLp DTA HREFhttps://example.org/a?utm_sourcefeed ADD_DATE1700000003示例文章/A /DLp /DLp /DLp看懂这个结构之后你会发现两件事。第一文件夹层级信息藏在嵌套里如果你用正则硬匹配A标签会丢掉分类信息后续想按文件夹归类就没依据了。第二属性里的时间戳是有用的它在去重时能帮你判断“哪一条是更早收藏的”我后面合并重复项时会用到。所以解析阶段一定不能用正则得用一个真正的 HTML 解析器按事件流逐层压栈。我用的方案是 Python 标准库里的html.parser不需要装任何第三方依赖几百行就能写得清清楚楚。选标准库不选第三方解析库的理由很实际这个脚本我打算用很多年第三方库的 API 变动过一次就够你喝一壶而标准库的接口十年不动。2.2 导入前必须做的三件事很多人拿到导出文件就直接往工具里塞结果导完更乱。导入之前有三件事必须先处理。第一件是编码统一。不同浏览器导出的文件有的声明charsetUTF-8有的干脆不声明中文标题就变成了乱码方框。稳妥做法是先用文本编辑器打开看一眼如果中文正常就直接用如果乱码按 UTF-8 重新解释一遍。我在脚本里加了一个兜底逻辑优先按 UTF-8 解码失败则退到 GB18030再失败就按errorsreplace硬解并打日志标记宁可丢几个字符也不要让整个流程崩掉。第二件是字段补全。大量历史书签的标题是空的或者干脆是域名本身。这类条目在最终首页上呈现出来毫无辨识度用户根本不知道该不该点。我的处理是给空标题补一个“域名 路径首段”的默认值比如example.com / docs虽然不如人工命名但至少能看出这是个什么东西。后续在首页生成阶段再用关键词自动打标签补一层。第三件是先做数量统计。把每个来源文件的书签条数、去重后的条数、目录层级深度都算出来形成一份基线。这份基线最大的作用是给你心理预期如果导入后条数和导出时对不上你能立刻发现是丢了还是被合了而不是等到几个月后才发现某批数据没导进来。注意不要在同一台机器上同时开着多个浏览器的账号同步。我吃过一次亏两个浏览器互相把对方同步过来的书签再同步回去产生了大量xxx (1)这类后缀的重复条目清理起来非常恶心。做导入前先把所有端的自动同步关掉。2.3 批量导入的实操步骤真正执行的时候我建议按这个顺序走不要跳步。新建一个工作目录比如~/bookmark-lab下面建raw/原始导出、work/中间产物、db/最终数据三个子目录。把每个来源导出的 HTML 文件重命名成有意义的文件名比如desktop-20240115.html、laptop-20240115.html文件名里带设备名和日期后面追溯来源全靠它。全部丢进raw/先不做任何处理。跑解析脚本输出一份work/records.jsonl每行一条记录字段包括title、url、folder、add_date、source。检查records.jsonl的总行数是否等于各文件书签数之和。第四步的记录结构我建议就用 JSON Lines一条一行好处是可以用命令行工具直接过滤、排序、统计不需要起数据库。一条记录大概长这样{title:示例文档站,url:https://example.com/docs,folder:书签栏/技术/文档,add_date:1700000001,source:desktop-20240115}字段里我特意保留了folder的完整路径而不是只留叶子节点名因为很多人的文件夹命名是重复的光看“文档”两个字你根本不知道是哪个“文档”。完整路径虽然长但在后续分类统计时信息量足够。3. 批量去重从 URL 规范化到相似标题判定3.1 URL 归一化是去重的地基去重效果好不好九成取决于归一化做得够不够狠。两个链接在浏览器里看起来不同、打开后是同一篇文章这种情况太常见了。我总结了一组必须处理的规则按优先级从高到低规则处理方式为什么这么处理协议差异http 与 https 视为同一站点升级后老链接仍能跳转主机大小写统一转小写域名本身不区分大小写www.前缀灰度合并部分站点两者都通部分不通需白名单控制默认端口去掉:80:443语义完全等价尾部斜杠统一去掉/a与/a/绝大多数站点等价锚点#全部去掉页面内锚点不影响内容跟踪参数删除白名单外参数utm_*、spm、from这类纯渠道标记路径大小写保留很多站点路径区分大小写不能动关于www.的处理我要单独说一句这是最容易翻车的地方。有些站点的www版本和裸域版本内容完全不同比如裸域是官网、www是旧版应用入口。我一开始为了省事直接把www.全删了结果合并掉了一批本该保留的条目。后来的做法是默认保留只对一份人工维护的白名单域名做合并白名单里全是确定两者等价的常见站点。这个名单不长三五十条就够覆盖大部分场景。跟踪参数的处理也是同理不能一刀切删所有 query。有些参数是业务必需的比如文章 ID、分页参数page2。我的策略是维护一份“确定无用的参数名”黑名单只有命中黑名单的才删其余一律保留。黑名单大致是这些TRACKING_PARAMS { utm_source, utm_medium, utm_campaign, utm_term, utm_content, spm, from, source, share_token, ref, referrer, fbclid, gclid, timestamp, t }注意t和timestamp这类要看场景有些站点用它们做缓存击穿删了不影响内容但也有的站点用t作为真实的资源标识删了就错页。所以我一般先只启用最保险的那几个跑一遍看合并结果逐条确认没问题再逐步加。去重宁可漏合并不可错合并因为漏了只是留个重复项错了就是丢数据。3.2 双维度去重策略只有 URL 归一化是不够的。我遇到过大量“同一篇文章被不同站点转载”的情况URL 完全不同标题一模一样。反过来也有“同一站点不同页面标题完全相同”的情况比如各种首页或者文档。所以必须两个维度交叉判断。我的判定逻辑是一棵小决策树URL 指纹相同 → 直接判定为重复合并。URL 指纹不同但标题完全相同且域名相同 → 高度疑似重复进入候选人工或规则复核。URL 指纹不同标题相似度 ≥ 0.85 → 疑似重复进入候选。其余情况 → 保留。标题相似度我用的是difflib.SequenceMatcher标准库里就有。0.85 这个阈值是我试出来的设到 0.9 会漏掉“示例文档站使用指南”和“示例文档站使用手册”这类设到 0.7 又会把“第一章 概述”和“第二章 概述”误判成同一篇。0.85 是一个相对平衡的点但仍然会有误判所以凡是靠相似度进来的一律只做标记不做自动合并最后统一输出一份候选清单让我过一眼。合并的时候保留哪一条我定的优先级是最早的add_date优先其次分类路径更深的优先再次标题更长的优先。理由分别是最早说明是你最初认可的那个来源路径更深说明当时分类更用心标题更长通常意味着信息更完整而不是“无标题”。3.3 去重脚本的核心实现把上面的规则落成代码主流程大概是这样import hashlib, json from html.parser import HTMLParser from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode class BookmarkParser(HTMLParser): def __init__(self): super().__init__(convert_charrefsTrue) self.stack [] # 文件夹层级栈 self.pending_folder None self.in_anchor False self.current {} self.records [] self.expect_h3 False def handle_starttag(self, tag, attrs): a dict(attrs) if tag h3: self.expect_h3 True elif tag a: self.in_anchor True self.current { url: a.get(href, ), add_date: int(a.get(add_date, 0) or 0), folder: /.join(self.stack), title: } def handle_data(self, data): if self.expect_h3: self.pending_folder data.strip() elif self.in_anchor: self.current[title] data def handle_endtag(self, tag): if tag h3: self.expect_h3 False if self.pending_folder: self.stack.append(self.pending_folder) self.pending_folder None elif tag dl: if self.stack: self.stack.pop() elif tag a and self.in_anchor: self.in_anchor False self.current[title] self.current[title].strip() self.records.append(self.current.copy())解析部分解决的是“层级压栈”的问题。DL结束就弹栈H3结束就压栈逻辑很朴素但对标准格式的文件足够用。要提醒一点不同浏览器导出的格式在嵌套细节上会有细微差别有的会把DT写成不成对的标签所以解析器必须对不规范的标签序列有容错别指望 HTML 是完美的。归一化与指纹部分是这样的def normalize(url: str) - str: if not url: return parts urlsplit(url.strip()) scheme https if parts.scheme in (http, https) else parts.scheme host parts.hostname.lower() if parts.hostname else port parts.port if port in (80, 443): port None netloc host if port is None else f{host}:{port} q [(k, v) for k, v in parse_qsl(parts.query, keep_blank_valuesTrue) if k.lower() not in TRACKING_PARAMS] q.sort() path parts.path.rstrip(/) or / return urlunsplit((scheme, netloc, path, urlencode(q), )) def fingerprint(normalized: str) - str: return hashlib.sha1(normalized.encode(utf-8)).hexdigest()q.sort()这一步经常被忽略但它其实很关键。?a1b2和?b2a1在服务端看来是同一个请求字符串却不相等不排序就会漏合并。排序之后参数顺序统一指纹才能稳定。主流程跑下来我在四千多条的真实数据上得到的合并结果是完全相同的 URL 指纹合并掉 611 条跟踪参数差异导致的合并 187 条标题相似度进入候选的 214 条人工确认后实际合并 63 条。也就是说自动规则解决了大约 18% 的冗余剩下的都是需要人眼判断的模糊情况。这个比例很说明问题——如果你指望一个脚本把书签洗得干干净净一定会失望。3.4 人工复核怎么抽样候选清单动辄两三百条逐条看也很累。我的做法是分层抽样加全量扫标题。先按相似度分组把 0.95 以上、0.85 到 0.95 分成两档。0.95 以上那档几乎都是真重复我只需要看标题和域名对不对得上速度很快通常两秒钟一条。0.85 那档才是真正需要打开页面确认的量少的时候手动点开量大的时候我把两个 URL 并排放在一个本地 HTML 页里用 iframe 肩并肩对照扫一眼就能判断。这个对照页是个小工具本质上就是读候选清单生成一个双栏页面不到一百行但省下来的时间很可观。实操心得复核阶段不要追求一次到位。我的习惯是先把自动合并的跑完、上线用两周等新标签页真正开始用了再把候选清单翻出来处理。因为使用过程中你会自然发现哪些条目是真的重复比对着列表空想准确得多。4. 自动备份把“记得导出”变成“不用管”4.1 备份策略的三个关键参数书签这类数据的特点是丢失的概率低但一旦丢失的挽回成本极高。它不是代码没有 Git 那样的协作生态大部分人的备份就是“某天想起来手动导出一次”。我把备份策略拆成三个必须明确的参数。备份粒度每天一次够用。书签的变化频率远低于代码我一个月的净新增也就一百多条日粒度完全能覆盖。如果你的书签主要来自某个自动化采集流程那就得提到小时级但那已经是另一个场景了。保留周期我采用的是“近七日全留 近十二月每月一份”的滚动策略。七天的日备能覆盖误删误改的即时恢复每月的月度快照能覆盖长期回溯。这样一年的数据量大概是 7 份日备加 12 份月备每份按 1MB 算也就不到 20MB完全不占地方。校验方式光备份不校验等于没备份。我每次备份完会做三项检查——文件大小是否在合理区间、解析出的书签条数是否与上一次相差超过 5%、文件哈希是否与已有备份重复重复说明当天没更新虽然不一定是问题但值得留意。三项里只要有一项异常就打日志告警。4.2 定时任务的配置方法备份脚本本身很简单核心就是把当前书签库导出成带时间戳的文件。难的是“怎么让它自己跑起来”这取决于你用的是什么系统。在 Linux 或 macOS 上用cron最省事。编辑用户的 crontab加一行# 每天凌晨 2 点 10 分执行备份避开整点的任务高峰 10 2 * * * /usr/bin/python3 /home/user/bookmark-lab/backup.py /home/user/bookmark-lab/logs/backup.log 21时间选 2 点 10 分而不是 2 点整是个小经验。整点是各种系统任务集中的时间机器负载高脚本跑得慢还容易和其他任务抢 IO。错开十分钟体验好很多。在 Windows 上就用任务计划程序图形界面不太适合写进文档我一般直接用命令行注册任务schtasks /create /tn BookmarkBackup /tr python C:\bookmark-lab\backup.py /sc daily /st 02:10备份脚本的核心逻辑是“导出 归档 清理”清理这一步很多人会漏掉结果是几个月后目录里堆了几百个文件反而没人敢动。import json, shutil, time from pathlib import Path DB Path.home() / bookmark-lab/db/bookmarks.json OUT Path.home() / bookmark-lab/backups OUT.mkdir(parentsTrue, exist_okTrue) def backup(): stamp time.strftime(%Y%m%d-%H%M) target OUT / fbookmarks-{stamp}.json shutil.copy2(DB, target) # 清理保留最近 7 个日备 全部月度快照 files sorted(OUT.glob(bookmarks-*.json), keylambda p: p.stat().st_mtime) daily [f for f in files if not f.name.endswith(-0101-0000.json)] for f in daily[:-7]: f.unlink() if __name__ __main__: backup()清理逻辑我这里写得简化了实际用的时候建议按“文件名里的日期”判断而不是按修改时间因为拷贝操作有可能改变修改时间按时间排序会出错。这是个很隐蔽的坑我调试了两次才发现问题出在copy2和copy的区别上。4.3 异地与多副本本地备份只能防误删防不了硬件故障。所以我还加了一层异地副本把备份目录再往一个外部存储或者另一台常开的设备上同步一份。同步方式我倾向于用最简单的 rsync 或者直接定期拷贝不做实时同步——实时同步有个致命问题如果你误删了源文件同步会把删除动作也复制过去异地副本一起没了而定期拷贝相当于给了一个时间窗口删了还有机会在下一轮同步前救回来。# 每次备份后追加同步到外部存储使用归档模式保留时间戳 rsync -av --delete-after ~/bookmark-lab/backups/ /mnt/external/bookmark-backups/--delete-after这个参数要慎用它的作用是让目标端严格镜像源端。好处是不会积累垃圾坏处是源端删了目标端也会删。如果你对安全性要求极高可以去掉它改成只增不删代价是目标端会越来越臃肿需要你自己定期手动整理。4.4 恢复演练没验证过的备份等于没有这条我要单独拿出来讲因为它是这套流程里最容易被跳过、也是最容易出事的一环。备份文件静静地躺在那里你永远不知道它能不能用直到有一天你真的需要它。我的做法是每个季度做一次恢复演练流程很简单找一台干净的机器或者一个临时目录把最近一份备份文件拿出来按导入流程完整走一遍确认能解析、条数对得上、分类路径完整、首页能正常渲染。整个过程十五分钟但它带来的确定性远超这十五分钟的价值。有一次演练我就发现了问题某个版本的脚本在写库时用了ensure_asciiFalse但读取时没指定编码在中文环境下正常换到另一台默认编码不同的机器上直接报错。如果没演练这个雷会一直埋到真正需要恢复的那天才爆。注意恢复演练一定要用独立目录不要直接覆盖生产数据。我有一次图省事演练时把备份解到工作目录里结果后面跑增量脚本时把演练数据当成新的合并进去了花了半天才理清楚。5. 首页替代把书签变成每天真正会用的入口5.1 首页真正需要解决的诉求做自建首页之前我建议先想清楚一个问题你每天打开新标签页到底想看到什么我观察自己的使用习惯答案其实很朴素——八十次打开里有七十次是为了去那五六个固定站点剩下十次才是搜索。而默认新标签页给我的是一堆推荐内容和一堆我永远不会点的快捷方式。所以自建首页的诉求可以拆成四条优先级从高到低第一是快。页面必须在 200 毫秒内渲染完成不能有任何外部请求阻塞。这意味着不能引 CDN 的字体、不能引第三方图标库、不能做任何网络请求。整个页面就是一个 HTML 文件加一个 JSON 数据文件纯本地加载。第二是可搜索。二十个链接靠眼睛找还行两百个就必须有搜索框。搜索逻辑用纯前端的模糊匹配就够不需要后端。第三是分类清晰但不深。分类层级超过两层你在首页上就得点两次才能到目标体验直接崩掉。我的分类控制在两层第一层是分组展开后直接是链接。第四是可维护。改一个链接不应该需要动 HTML 结构只改 JSON 数据文件就行。这样首页的样式和内容彻底解耦。5.2 数据结构与生成逻辑首页吃的数据不是完整的书签库而是一份精简版。完整的库里有四千条首页上只放真正高频的两百条左右。所以我加了一个star字段标记哪些条目需要出现在首页这个字段在清洗阶段由访问频率或者手动标记产生。{ groups: [ { name: 常用工具, items: [ {title: 示例文档站, url: https://example.com/docs, tag: 文档}, {title: 示例控制台, url: https://example.org/console, tag: 运维} ] }, { name: 学习资料, items: [ {title: 示例教程, url: https://example.net/tutorial, tag: 教程} ] } ] }分组名和条目的对应关系我直接从清洗阶段保留的folder路径里取第一层也就是顶层文件夹名。这样你在浏览器里怎么分类首页就怎么呈现不用维护两套分类体系。这一点的价值在长期使用中会非常明显——只有一个分类来源就不会出现两边不一致的情况。生成脚本就是在清洗完的库上做一次过滤和分组输出上面这个 JSON。我把它挂在备份脚本前面每次备份前先重新生成一次首页数据保证首页永远和书签库一致。5.3 一个够用的静态首页实现下面这个模板是我用了很久的版本去掉了很多花哨的东西只保留必要的功能。整个文件不到 150 行没有任何外部依赖。!DOCTYPE html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title我的入口/title style :root { --bg:#fafafa; --fg:#222; --muted:#888; --line:#e5e5e5; } media (prefers-color-scheme: dark) { :root { --bg:#1a1a1a; --fg:#e8e8e8; --muted:#888; --line:#333; } } * { box-sizing: border-box; } body { margin: 0; padding: 48px 24px; background: var(--bg); color: var(--fg); font: 15px/1.6 -apple-system, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif; } .wrap { max-width: 880px; margin: 0 auto; } #q { width: 100%; padding: 12px 16px; font-size: 16px; color: var(--fg); background: transparent; border: 1px solid var(--line); border-radius: 8px; outline: none; margin-bottom: 32px; } #q:focus { border-color: var(--muted); } .group { margin-bottom: 28px; } .group h2 { font-size: 13px; font-weight: 600; color: var(--muted); letter-spacing: .08em; text-transform: uppercase; margin: 0 0 12px; } .items { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; } .item { display: block; padding: 10px 12px; border-radius: 6px; color: var(--fg); text-decoration: none; border: 1px solid transparent; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .item:hover { border-color: var(--line); background: rgba(128,128,128,.06); } .empty { color: var(--muted); padding: 24px 0; } /style /head body div classwrap input idq typetext placeholder搜索链接回车打开第一个结果 autofocus div idapp/div /div script let DATA { groups: [] }; let visible []; fetch(./bookmarks.json).then(r r.json()).then(d { DATA d; render(d.groups); }); function render(groups) { const app document.getElementById(app); app.innerHTML ; visible []; if (!groups.length) { app.innerHTML div classempty没有匹配的链接/div; return; } groups.forEach(g { const sec document.createElement(div); sec.className group; const h document.createElement(h2); h.textContent g.name; sec.appendChild(h); const box document.createElement(div); box.className items; g.items.forEach(it { const a document.createElement(a); a.className item; a.href it.url; a.textContent it.title; a.title it.url; box.appendChild(a); visible.push(a); }); sec.appendChild(box); app.appendChild(sec); }); } document.getElementById(q).addEventListener(input, e { const kw e.target.value.trim().toLowerCase(); if (!kw) { render(DATA.groups); return; } const filtered DATA.groups.map(g ({ name: g.name, items: g.items.filter(it it.title.toLowerCase().includes(kw) || it.url.toLowerCase().includes(kw)) })).filter(g g.items.length); render(filtered); }); document.getElementById(q).addEventListener(keydown, e { if (e.key Enter visible.length) window.location.href visible[0].href; if (e.key Escape) { e.target.value ; render(DATA.groups); } }); /script /body /html这段代码有几个设计上的取舍值得说。用fetch读 JSON 而不是把数据内联进 HTML是为了让内容和结构分离改数据不用动页面。但fetch在file://协议下会被跨域策略拦住所以这个页面不能直接双击打开得用一个本地静态服务跑起来。最省事的方式是在目录下执行python3 -m http.server 8080然后浏览器访问本地端口。如果你希望双击就能用那就得把 JSON 内联成const DATA {...}的形式代价是每次改数据都要重新生成 HTML 文件——反正生成脚本都已经写了这点代价其实可以接受。键盘交互是我用得最多的部分。搜索框自动聚焦输入几个字母回车直接跳转这个动作比“移动鼠标、找到图标、点击”快得多。Escape清空搜索回到全量视图也是肌肉记忆级别的操作。这类小交互看起来不起眼但它是“首页替代”能不能真正替代默认页的关键——如果新首页比默认页慢你很快就会回去用默认页。5.4 离线与隐私上的考量自建首页有个经常被忽略的好处它完全不联网也能用。我经常在没有网络的环境下工作默认首页在这种时候只会转圈而自建页面的链接列表照样能看至少能回忆起某个资源叫什么名字、存在哪个位置。隐私上也是同理。默认新标签页会加载推荐内容这些内容来自网络请求你的浏览行为在某种程度上是被记录的。自建首页只加载本地文件一个外部请求都没有干净得可以离线打包带走。这是我坚持自建而不是用现成扩展的最实际的理由之一。6. 常见问题与排查实录6.1 问题速查表这套流程跑了两年多遇到的问题基本集中在下面这几类整理成表格方便对照排查。现象可能原因排查方向处理办法导入后条数明显变少归一化过于激进对比合并前后的指纹分布检查www白名单和参数黑名单是否过宽中文标题变乱码编码声明缺失或错误用十六进制查看头部字节加 UTF-8 → GB18030 的回退解码文件夹层级全平了解析器未处理嵌套打印stack的变化轨迹确认DL开始与结束是成对压弹栈首页加载空白fetch被跨域拦截看控制台报错起本地静态服务或内联数据备份文件体积异常写入了重复数据统计条数变化曲线检查增量脚本是否重复合并定时任务没执行环境变量缺失查看任务日志脚本内用绝对路径避免依赖 PATH相似度误判严重阈值过低或标题太短抽样看候选对提高阈值短标题直接跳过相似判定表格里“定时任务没执行”这条值得展开。我用 cron 跑脚本时踩过一次经典坑手动执行完全正常放到定时任务里就失败。排查半天发现是 cron 的环境变量极简PATH 里没有 Python 的路径脚本第一行python3直接找不到。解决办法很简单所有涉及的命令和路径都写绝对路径不依赖任何环境变量。这个坑在 Windows 的任务计划程序里也有类似表现本质原因是一样的。6.2 几个踩过的坑和私房技巧第一个坑是“过度去重”。我一开始追求极致的合并率把参数黑名单设得特别宽结果把带分页参数的列表页全合并成了一条丢了后面几页的内容。教训是任何删除型的规则都要有白名单兜底宁可多留一条也不要少一条。现在我所有归一化规则都跑在“只标记不删除”模式下先观察一周确认无误才开启实际合并。第二个坑是图标和缩略图。早期版本我把书签里的ICON属性base64 编码的小图标也一起存了导致数据文件膨胀到十几 MB首页加载肉眼可见地卡。后来全部丢掉首页改用纯文本视觉上反而更清爽。如果你的首页确实需要图标建议只对首页上那两百个条目额外维护一份图标别把整个库的图标都带上。第三个坑是分类的过度设计。我最开始设计了七大类三十多个子类结果用了两个月发现绝大部分条目都在三个类里剩下的类全是空的。分类体系是给检索服务的不是为了完整性。我的建议是先只分三到五个大类等某类条目超过三十条再拆子类让分类跟着数据长而不是先画好框子往里塞。第四个是私房技巧给链接打“有效期”标签。我在清洗脚本里加了一步对每条 URL 发一个轻量请求只请求头部超时三秒把返回 4xx 或 5xx 的条目标记成suspect。首页上默认不显示这些条目但数据库里保留。这样做的收益是首页永远没有死链代价是需要偶尔联网跑一次。注意这个探测动作不要太频繁同一个域名短时间内连续请求容易被限流我一般是按域名分组每组之间加一秒间隔四千条跑完大概二十来分钟放在夜里执行就行。第五个是把这份数据当成纯文本资产长期维护。它的本质就是一堆记录JSON 或者 JSONL 都能存任何编辑器都能打开任何脚本都能处理。我刻意避开了需要特定软件才能读的格式就是为了十年后哪怕工具全换了我用grep也能把数据捞出来。基础设施类的东西简单到无聊才是最可靠的状态。用了这么久我自己最直观的体会是这套流程真正的价值不在那个首页有多好看而在于它逼着你把“收藏”这个动作从无意识变成有意识。以前点星标是下意识的现在因为知道数据会经过清理、会出现在首页上点之前会多想一秒“这个我真的会再用吗”。就这一秒的犹豫让我的新增书签量降了一半而真正用到的比例反而上去了。最后再分享一个小习惯每个月月底花十分钟把首页数据过一遍把一个月没点过的条目从star里去掉。这个动作很轻但它能保证首页始终只留真正高频的东西不至于慢慢又退化成一个迷你版的书签堆。
企业数字化 ERP 产品动态
相关推荐
工厂线上推广素材低成本制作与优化策略 1. 工厂线上推广的素材困境与破局思路去年帮一家东莞的灯具代工厂做线上转型时,老板老陈给我看了他们的产品图——昏暗的厂房里,灯具摆在包装纸箱上拍摄,背景还能看到半截扫把。这种素材直接导致他们的1688店铺点击率不到行业平均值的1/3。这… · 2026/11/4 18:48:12
STM32CubeMX下载安装配置与代码生成链路自检指南 装个软件而已,能有多难?这是很多人第一次碰 STM32CubeMX 时的真实想法,也包括我自己。结果实际情况是:双击安装包没反应,装完打开一片空白,新建工程选完芯片却生成不了代码,固件包卡在某个百分比… · 2026/11/4 18:47:11
AI动画插件压缩中间帧工时:骨骼映射、曲线瘦身与脚部锁地实战 做动画的人都懂那种感觉:一个五秒的镜头,分镜定了、Layout 过了、关键姿势也摆完了,剩下那一百多帧的中间动作,全靠一帧一帧往时间轴上推。我见过不少团队,产能瓶颈不在创意,也不在渲染农场,就卡… · 2026/11/4 18:47:11
Hermes Agent架构解密:可扩展AI代理系统的设计模式与实现原理 Hermes Agent架构解密:可扩展AI代理系统的设计模式与实现原理
Hermes Agent是一款面向开发者和架构师设计的可扩展AI代理框架,它通过模块化架构实现了多平台集成、工具调用管理和上下文压缩等核心功能。该项目采用Python技术栈,支持OpenAI工… · 2026/11/4 19:24:44
Hermes Agent与自动驾驶仿真平台集成:CARLA与LGSVL Simulator完整指南 Hermes Agent与自动驾驶仿真平台集成:CARLA与LGSVL Simulator完整指南
Hermes Agent是一款功能强大的开源AI代理工具,能够自主执行各类任务并不断学习成长。本文将详细介绍如何将Hermes Agent与主流自动驾驶仿真平台CARLA和LGSVL Simulator进行集成&… · 2026/11/4 19:19:41
Hermes Agent与区块链智能合约测试框架:Truffle与Hardhat全解析 Hermes Agent与区块链智能合约测试框架:Truffle与Hardhat全解析
在区块链开发领域,智能合约的安全性与可靠性至关重要。Hermes Agent作为一款强大的AI驱动开发工具,能够与主流智能合约测试框架无缝集成,显著提升开发效率与代码质… · 2026/11/4 19:19:41
优化Hermes Agent的网络连接:TCP优化与连接复用实用指南 优化Hermes Agent的网络连接:TCP优化与连接复用实用指南
Hermes Agent作为一款强大的AI代理工具,其网络通信效率直接影响整体性能。本文将详细介绍如何通过TCP优化和连接复用技术提升Hermes Agent的网络连接稳定性与响应速度,帮助用户获得更… · 2026/11/4 19:19:41
Hermes Agent性能监控告警集成:Slack、Email与PagerDuty全攻略 Hermes Agent性能监控告警集成:Slack、Email与PagerDuty全攻略
Hermes Agent 是一款功能强大的开源智能代理框架,支持多平台集成与自动化任务处理。本文将详细介绍如何为 Hermes Agent 配置性能监控告警系统,实现 Slack 实时通知、Email 详细… · 2026/11/4 19:19:41
优化Hermes Agent的存储性能:RAID级别与存储协议选择 优化Hermes Agent的存储性能:RAID级别与存储协议选择
在处理大规模数据和高并发任务时,Hermes Agent的存储性能直接影响整体运行效率。本文将详细介绍如何通过选择合适的RAID级别和存储协议,显著提升Hermes Agent的存储吞吐量、数据可靠性和响… · 2026/11/4 19:19:41
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38