
本文介绍如何利用 Puppeteer 的 $$eval 方法,从结构化课表 DOM 中批量抓取每节课的课程名、时间、教室、教师、类型等字段,并避免常见选择器陷阱与性能问题。
本文介绍如何利用 puppeteer 的 `$$eval` 方法,从结构化课表 dom 中批量抓取每节课的课程名、时间、教室、教师、类型等字段,并避免常见选择器陷阱与性能问题。
在爬取学校课表(如 Visma Inschool)这类动态渲染的 Web 应用时,关键不在于“获取整个日历区块”,而在于精准定位每一节课的独立容器节点,并结构化提取其多维度属性与文本内容。原始代码中使用 page.$("[class='active Timetable-TimetableDays_day']") 获取顶层 div 后仅调用 textContent,导致所有信息混为一串不可解析的纯文本——这正是结构化数据提取失败的典型原因。
✅ 正确策略:以 .Timetable-TimetableItem 为单位批量解析
观察 HTML 片段可知,每节课均包裹在具有 class="Timetable-TimetableItem" 的 <div> 中,且该元素携带丰富语义化属性(如 subjectcode、teachername、starttimeanddateunix)和嵌套结构(如 .Timetable-TimetableItem-hours、.Timetable-TimetableItem-location)。因此,应直接定位所有课表项,再逐个提取:
const data = await page.$$eval(
".Timetable-TimetableItem", // ✅ 稳健选择器:类名组合,不依赖顺序或冗余类
(els) => els.map((el) => {
const getText = (selector) => {
const node = el.querySelector(selector);
return node ? node.textContent.trim() : "";
};
return {
subjectCode: el.getAttribute("subjectcode") || null,
teacher: el.getAttribute("teachername") || "未知教师",
hours: getText(".Timetable-TimetableItem-hours"),
location: getText(".Timetable-TimetableItem-location"),
name: getText(".Timetable-TimetableItem-subject-name"),
type: getText(".Timetable-TimetableItem-type"),
tttype: el.getAttribute("tttype"), // 如 "LESSON" 或 "SUBSTITUTION"
entityId: el.getAttribute("entityid"),
startTimeUnix: el.getAttribute("starttimeanddateunix"),
ariaLabel: el.getAttribute("aria-label"),
// 可选:从 header 的 sr-only 标题中解析更完整的描述
fullDescription: el.previousElementSibling?.textContent?.trim() || "",
};
})
);? 为什么用 $$eval 而非 $$ + ElementHandle.getProperty()?
$$eval 在浏览器上下文中执行 JavaScript,一次性完成全部 DOM 查询与数据组装,避免多次跨进程通信,性能更高、代码更简洁,且不会因 ElementHandle 失效(stale element reference)而报错。
⚠️ 关键注意事项
-
等待元素就绪:课表常为异步加载,务必在提取前等待目标元素出现:
await page.waitForSelector(".Timetable-TimetableItem", { timeout: 10000 }); - 选择器要健壮:避免 [class='...'] 这类严格匹配(易因 CSS 类顺序/增删失效),改用 .ClassA.ClassB 多类组合;若 .Timetable-TimetableItem 已唯一,可直接使用。
- 容错处理:对可能缺失的属性(如 teachername)添加 || null 或默认值,防止 undefined 导致后续逻辑中断。
- 时间戳转换(进阶):starttimeanddateunix 是秒级 Unix 时间戳,可用 new Date(Number(timestamp) * 1000) 转为可读日期对象。
- 反爬提示:虽属校内系统且获授权,仍建议添加合理延时、设置 userAgent 并复用 browser 实例,体现良好实践。
? 总结
结构化网页数据抓取的核心是「按数据单元选择,而非按视觉区块选择」。针对课表这类列表型内容,应锚定每一项的原子容器(.Timetable-TimetableItem),再通过 $$eval 高效批量提取属性与子节点文本。此举不仅提升代码可维护性与鲁棒性,也为后续构建信息屏、同步日历或生成 JSON API 奠定清晰的数据基础。

















