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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册账号,然后进控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 创建一个 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_URL=https://taotoken.net/api TAOTOKEN_API_KEY=sk-你的Key替换这里 TARGET_BASE_URL=http://www.gushiwen.org REQUEST_DELAY_MS=800 MAX_CONCURRENCY=3这里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;q=0.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降到 1,REQUEST_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/api;Key 用 TaoToken 控制台创建的sk-开头的字符串;Model ID 以控制台显示为准。这三样对齐了,大部分鉴权类报错都会消失。
如果排查完还是跑不通,去 TaoToken 的接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 对照一下请求示例,或者直接到 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个 Key 试试。有时候是 Key 复制时漏了字符,重新生成最快。
6. 语义一致 CTA:把抓取链路接到 TaoToken 通道上
抓取脚本跑通之后,下一步通常是「抓下来的文本怎么用」。如果你只是存成 JSON 放着,那确实不需要 TaoToken。但如果你想让抓取链路更完整——比如抓完自动做章节摘要、关键词提取、或者把正文转成向量存进本地库——那就需要调模型接口。这时候 TaoToken 的价值就体现出来了:同一个 Key 管所有模型调用,不用在抓取脚本里再维护一套鉴权逻辑。
具体怎么接?在crawler.js的main函数末尾加一段后处理逻辑,把results里的每章正文通过 TaoToken 的模型对话接口做摘要。模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,你可以先在那里试一下请求格式,确认返回正常再写进脚本。
如果你打算长期做古籍抓取和文本处理,建议直接上 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,把抓取、清洗、摘要、存储整条链路都挂在同一个通道下。这样后面加新书的时候,只需要改目标 URL 和选择器,凭证和通道配置完全复用。
最后提醒一句:抓取频率控制、robots.txt 遵守、目标站点版权声明,这些是脚本之外的事,但同样重要。TaoToken 帮你管的是请求凭证和通道,不帮你绕过访问限制。把这两件事分清楚,你的抓取链路才能跑得稳、跑得久。