用 Node.js 构建自定义 SEO 爬虫:从抓取调度到结构化问题报告

市面上的 SEO 审计工具不少,但当你需要针对特定业务逻辑做定制化检查——比如验证某个 SPA 站点的预渲染输出、监控竞品站点的标题变更、或者批量检查上千个 URL 的 canonical 标签——现成工具往往力不从心。自己动手写一个 SEO 爬虫,反而更灵活、更可控。

本文将从零开始,用 Node.js 搭建一个可用的 SEO 爬虫,覆盖抓取调度、页面解析、问题检测和结构化报告四个核心环节。

为什么选择 Node.js

Node.js 在爬虫场景中有几个天然优势:事件驱动的非阻塞 I/O 模型非常适合大量 HTTP 请求的并发处理;npm 生态中有 cheerioundicip-limit 等成熟工具;JavaScript 本身就是处理 DOM 和字符串的顺手语言。对于中小规模的 SEO 审计(几千到几万页面),Node.js 完全够用。

整体架构设计

一个实用的 SEO 爬虫可以拆成四个模块:

  1. 调度器(Scheduler):管理待抓取队列,控制并发数,处理重试和去重
  2. 抓取器(Fetcher):发起 HTTP 请求,获取页面 HTML 和响应头
  3. 解析器(Parser):从 HTML 中提取 SEO 相关元素
  4. 报告器(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/pagehttps://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.txtsitemap.xml 确定爬取边界,避免爬到无关页面。设置最大深度限制(比如 3 层)通常足够发现大部分问题。

处理 JavaScript 渲染。对于 SPA 站点,undici 拿到的 HTML 可能是空壳。这时需要引入 Puppeteer 做渲染后再解析,但性能开销会显著增加。建议先用静态抓取跑一遍,对关键页面再用 Puppeteer 补充。

增量爬取。记录上次爬取的 URL 和内容哈希,只对内容有变化的页面重新检测,可以大幅减少重复工作量。

结果持久化。把每次爬取的结果存入 SQLite 或 JSON 文件,方便对比不同时间点的变化趋势——比如追踪某个页面的标题是否被修改过。

结语

用 Node.js 构建 SEO 爬虫并不复杂,核心在于把「抓取—解析—检测—报告」这条链路设计清晰。一旦框架搭好,后续增加新的检测规则只需要写一个函数。相比依赖第三方工具,自建爬虫的最大价值在于:你可以根据业务需求定义检测规则,把 SEO 审计真正嵌入到开发和运维流程中。

未经允许不得转载:任鹏个人博客 » 用 Node.js 构建自定义 SEO 爬虫:从抓取调度到结构化问题报告

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏