使用Puppeteer爬取AtCoder竞赛时,进行中竞赛返回undefined错误
问题描述
我开发了一个爬取AtCoder竞赛测试用例的网络爬虫,处理已结束的竞赛时一切正常,但处理进行中的竞赛时会报错。错误出在访问HTML元素<table>的rows属性时——我确认该table元素应该存在,但脚本处理进行中的竞赛时返回undefined。
错误信息
Error: Evaluation failed: TypeError: Cannot read properties of undefined (reading 'rows') at pptr://__puppeteer_evaluation_script__:3:32 at ExecutionContext._ExecutionContext_evaluate (/mnt/d/c++/node_modules/puppeteer-core/lib/cjs/puppeteer/common/ExecutionContext.js:229:15) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async ExecutionContext.evaluate (/mnt/d/c++/node_modules/puppeteer-core/lib/cjs/puppeteer/common/ExecutionContext.js:107:16) at async scrapeSite (/mnt/d/c++/codeforces/atcoder.js:57:33)
爬虫代码(atcoder.js)
const puppeteer = require("puppeteer"); const fs = require("fs"); const contest_id = process.argv[2]; async function scrapeProblem(problem_letter) { const url = `https://atcoder.jp/contests/${contest_id}/tasks/${contest_id}_${problem_letter.toLowerCase()}`; console.log(url); try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle0" }); const samples_scraped = await page.evaluate(() => { const samples = document.querySelectorAll("pre"); const scraped = Array.from(samples).filter((child) => { return child.id !== ""; }); let num_scraped = scraped.length; // 元素重复了两次,移除多余的部分 for (let i = 0; i < num_scraped / 2; i++) scraped.pop(); return scraped.map((ele) => ele.innerText); }); let id = 1; // 将测试用例保存为文本文件 samples_scraped.map((ele, idx) => { if (idx % 2 == 0) { // 输入用例 fs.writeFile(`${problem_letter}-${id}.in`, ele, (err) => { if (err) throw err; }); } else { // 输出用例 fs.writeFile(`${problem_letter}-${id}.out`, ele, (err) => { if (err) throw err; }); id++; } return ele; }); await browser.close(); } catch (e) { console.log(e); } } async function scrapeSite() { const url = `https://atcoder.jp/contests/${contest_id}/tasks`; console.log(url); try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: "networkidle0" }); // 获取所有问题字母 const problem_letters = await page.evaluate(() => { const table = document.querySelectorAll("table")[0]; const rows = table.rows.length; const letters = []; for (let i = 1; i < rows; i++) { letters.push(table.rows[i].cells[0].innerText); } return letters; }); console.log(problem_letters); for (problem_letter of problem_letters) { scrapeProblem(problem_letter); } await browser.close(); } catch (e) { console.log(e); } } scrapeSite();
代码说明
scrapeProblem(problem_letter):辅助函数,负责爬取指定问题字母对应的测试用例,通过fs模块保存到本地文件系统。scrapeSite():先访问竞赛任务主页,解析页面获取所有问题的字母标识,再循环调用scrapeProblem处理每个问题。
运行命令
node scrapper.js abc280
后续排查发现
测试新的进行中竞赛时仍出现相同错误,通过Puppeteer截图确认:未登录状态下访问进行中的竞赛会被拒绝权限,页面显示权限不足的提示。
解决方案
1. 添加登录逻辑,绕过权限限制
AtCoder对进行中的竞赛设置了访问权限,未登录用户无法查看任务页面。需要在爬虫中加入登录步骤:
async function scrapeSite() { const url = `https://atcoder.jp/contests/${contest_id}/tasks`; const loginUrl = "https://atcoder.jp/login"; console.log(url); try { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 执行登录操作 await page.goto(loginUrl, { waitUntil: "networkidle0" }); await page.type("#username", "你的AtCoder账号"); await page.type("#password", "你的AtCoder密码"); await page.click('input[type="submit"]'); await page.waitForNavigation({ waitUntil: "networkidle0" }); // 等待登录跳转完成 // 访问竞赛任务页 await page.goto(url, { waitUntil: "networkidle0" }); // 获取问题字母前先检查表格是否存在 const problem_letters = await page.evaluate(() => { const table = document.querySelectorAll("table")[0]; if (!table) return []; // 权限不足时返回空数组,避免报错 const rows = table.rows.length; const letters = []; for (let i = 1; i < rows; i++) { letters.push(table.rows[i].cells[0].innerText); } return letters; }); console.log(problem_letters); // 用Promise.all等待所有爬取任务完成,避免提前关闭浏览器 await Promise.all(problem_letters.map(problem_letter => scrapeProblem(problem_letter))); await browser.close(); } catch (e) { console.log(e); } }
2. 修复并发爬取的问题
原代码循环调用scrapeProblem时未使用await,会导致多个浏览器实例同时运行,且主浏览器可能在子任务完成前就关闭。改用Promise.all等待所有爬取任务结束后再关闭浏览器。
3. 优化错误边界处理
在page.evaluate中先检查表格是否存在,避免直接访问rows属性导致undefined报错。
4. 安全提示
不要硬编码账号密码,建议通过环境变量传入:
const username = process.env.ATCODER_USERNAME; const password = process.env.ATCODER_PASSWORD;
运行时传入环境变量:
ATCODER_USERNAME=你的账号 ATCODER_PASSWORD=你的密码 node scrapper.js abc280
内容的提问来源于stack exchange,提问作者BeruboIV
相关产品推荐
相关产品推荐

