Repository navigation
Timetable
ChangYo edited this page Oct 25, 2026
·
2 revisions
| 项 | 值 |
|---|---|
| 平台 | i.sjtu.edu.cn |
| 认证 | 校园 Web 会话(jAccount SSO Cookie) |
| 数据方向 | 只读 |
| 难度 | 中 |
| 最后验证 | 2026-09-15 |
| 证据等级 | A(Dela 实测) |
教学信息服务网是本科教务系统(正方)。它没有公开 API,课表来自登录后的 JSON 端点。Dela 用它读取个人课表;成绩、考试和 GPA 目前只有社区项目验证,见第 8 节。
- 可获取:学年与学期、课程名称与代码、教学班、教师、学分、校区、教室、星期、节次、周次、选课备注。
- 前置:
i.sjtu.edu.cn登录会话。用户在受控窗口中完成一次 jAccount 登录。 - 注意:
xqm使用3/12/16;周次zcd是中文表达式;登录失效通常返回 302 或 HTML,很少返回 401。
| 对象 | 字段 | 说明 |
|---|---|---|
| 学期 | 学年、学期代码、学期开始日期 |
xnm + xqm;开始日期来自 rqazcList
|
| 课程 |
kcmc、kch、kch_id
|
课程名、课程号 |
| 教学班 |
jxb_id、jxbmc
|
教学班 ID/名称;同一课程可能多个班 |
| 教师 | xm |
姓名,可能多人用分隔符 |
| 学分 | xf |
字符串,转数字要容错 |
| 校区 | xqmc |
如「闵行」 |
| 教室 | cdmc |
如「东上院 101」 |
| 时间 |
xqj、jc/jcs、zcd
|
星期几、节次、周次表达式 |
| 备注 | xkbz |
选课备注 |
| 校历 | rqazcList |
含日期字符串,可解析学期开始 |
无法获取(Dela 未实现或未验证):
- 成绩、GPA、考试安排、选课结果、培养方案。社区项目有实现,但需按当前教务系统复核。
https://i.sjtu.edu.cn/jaccountlogin
用户在受控窗口里完成 jAccount 登录(账号密码/扫码/短信 + 验证码),教学网在自己的域上种会话 Cookie。
登录后不要只看 URL,直接拉一次当前学期课表:
- 返回
{"kbList":[...]}:登录有效。 - 302 到登录页:登录失效。
- 返回 HTML:登录失效或被反爬/跳转。
- JSON 解析失败:按登录失效处理,提示重新登录。
POST https://i.sjtu.edu.cn/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: https://i.sjtu.edu.cn/kbcx/xskbcx_cxXsgrkbIndex.html?gnmkdm=N2151
xnm=2026&xqm=3&kzlx=ck&xsdm=-
X-Requested-With和Referer都要带;缺了可能返回登录页或错误页。[实测] - 请求必须复用登录后的同一个 Session;不要新建一个干净的 HTTP 客户端。
-
gnmkdm=N2151是功能模块号,不要漏。
xnm 是学年开始年份,xqm 是学期代码:
| 学期 | xqm |
示例 |
|---|---|---|
| 第一学期(秋) | 3 |
xnm=2026&xqm=3 = 2026-2027 秋 |
| 第二学期(春) | 12 |
xnm=2026&xqm=12 = 2026-2027 春 |
| 第三学期(暑) | 16 |
xnm=2026&xqm=16 = 2026-2027 暑 |
Dela 内部用 2026-2027-1/2/3 表示,再映射到 xnm/xqm。[实测]
没有可靠的「当前学期」接口时,Dela 按本机月份推断:
| 月份 | 推断 |
|---|---|
| 9–12 | <year>-<year+1>-1 |
| 1–2 |
<year-1>-<year>-1(仍属上一学年秋) |
| 3–7 | <year-1>-<year>-2 |
| 8 |
<year-1>-<year>-3(暑期) |
这是本地推断,学校没有公开统一规则。系统时间错误会导致推断错误;界面应显示推断依据,并允许用户手动选择学期。[代码 2026-09]
{
"kbList": [
{
"kcmc": "机器学习",
"kch": "CS3308",
"kch_id": "CS3308-01",
"jxb_id": "2026-2027-1-...",
"jxbmc": "机器学习-01",
"xm": "张三",
"xf": "3.0",
"xqmc": "闵行",
"cdmc": "东上院101",
"xqj": "2",
"jc": "1-2节",
"jcs": "1-2",
"zcd": "1-16周",
"xkbz": ""
}
],
"rqazcList": [/* 元素形态未记录;按 unknown 处理 */]
}| 字段 | 含义 | 注意 |
|---|---|---|
kcmc |
课程名称 | 中文,可能带空格 |
kch |
课程号 | 如 CS3308
|
kch_id |
课程号 ID | 可能带班号 |
jxb_id |
教学班 ID | 稳定的课表主键候选 |
jxbmc |
教学班名称 | 展示用 |
xm |
教师 | 可能多人 |
xf |
学分 | 字符串 |
xqmc |
校区 | 展示用 |
cdmc |
教室 | 展示用 |
xqj |
星期几 | 1–7;非法值要跳过 |
jc / jcs
|
节次 |
jc 是展示文本,jcs 是数字范围 |
zcd |
周次 | 中文表达式,见下 |
xkbz |
选课备注 | 可能为空 |
zcd 的常见形态(不保证穷尽;仓库内没有固定样本测试):
1-16周1-8周(单)2-16周(双)1,3,5,7周1-4,6-8周-
1-16周(单),2-16周(双)(少见)
Dela 的解析策略(src/main/timetable/api.ts):
- 检测
单/双。 - 去掉
周、括号、单/双。 - 按
,、,、、分段。 - 每段取数字,第一个是起点,第二个是终点;只有一个数字就是单周。
- 展开到 30 周上限;单/双周过滤。
- 排序去重。
export function parseWeekNumbers(value: string | undefined): number[] {
if (!value) return [];
const odd = /单/.test(value);
const even = /双/.test(value);
const result = new Set<number>();
for (const part of value.replace(/[周()()单双]/g, '').split(/[,,、]/)) {
const numbers = part.match(/\d+/g)?.map(Number) ?? [];
if (numbers.length === 0) continue;
const start = numbers[0]!;
const end = numbers[1] ?? start;
for (let week = start; week <= end && week <= 30; week += 1) {
if ((odd && week % 2 === 0) || (even && week % 2 === 1)) continue;
result.add(week);
}
}
return [...result].sort((a, b) => a - b);
}注意:
- 单/双周标记作用于整个字符串。混合表达式需要按段解析;上面的简化版按整串处理,遇到混合会出错。
- 不要假设周次从 1 开始;有的课从第 2 周开始。
- 不要假设所有课都有周次;缺失时显示「周次未提供」,不要猜。
课表接口的 rqazcList 里含日期字符串。Dela 的做法:
- 递归/字符串化所有值。
- 用正则抓
20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}。 - 校验年月日是否合法。
- 取排序后的第一个日期作为学期开始。
function parseSemesterStart(values: unknown[] | undefined): string | null {
const dates = (values ?? [])
.flatMap((value) => {
const text = typeof value === 'string' ? value : (JSON.stringify(value) ?? '');
return text.match(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/g) ?? [];
})
.map((value) => {
const [year, month, day] = value.split(/[-/.]/).map(Number);
if (!year || !month || !day) return null;
const date = new Date(year, month - 1, day);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return null;
}
return `${year.toString().padStart(4, '0')}-${month
.toString()
.padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
})
.filter((value): value is string => value !== null)
.sort();
return dates[0] ?? null;
}如果解析不到:
- 不要用「今天」或「第 1 周 = 开学」硬猜。
- 让用户手动填写学期开始日期;Dela 使用
setSemesterStart提供这个入口。 - 没有开始日期时,日历事件只能显示「第 N 周」,不能生成具体日期。
| 表现 | 含义 | 处理 |
|---|---|---|
302 到 /jaccountlogin 或 jaccount.sjtu.edu.cn
|
会话失效 | 重新登录 |
| 200 但返回 HTML | 登录页/错误页 | 按失效处理,不要按 JSON 解析 |
| JSON 解析失败 | 接口变了/被拦截 | 报错并保留旧缓存 |
kbList 缺失 |
响应结构变了 | 报错,不静默返回空 |
空 kbList
|
可能真的没课,也可能学期选错 | 显示空状态,提示核对学期 |
原则:同步失败不清空已缓存课表。Dela 的做法是保留旧数据,标记同步错误。
Dela 目前只实现课表。成绩/考试/GPA 是社区项目覆盖的,属于 C 级证据:
-
pysjtu:Python 的 i.sjtu 客户端,覆盖课程、选课、课表、考试、GPA 等;部分接口可能已随教务系统变化。 -
SJTU-Geek/sjtu-mcp-server:C# MCP Server,覆盖个人课表、课程成绩、学积分排名、考试信息。 -
openclaw-sjtu/sjtu-agent:也包含成绩/考试相关工具。
使用前必须:
- 用本人账号重新抓一次请求。
- 确认端点、参数、CSRF/Referer 是否仍然需要。
- 确认返回字段和分页。
- 遵守「只读本人数据、不自动选课、不抢资源」的边界。
具体端点不写死在这里,避免把过时的社区实现当成契约。需要时看待验证清单。
-
xqm使用3/12/16:写成1/2/3会返回空课表或错误页。 -
xnm是学年开始年:2026-2027 学年传2026。 -
必须带
X-Requested-With和Referer:否则可能返回登录页/错误页。 - 必须复用同一个 Session:新建 HTTP 客户端不会自动有 Cookie。
- 登录失效通常返回 302 或 HTML:先看内容类型和最终 URL,不要只等 401。
-
周次是中文表达式:不要按
1-16硬切;单/双周、中文逗号、括号都要处理。 - 学期开始日期可能解析不到:应允许用户手动设置,不要猜测。
- 当前学期是本地推断:9 月/1–2 月/3–7 月/8 月四段规则;系统时间错了会推错。
- 空课表不一定是没课:可能学期选错、账号没选课、接口返回了错误页。
- 不要做自动选课或抢课:这违反学校流程,也不在本 Wiki 范围内。
// 伪代码:session 是已经完成 jAccount 登录的持久会话
async function fetchTimetable(session: CampusSession, xnm: string, xqm: string) {
const response = await session.fetch(
'https://i.sjtu.edu.cn/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
Referer: 'https://i.sjtu.edu.cn/kbcx/xskbcx_cxXsgrkbIndex.html?gnmkdm=N2151',
},
body: new URLSearchParams({ xnm, xqm, kzlx: 'ck', xsdm: '' }).toString(),
},
);
if (response.redirected) throw new Error('登录已失效,请重新登录');
const text = await response.text();
if (!text.trimStart().startsWith('{')) throw new Error('返回了 HTML,可能登录失效');
const data = JSON.parse(text) as { kbList?: unknown[] };
if (!Array.isArray(data.kbList)) throw new Error('课表数据结构已变化');
return data.kbList;
}生成日历见课表与 DDL 日历。
- 登录入口、端点、
xnm/xqm映射、请求头、kbList字段、周次解析、学期开始解析:[实测 2026-09,Dela src/main/timetable/api.ts] - 成绩/考试/GPA 社区覆盖:
[社区]pysjtu、SJTU-Geek/sjtu-mcp-server、openclaw-sjtu - 本科教学信息服务网:
[官方 2026-10-25]https://i.sjtu.edu.cn/
- 2026-10-25:首版。基于 Dela 课表实现重写,补学期代码、周次解析、错误处理;成绩/考试/GPA 标为社区证据。
本 Wiki 记录的是开发实践,不是学校官方承诺。每个结论都标注证据与日期;校园平台会调整字段、权限和登录方式,动手前请按页面里的「最后验证」复核。发现过期内容请提 Issue 或直接改。