You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

递归爬虫回调引用疑问:为何特定回调可终止递归?

递归爬虫回调函数的疑问解析

问题背景

我写了一段可递归下载网站所有HTML页面的爬虫,核心逻辑在spiderLinks函数的iterate递归遍历环节。我发现一个奇怪的现象:

  • 使用回调A时,递归能正常退出:
(err) => {
  if (err) {
    console.error(err);
  }

  console.log("All files downloaded");
}
  • 而使用我原本认为正确的回调B时,会引发无限递归:
(err) => {
  if (err) {
    return callback(err);
  }
  iterate(index + 1);
}

问题原因解析

先理清楚整个爬虫的回调链逻辑:

  1. spiderNested是核心入口,负责读取本地文件或下载页面,之后调用spiderLinks处理页面内的链接。
  2. spiderLinks的作用是遍历当前页面的所有同域名链接,逐个调用spiderNested处理(嵌套层级减1),全部处理完后调用传入的callback向上层报告完成。

为什么回调A能“正常退出”?

当把spiderNested的回调换成回调A时,每个链接处理完成后,不会触发下一个链接的遍历(省略了iterate(index+1)),相当于只处理第一个链接就终止了流程。这种“正常退出”其实是功能不完整的——它并没有真正爬完所有链接,自然不会触发后续的循环问题。

为什么回调B会引发无限递归?

回调B本身是正确的遍历逻辑,但你的爬虫缺少URL去重机制:如果页面A包含页面B的链接,页面B又包含页面A的链接,当nesting层级足够时,爬虫会反复下载A→B→A→B...相同的URL被不断加入遍历队列,最终形成无限递归。

你看到的“回调A正常退出”只是因为它提前终止了遍历,没触发后续的循环引用而已。

修复方案:添加URL去重

要解决无限递归问题,只需给爬虫加上已爬取URL的记录,避免重复处理。修改代码如下:

  1. 新增一个全局的已爬取集合:
const crawledUrls = new Set();
  1. 修改spiderNested函数,先检查URL是否已爬取:
function spiderNested(url, nesting, callback) {
  if (crawledUrls.has(url)) {
    return process.nextTick(callback);
  }
  crawledUrls.add(url);
  
  // 后续原有逻辑不变...
}

这样就能避免重复爬取相同URL,回调B的遍历逻辑也能正常工作,爬虫会在所有符合层级要求的页面都爬取完成后正常退出。

完整修复后的爬虫代码

const request = require("request");
const fs = require("fs");
const path = require("path");
const cheerio = require("cheerio");
const crawledUrls = new Set(); // 新增URL去重集合

function urlToFilename(url) {
  const parsedUrl = new URL(url);
  const hostname = parsedUrl.hostname;
  const pathname = parsedUrl.pathname;

  return pathname === "/"
    ? `${hostname.slice(0, hostname.lastIndexOf("."))}/${hostname.slice(
        0,
        hostname.lastIndexOf(".")
      )}.html`
    : `${hostname.slice(0, hostname.lastIndexOf("."))}/${pathname.slice(
        0,
        pathname.lastIndexOf(".")
      )}.html`;
}

function getPageLinks(url, body) {
  const $ = cheerio.load(body);
  const linkObjects = $("a");
  const links = [];
  const urlObj = new URL(url);
  linkObjects.each((i, elem) => {
    const link = $(elem).attr("href");

    try {
      const linkObj = new URL(link);
      if (linkObj.hostname === urlObj.hostname) {
        links.push(link);
      }
    } catch (err) {
      // console.error(err);
    }
  });

  return links;
}

function saveFile(filename, contents, callback) {
  fs.mkdir(path.dirname(filename), { recursive: true }, (err) => {
    if (err) {
      return callback(err);
    }

    fs.writeFile(filename, contents, callback);
  });
}

function download(url, filename, callback) {
  console.log(`Downloading ${url}`);

  request(url, (err, response, body) => {
    if (err) {
      return callback(err);
    }

    saveFile(filename, body, (err) => {
      if (err) {
        return callback(err);
      }

      console.log(`Downloaded and saved: ${url}`);
      callback(null, body);
    });
  });
}

function spiderLinks(currentUrl, body, nesting, callback) {
  if (nesting === 0) {
    return process.nextTick(callback);
  }
  const links = getPageLinks(currentUrl, body);
  function iterate(index) {
    if (index === links.length) {
      return callback();
    }

    spiderNested(links[index], nesting - 1, (err) => {
      if (err) {
        return callback(err);
      }
      iterate(index + 1);
    });
  }
  iterate(0);
}

function spiderNested(url, nesting, callback) {
  if (crawledUrls.has(url)) {
    return process.nextTick(callback);
  }
  crawledUrls.add(url);
  
  const filename = urlToFilename(url);
  fs.readFile(filename, "utf-8", (err, body) => {
    if (err) {
      if (err.code !== "ENOENT") {
        return callback(err);
      }

      return download(url, filename, (err, body) => {
        if (err) {
          return callback(err);
        }

        spiderLinks(url, body, nesting, callback);
      });
    }

    spiderLinks(url, body, nesting, callback);
  });
}

spiderNested(process.argv[2], 1, (err) => {
  if (err) {
    console.error(err);
  }

  console.log("All files downloaded");
});

内容的提问来源于stack exchange,提问作者Danko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:17:54