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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 17:25:26