递归爬虫回调引用疑问:为何特定回调可终止递归?
递归爬虫回调函数的疑问解析
问题背景
我写了一段可递归下载网站所有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); }
问题原因解析
先理清楚整个爬虫的回调链逻辑:
spiderNested是核心入口,负责读取本地文件或下载页面,之后调用spiderLinks处理页面内的链接。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的记录,避免重复处理。修改代码如下:
- 新增一个全局的已爬取集合:
const crawledUrls = new Set();
- 修改
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
相关产品推荐
相关产品推荐

