
本文介绍如何用 Puppeteer 高效提取学校课表中每节课的结构化数据(如课程名、时间、教室、教师、类型等),避免“整页抓取一团文本”的常见误区,通过精准选择器与 $$eval 批量解析实现健壮、可维护的爬虫逻辑。
本文介绍如何用 puppeteer 高效提取学校课表中每节课的结构化数据(如课程名、时间、教室、教师、类型等),避免“整页抓取一团文本”的常见误区,通过精准选择器与 `$$eval` 批量解析实现健壮、可维护的爬虫逻辑。
在自动化抓取动态课表(如 Visma Inschool)时,初学者常陷入一个典型陷阱:只选取最外层容器(如 .Timetable-TimetableDays_day),再调用 textContent 获取全部内容——结果得到一大段不可分割的纯文本,无法进一步提取课程名称、时间、教室等关键字段。正确做法是自底向上定位最小语义单元:直接锚定代表单节课的 DOM 元素(如 .Timetable-TimetableItem),再逐项提取其内部结构化子节点与属性。
根据提供的 HTML 结构,每个课程条目均包裹在具有 class="Timetable-TimetableItem" 的 <div> 中,且其子元素具备清晰的语义类名(如 <code>.Timetable-TimetableItem-hours、.Timetable-TimetableItem-location)。因此,推荐使用 page.$$eval() —— 它在浏览器上下文中批量执行 JavaScript,一次性获取所有匹配元素的数据,既高效又避免 ElementHandle 失效风险。
以下为生产就绪的示例代码(已适配实际 HTML 结构,注意类名拼写一致性):
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: 'new' });
const [page] = await browser.pages();
// ✅ 替换为真实 URL(需确保已登录且有权限)
// await page.goto('https://inschool.yourschool.no/timetable', {
// waitUntil: 'networkidle0'
// });
// ⚠️ 开发调试时可先加载本地 HTML 文件(如上文提供的 test.html)
// const html = await fs.readFile('test.html', 'utf8');
// await page.setContent(html);
// ? 等待课表项加载完成,提升稳定性
await page.waitForSelector('.Timetable-TimetableItem', { timeout: 10000 });
// ? 批量提取每节课结构化数据
const scheduleItems = await page.$$eval('.Timetable-TimetableItem', (els) =>
els.map((el) => {
// 辅助函数:安全查询子元素文本(防 null)
const getText = (selector) => {
const node = el.querySelector(selector);
return node ? node.textContent.trim() : '';
};
return {
// 从 aria-label 或 sr-only 标题中提取原始描述(可选)
description: el.previousElementSibling?.textContent?.trim() || '',
ariaLabel: el.getAttribute('aria-label') || '',
// 关键属性(来自 HTML 属性)
subjectCode: el.getAttribute('subjectcode') || null,
tttype: el.getAttribute('tttype') || 'UNKNOWN',
entityId: el.getAttribute('entityid') || null,
teachingGroupId: el.getAttribute('teachinggroupid') || null,
startTimeUnix: el.getAttribute('starttimeanddateunix') || null,
teacher: el.getAttribute('teachername') || 'N/A',
// 结构化子内容
hours: getText('.Timetable-TimetableItem-hours'),
location: getText('.Timetable-TimetableItem-location'),
subjectName: getText('.Timetable-TimetableItem-subject-name'),
lessonType: getText('.Timetable-TimetableItem-type'),
};
})
);
console.log(`✅ 成功提取 ${scheduleItems.length} 节课:`);
console.table(scheduleItems.map(({ subjectName, hours, location, lessonType }) => ({
subjectName,
hours,
location,
lessonType
})));
await browser.close();
})();关键实践建议:
-
选择器要稳健:避免
[class='active Timetable-TimetableDays_day']这类严格匹配——一旦 CSS 类顺序变动或新增类即失效。改用.active.Timetable-TimetableDays_day(多类并列)或更语义化的.Timetable-TimetableItem。 -
优先
$$eval/$eval:相比page.$()+getProperty()链式调用,$$eval在浏览器端执行,减少跨进程通信开销,且天然规避 ElementHandle 失效问题。 -
务必显式等待:在
$$eval前加入page.waitForSelector('.Timetable-TimetableItem'),防止因页面异步渲染未完成导致空数组。 -
防御性编程:对
querySelector结果做空值检查(如node?.textContent?.trim()),避免运行时错误中断流程。
最终输出为标准 JavaScript 对象数组,可直接用于生成信息屏、同步至数据库或导出为 JSON/CSV,真正实现从“网页快照”到“可用数据”的跨越。

















