
1. 从零拆解《文心雕龙》抓取任务Node.js 工程化落盘到底难在哪《文心雕龙》全文抓取说白了就是把一个古籍目录页里的章节链接全部找出来逐个请求详情页再把正文抽出来存成结构化 JSON。听起来像是个下午就能搞定的练手项目但真正动手你会发现坑比想象中多目录页的链接是相对路径还是绝对路径、详情页正文容器的 class 名会不会变、请求频率高了会不会被限流、章节顺序怎么保证不乱、落盘时中文编码怎么处理——这些问题在 Node.js 抓取场景里一个都躲不掉。我这次的目标很明确用 Node.js 写一套可复现的抓取脚本把《文心雕龙》五十篇的标题、正文、来源链接完整抓下来最终输出一份干净的 JSON 文件方便后续做文本分析或者喂给大模型做语义检索。整个链路涉及请求头发送、分页/目录遍历、HTML 解析、章节切分、JSON 落盘五个环节每个环节我都会给出可直接复制的代码。适合谁看如果你已经会写基本的 Node.js 脚本但对 HTTP 请求头、DOM 解析、异步并发控制这些细节还不够熟这篇就是为你准备的。如果你完全没接触过 Node.js建议先补一下npm init和node 文件名.js的基本用法否则后面跟起来会有点吃力。另外抓取过程中会涉及请求凭证的管理。我一开始是把 User-Agent 和 Referer 硬编码在脚本里后来发现多个抓取任务共用一套请求配置时很容易乱。这次我改用 TaoToken 来统一管理 API 通道和请求凭证把抓取链路里的鉴权部分抽出来脚本本身只负责业务逻辑。这样后面扩展抓别的古籍时改配置就行不用动代码。下面从环境准备开始一步步把整条链路搭起来。2. TaoToken 前置准备统一 Key 与 API 通道管理在正式写抓取脚本之前先把请求凭证这块理清楚。很多教程会直接让你在代码里写死User-Agent和Referer短期跑一次没问题但如果你要抓多个站点、或者要把脚本分享给别人跑硬编码就会变成维护噩梦。更麻烦的是有些站点会对同一 IP 的高频请求做限制这时候你需要一个统一的出口来管理请求头、超时、重试策略。TaoToken 在这里的角色是「统一 API 通道 凭证管理」。你可以把它理解成一个中间层脚本不直接暴露目标站点的请求细节而是通过 TaoToken 配置好的通道发请求Key 和 Base URL 都在控制台里管理。这样做的直接好处是抓取脚本里不需要出现任何敏感凭证换环境时只改配置文件。具体操作路径如下。先打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册账号然后进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建一个 API Key。创建时注意选择「抓取/通用」类型的 Key这类 Key 的权限范围适合外部 HTTP 请求场景不会误触其他服务。拿到 Key 之后你需要记下三个东西Base URL、API Key、以及你要调用的模型或通道 ID。Base URL 统一用 https://taotoken.net/api不要加任何 UTM 参数这是 API 调用的规范地址。API Key 是一串以sk-开头的字符串复制后先存到本地环境变量里别直接写进代码。如果你用的是 Claude Code 或者 Cline 这类工具来做辅助开发可以在它们的配置里填入 TaoToken 的 Base URL 和 Key。比如 Claude Code 的 settings 文件里把ANTHROPIC_BASE_URL指向 TaoToken 的 API 地址ANTHROPIC_API_KEY填你刚创建的 Key。这样你在写抓取脚本时如果需要让模型帮你分析 HTML 结构或者生成解析规则可以直接走 TaoToken 的通道不用额外配一套凭证。对于纯 Node.js 脚本场景我建议把凭证放在.env文件里用dotenv加载。这样脚本里只写process.env.TAOTOKEN_API_KEY既安全又方便切换环境。下面第三节的配置片段会给出完整的.env和settings.json示例。有一点要注意TaoToken 的 API 通道是给你管理请求凭证用的不是让你绕过目标站点的访问限制。抓取时该加的User-Agent、该控制的请求频率一个都不能少。TaoToken 解决的是「凭证统一管理」的问题不是「无限并发」的问题。这点想清楚后面的脚本才不会跑偏。3. 可复制配置.env、settings.json 与抓取脚本骨架这一节直接给可复制的配置和代码。先建项目目录然后依次创建文件。3.1 项目初始化与依赖清单mkdir wenxin-diaolong-crawler cd wenxin-diaolong-crawler npm init -y npm install axios cheerio dotenv p-limit依赖说明axios负责 HTTP 请求比原生http模块好用cheerio做 HTML 解析语法跟 jQuery 几乎一样dotenv加载环境变量p-limit控制并发数避免请求过猛。3.2 .env 文件凭证与通道配置# .env TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYsk-你的Key替换这里 TARGET_BASE_URLhttp://www.gushiwen.org REQUEST_DELAY_MS800 MAX_CONCURRENCY3这里TARGET_BASE_URL是目标站点根地址REQUEST_DELAY_MS控制每次请求间隔MAX_CONCURRENCY限制并发数。这三个参数后面在脚本里会用到。3.3 settings.json如果你用 Claude Code 辅助开发{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key替换这里, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }这个文件放在项目根目录Claude Code 启动时会自动读取。注意ANTHROPIC_MODEL填你在 TaoToken 控制台里看到的模型 ID不同账号可能略有差异以控制台显示为准。3.4 抓取脚本骨架 crawler.js// crawler.js require(dotenv).config(); const axios require(axios); const cheerio require(cheerio); const fs require(fs); const path require(path); const pLimit require(p-limit); const BASE process.env.TARGET_BASE_URL; const DELAY parseInt(process.env.REQUEST_DELAY_MS || 800, 10); const limit pLimit(parseInt(process.env.MAX_CONCURRENCY || 3, 10)); const HEADERS { User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: BASE, Accept-Language: zh-CN,zh;q0.9, }; function sleep(ms) { return new Promise((resolve) setTimeout(resolve, ms)); } async function fetchHtml(url) { await sleep(DELAY); const res await axios.get(url, { headers: HEADERS, timeout: 15000 }); return res.data; } async function getChapterList() { const html await fetchHtml(${BASE}/guwen/wenxin.aspx); const $ cheerio.load(html); const chapters []; $(.bookcont a).each((i, el) { const href $(el).attr(href); const title $(el).text().trim(); if (!href || !title) return; const fullUrl href.startsWith(http) ? href : ${BASE}${href}; chapters.push({ title, url: fullUrl, order: i 1 }); }); return chapters; } async function getChapterContent(chapter) { const html await fetchHtml(chapter.url); const $ cheerio.load(html); const content $(.contson).text().trim(); return { ...chapter, content }; } async function main() { console.log(开始抓取目录...); const chapters await getChapterList(); console.log(共发现 ${chapters.length} 个章节); const tasks chapters.map((ch) limit(() getChapterContent(ch)) ); const results await Promise.all(tasks); const output { book: 文心雕龙, author: 刘勰, dynasty: 南朝, crawledAt: new Date().toISOString(), totalChapters: results.length, chapters: results, }; const outPath path.join(__dirname, wenxin_diaolong.json); fs.writeFileSync(outPath, JSON.stringify(output, null, 2), utf-8); console.log(抓取完成已写入 ${outPath}); } main().catch((err) { console.error(抓取失败:, err.message); process.exit(1); });这份脚本把目录抓取、详情抓取、并发控制、JSON 落盘串成了一条线。p-limit保证同时最多 3 个请求在跑sleep在每次请求前插入 800ms 间隔避免触发目标站点的频率限制。cheerio的选择器.bookcont a和.contson是根据目标页面结构写的如果页面改版这两个选择器需要相应调整。跑之前确认.env里的TAOTOKEN_API_KEY已经填好。虽然这个脚本本身不直接调 TaoToken 的 API但如果你后续要加「抓取后自动摘要」或者「正文清洗」的步骤就可以在同一个项目里通过 TaoToken 的通道调模型不用再配一套凭证。4. 验证请求与成功结果本地跑通并检查 JSON 结构配置写完之后直接运行node crawler.js正常情况你会看到类似输出开始抓取目录... 共发现 50 个章节 抓取完成已写入 /path/to/wenxin-diaolong-crawler/wenxin_diaolong.json打开生成的wenxin_diaolong.json结构应该是这样的{ book: 文心雕龙, author: 刘勰, dynasty: 南朝, crawledAt: 2025-01-15T08:30:00.000Z, totalChapters: 50, chapters: [ { title: 原道第一, url: http://www.gushiwen.org/guwen/wenxin_1.aspx, order: 1, content: 文之为德也大矣与天地并生者何哉... } ] }检查三个关键点第一totalChapters是否等于 50如果少于 50说明目录页有分页或者选择器漏抓了第二content字段是否为空字符串如果为空说明详情页的正文容器 class 名跟.contson不一致第三title是否包含「第」字有些站点的标题会带序号有些不会按需清洗。如果一切正常你可以用下面这段代码快速验证 JSON 的完整性const data require(./wenxin_diaolong.json); const empty data.chapters.filter((ch) !ch.content || ch.content.length 10); console.log(总章节: ${data.totalChapters}); console.log(空内容章节: ${empty.length}); if (empty.length 0) { console.log(空内容章节列表:, empty.map((ch) ch.title)); }实测下来最常见的「空内容」原因是详情页的正文不在.contson里而是在.contson的某个子元素里。这时候把选择器改成.contson p或者.contson的父级容器再试一次就行。另外如果你在请求过程中看到429 Too Many Requests说明并发数或请求间隔需要调大。把.env里的MAX_CONCURRENCY降到 1REQUEST_DELAY_MS提到 1500再跑一次。抓取这件事慢就是快别跟目标站点的限流机制硬碰。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth抓取脚本跑不起来报错信息往往很模糊。这一节把几个高频错误和对应的排查路径列出来你对照着看。5.1 401 Unauthorized如果你在脚本里加了 TaoToken 的 API 调用比如抓取后自动摘要报 401 通常意味着 Key 没传对。检查.env里的TAOTOKEN_API_KEY是否以sk-开头有没有多余空格。另外确认请求头里是Authorization: Bearer sk-xxx的格式不是x-api-key。TaoToken 的 API 通道用的是 Bearer Token 认证这点跟某些平台不一样。5.2 local proxy failed这个报错一般出现在你本地配了代理工具的情况下。Node.js 的axios默认会读取系统代理设置如果代理工具没开或者端口不对就会报local proxy failed。解决办法是在axios请求里显式禁用代理const res await axios.get(url, { headers: HEADERS, timeout: 15000, proxy: false, });加上proxy: false之后请求会直连目标地址不再走系统代理。如果你确实需要通过代理发请求那就把代理地址配在.env里用httpsAgent传给 axios而不是依赖系统全局代理。5.3 reading choices 报错这个错误通常出现在你调模型接口时返回体里没有choices字段。原因可能是模型 ID 填错了或者请求体格式不对。检查你的请求 JSON 里model字段是否跟 TaoToken 控制台里显示的模型 ID 完全一致大小写都不能差。另外确认messages数组的格式是[{ role: user, content: ... }]不是字符串。5.4 OAuth 相关报错如果你用 Claude Code 或者 Cline 这类工具启动时报 OAuth 错误大概率是settings.json里的ANTHROPIC_BASE_URL没配对。确认地址是https://taotoken.net/api末尾不要加斜杠。另外ANTHROPIC_API_KEY要填 TaoToken 的 Key不是 Anthropic 官方的 Key。这两个 Key 格式不同混用会直接报 OAuth 失败。5.5 三件套检查清单不管报什么错先检查这三样Base URL、Key、Model ID。Base URL 用https://taotoken.net/apiKey 用 TaoToken 控制台创建的sk-开头的字符串Model ID 以控制台显示为准。这三样对齐了大部分鉴权类报错都会消失。如果排查完还是跑不通去 TaoToken 的接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 对照一下请求示例或者直接到 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一个 Key 试试。有时候是 Key 复制时漏了字符重新生成最快。6. 语义一致 CTA把抓取链路接到 TaoToken 通道上抓取脚本跑通之后下一步通常是「抓下来的文本怎么用」。如果你只是存成 JSON 放着那确实不需要 TaoToken。但如果你想让抓取链路更完整——比如抓完自动做章节摘要、关键词提取、或者把正文转成向量存进本地库——那就需要调模型接口。这时候 TaoToken 的价值就体现出来了同一个 Key 管所有模型调用不用在抓取脚本里再维护一套鉴权逻辑。具体怎么接在crawler.js的main函数末尾加一段后处理逻辑把results里的每章正文通过 TaoToken 的模型对话接口做摘要。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 你可以先在那里试一下请求格式确认返回正常再写进脚本。如果你打算长期做古籍抓取和文本处理建议直接上 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 把抓取、清洗、摘要、存储整条链路都挂在同一个通道下。这样后面加新书的时候只需要改目标 URL 和选择器凭证和通道配置完全复用。最后提醒一句抓取频率控制、robots.txt 遵守、目标站点版权声明这些是脚本之外的事但同样重要。TaoToken 帮你管的是请求凭证和通道不帮你绕过访问限制。把这两件事分清楚你的抓取链路才能跑得稳、跑得久。