市面上的 SEO 审计工具不少,但当你需要针对特定业务逻辑做定制化检查——比如验证某个 SPA 站点的预渲染输出、监控竞品站点的标题变更、或者批量检查上千个 URL 的 canonical 标签——现成工具往往力不从心。自己动手写一个 SEO 爬虫,反而更灵活、更可控。
本文将从零开始,用 Node.js 搭建一个可用的 SEO 爬虫,覆盖抓取调度、页面解析、问题检测和结构化报告四个核心环节。
为什么选择 Node.js
Node.js 在爬虫场景中有几个天然优势:事件驱动的非阻塞 I/O 模型非常适合大量 HTTP 请求的并发处理;npm 生态中有 cheerio、undici、p-limit 等成熟工具;JavaScript 本身就是处理 DOM 和字符串的顺手语言。对于中小规模的 SEO 审计(几千到几万页面),Node.js 完全够用。
整体架构设计
一个实用的 SEO 爬虫可以拆成四个模块:
- 调度器(Scheduler):管理待抓取队列,控制并发数,处理重试和去重
- 抓取器(Fetcher):发起 HTTP 请求,获取页面 HTML 和响应头
- 解析器(Parser):从 HTML 中提取 SEO 相关元素
- 报告器(Reporter):将检测结果聚合成结构化的问题清单
数据流很清晰:调度器从队列取出 URL → 抓取器请求页面 → 解析器提取 SEO 数据 → 检测规则判断是否有问题 → 结果写入报告器,同时将新发现的链接推回调度器。
抓取调度:队列、并发与去重
调度的核心是控制好三件事:并发数、去重和重试。
import PQueue from 'p-queue';
const queue = new PQueue({ concurrency: 5 });
const visited = new Set();
const results = [];
function enqueue(url) {
const normalized = normalizeUrl(url);
if (visited.has(normalized)) return;
visited.add(normalized);
queue.add(() => crawlPage(normalized));
}
并发数建议控制在 3–10 之间。太高容易触发目标站点的限流,太低则爬取效率不够。p-queue 可以方便地设置并发上限,还能通过 queue.onIdle() 在队列清空时触发报告生成。
URL 去重时需要先做归一化处理:去掉 fragment(# 后面的部分)、统一大小写、移除默认端口、排序查询参数。否则 https://example.com/page 和 https://example.com/page? 会被当成两个不同的页面重复抓取。
对于需要登录或有反爬机制的站点,可以在请求头中携带自定义 User-Agent,并设置合理的请求间隔:
import { request } from 'undici';
async function fetchPage(url) {
const res = await request(url, {
headers: {
'User-Agent': 'MySEOBot/1.0 (+https://example.com/bot)',
'Accept': 'text/html,application/xhtml+xml',
},
maxRedirections: 5,
});
const html = await res.body.text();
return { status: res.statusCode, headers: res.headers, html };
}
解析页面:提取关键 SEO 元素
拿到 HTML 后,用 cheerio 解析并提取以下元素:
<title>内容及长度<meta name="description">内容及长度<link rel="canonical">的 href<meta name="robots">指令<h1>到<h3>的层级结构- 所有
<a href>链接(用于继续爬取) - 图片的
alt属性 - 结构化数据(JSON-LD)
- Open Graph 和 Twitter Card 标签
import * as cheerio from 'cheerio';
function parsePage(html, url) {
const $ = cheerio.load(html);
return {
url,
title: $('title').first().text().trim(),
metaDescription: $('meta[name="description"]').attr('content')?.trim() || '',
canonical: $('link[rel="canonical"]').attr('href') || '',
robots: $('meta[name="robots"]').attr('content') || '',
h1: $('h1').map((_, el) => $(el).text().trim()).get(),
links: $('a[href]').map((_, el) => $(el).attr('href')).get(),
images: $('img').map((_, el) => ({
src: $(el).attr('src'),
alt: $(el).attr('alt'),
})).get(),
jsonLd: $('script[type="application/ld+json"]').map((_, el) => $(el).html()).get(),
};
}
问题检测:定义你的规则集
这是整个爬虫最有价值的部分——把 SEO 最佳实践转化为可自动检测的规则。以下是一些高价值的检测项:
标题相关
- 标题缺失或为空
- 标题长度超过 60 个字符(可能在 SERP 中被截断)
- 标题在多个页面间重复
Meta Description
- 描述缺失
- 长度超过 160 个字符
- 多页面描述重复
Canonical 标签
- canonical 指向的 URL 与当前页面不一致(需人工确认是否为有意为之)
- canonical 指向 404 页面
- 页面缺少 canonical 标签
内容结构
- 页面没有 H1
- 存在多个 H1
- H1 内容与 title 完全一致(可能浪费了优化机会)
链接与图片
- 内链指向 404
- 图片缺少 alt 属性
- 存在 nofollow 内链(通常不推荐)
function checkPage(page) {
const issues = [];
if (!page.title) {
issues.push({ type: 'error', rule: 'missing-title', url: page.url });
} else if (page.title.length > 60) {
issues.push({
type: 'warning',
rule: 'title-too-long',
url: page.url,
detail: `标题长度 ${page.title.length} 字符`,
});
}
if (!page.metaDescription) {
issues.push({ type: 'warning', rule: 'missing-description', url: page.url });
}
if (page.h1.length === 0) {
issues.push({ type: 'error', rule: 'missing-h1', url: page.url });
} else if (page.h1.length > 1) {
issues.push({
type: 'warning',
rule: 'multiple-h1',
url: page.url,
detail: `发现 ${page.h1.length} 个 H1`,
});
}
const noAltImages = page.images.filter(img => !img.alt);
if (noAltImages.length > 0) {
issues.push({
type: 'warning',
rule: 'images-missing-alt',
url: page.url,
detail: `${noAltImages.length} 张图片缺少 alt`,
});
}
return issues;
}
结构化报告:按严重程度聚合
报告的输出格式直接影响可用性。推荐按 严重程度 和 规则类型 两个维度聚合,输出 JSON 和 HTML 两种格式。
function buildReport(allIssues, totalPages) {
const bySeverity = { error: [], warning: [], notice: [] };
const byRule = {};
for (const issue of allIssues) {
bySeverity[issue.type]?.push(issue);
byRule[issue.rule] = (byRule[issue.rule] || 0) + 1;
}
return {
summary: {
totalPages,
totalIssues: allIssues.length,
errors: bySeverity.error.length,
warnings: bySeverity.warning.length,
notices: bySeverity.notice.length,
},
byRule,
details: bySeverity,
};
}
最终生成的报告可以输出为:
- JSON:方便接入 CI/CD 流水线,做自动化门禁
- HTML:给非技术团队成员查看的可视化报告
- CSV:方便在 Excel 中进一步筛选和排序
实战建议
控制爬取深度和范围。通过 robots.txt 和 sitemap.xml 确定爬取边界,避免爬到无关页面。设置最大深度限制(比如 3 层)通常足够发现大部分问题。
处理 JavaScript 渲染。对于 SPA 站点,undici 拿到的 HTML 可能是空壳。这时需要引入 Puppeteer 做渲染后再解析,但性能开销会显著增加。建议先用静态抓取跑一遍,对关键页面再用 Puppeteer 补充。
增量爬取。记录上次爬取的 URL 和内容哈希,只对内容有变化的页面重新检测,可以大幅减少重复工作量。
结果持久化。把每次爬取的结果存入 SQLite 或 JSON 文件,方便对比不同时间点的变化趋势——比如追踪某个页面的标题是否被修改过。
结语
用 Node.js 构建 SEO 爬虫并不复杂,核心在于把「抓取—解析—检测—报告」这条链路设计清晰。一旦框架搭好,后续增加新的检测规则只需要写一个函数。相比依赖第三方工具,自建爬虫的最大价值在于:你可以根据业务需求定义检测规则,把 SEO 审计真正嵌入到开发和运维流程中。
未经允许不得转载:任鹏个人博客 » 用 Node.js 构建自定义 SEO 爬虫:从抓取调度到结构化问题报告


朋友圈点赞图在线生成源码