小玩具:HUST 吃饭
2025年12月15日
灵感是上边这个👆,原项目是用 perl 写的,奈何本人并不会写 perl ,并且 perl 对于我而言针对 web 集成而言其实相当不友好,于是决定用 next.js 重写一个版本,嘻嘻,这下配置容易些了。原项目:HUST-Chifan (Perl 版本)
核心
核心原理其实很简单,我们通过cheerio爬取华中科技大学后勤处官网的链接(上面包含着各个食堂的营业时间等等),然后我们针对性的进行一些处理:
开发
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from "next/server";
import { parseCanteenTd } from "@/lib/parseCanteen";
import * as cheerio from "cheerio";
export async function GET() {
const html = await fetch("https://hq.hust.edu.cn/ysfw/stfw.htm").then((r) =>
r.text()
);
const $ = cheerio.load(html);
const canteens: any[] = [];
$("td[valign='top']").each((_, td) => {
const tdHtml = $.html(td);
const parsed = parseCanteenTd(tdHtml);
if (parsed.name) canteens.push(parsed);
});
return NextResponse.json({ canteens });
}
嘻嘻,由你所见,我们只是fetch了一下静态的html页面,然后利用cheerio加载,找到匹配的td[valign='top']元素,然后对其进行一些处理:
import * as cheerio from "cheerio";
export function parseCanteenTd(tdHtml: string) {
const $ = cheerio.load(tdHtml);
const name = $("strong span")
.text()
.replace(/^\d+、?/, "")
.replace(/\s+/g, " ")
.trim();
const times: { meal: string; start: string; end: string }[] = [];
$("p").each((_, p) => {
const text = $(p).text().replace(/\s+/g, " ").trim();
const match =
/(早餐|中餐|午餐|晚餐|早、中餐)\s*(\d{1,2}[::]\d{2})\s*[-–~]\s*(\d{1,2}[::]\d{2})/.exec(
text
);
if (match) {
let [start, end] = match.slice(2);
const meal = match[1];
start = start.replace(":", ":");
end = end.replace(":", ":");
times.push({ meal, start, end });
}
});
return { name, times };
}
然后我们用正则表达式清洗掉鲨臂官网上乱七八糟的标题文本,然后匹配match的文本,注意,HUST有些食堂标注的是“早、午餐”,很是神奇。
现在,我们就得到了干干净净的数据,我们就可以把这些数据乖乖地push到我们构建的数组中咯!
同样地,对于/api/open-now,我们进行一些处理:
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NextResponse } from "next/server";
export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
const data = await fetch(`${baseUrl}/api/canteen`).then((r) => r.json());
const now = new Date();
const beijingMinutes = (now.getUTCHours() + 8) * 60 + now.getUTCMinutes();
const open = data.canteens
.map((c: any) => {
const currentTimeSlot = c.times.find((t: any) => {
const [sh, sm] = t.start.split(":").map(Number);
const [eh, em] = t.end.split(":").map(Number);
const start = sh * 60 + sm;
const end = eh * 60 + em;
return beijingMinutes >= start && beijingMinutes <= end;
});
if (!currentTimeSlot) return null;
const [eh, em] = currentTimeSlot.end.split(":").map(Number);
const endMinutes = eh * 60 + em;
const remaining = Math.max(0, (endMinutes - beijingMinutes) * 60 * 1000);
return {
...c,
remaining,
};
})
.filter(Boolean);
if (!open.length) {
return new Response("坏了,现在没有吃的了", { status: 404 });
}
return NextResponse.json(open);
}
由于我们最终可能要部署到 Vercel 或者类似的服务器,服务器发起请求时时间的运算时 UTC 时间,而我们在中国,因此需要手动转换成北京时间,否则我们在计算当前是否有食堂开门的时候就会有一些些问题
最终我们就得到了以下的 API:
API
/api/canteen
Method:GET 获取所有食堂的JSON数据
/api/canteen/:canteenName
Method:GET 获取指定食堂的JSON数据
/api/canteen/open-now
Method:GET 获取目前能吃的食堂的数据
/api/kaifan
Method:GET 获取所有食堂目前的开饭状态
与你的 bot 集成
这个玩意儿可以轻松缝合到你的 next 项目中,搭建一个属于你的提醒吃饭 bot!
只需要注意把当前的系统时间和获取到的数据一起写入llm的prompt里就可以了!