Skip to content

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。

1. 数据清单

对象 字段 说明
学期 学年、学期代码、学期开始日期 xnm + xqm;开始日期来自 rqazcList
课程 kcmc、kch、kch_id 课程名、课程号
教学班 jxb_id、jxbmc 教学班 ID/名称;同一课程可能多个班
教师 xm 姓名,可能多人用分隔符
学分 xf 字符串,转数字要容错
校区 xqmc 如「闵行」
教室 cdmc 如「东上院 101」
时间 xqj、jc/jcs、zcd 星期几、节次、周次表达式
备注 xkbz 选课备注
校历 rqazcList 含日期字符串,可解析学期开始

无法获取(Dela 未实现或未验证):

  • 成绩、GPA、考试安排、选课结果、培养方案。社区项目有实现,但需按当前教务系统复核。

2. 认证

登录入口

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 是功能模块号,不要漏。

3. 学期代码

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]


4. 响应字段

{
  "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 选课备注 可能为空

5. 周次解析

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):

  1. 检测 单/双。
  2. 去掉 周、括号、单/双。
  3. 按 ,、,、、 分段。
  4. 每段取数字,第一个是起点,第二个是终点;只有一个数字就是单周。
  5. 展开到 30 周上限;单/双周过滤。
  6. 排序去重。
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 周开始。
  • 不要假设所有课都有周次;缺失时显示「周次未提供」,不要猜。

6. 学期开始日期

课表接口的 rqazcList 里含日期字符串。Dela 的做法:

  1. 递归/字符串化所有值。
  2. 用正则抓 20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}。
  3. 校验年月日是否合法。
  4. 取排序后的第一个日期作为学期开始。
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 周」,不能生成具体日期。

7. 错误与失效

表现 含义 处理
302 到 /jaccountlogin 或 jaccount.sjtu.edu.cn 会话失效 重新登录
200 但返回 HTML 登录页/错误页 按失效处理,不要按 JSON 解析
JSON 解析失败 接口变了/被拦截 报错并保留旧缓存
kbList 缺失 响应结构变了 报错,不静默返回空
空 kbList 可能真的没课,也可能学期选错 显示空状态,提示核对学期

原则:同步失败不清空已缓存课表。Dela 的做法是保留旧数据,标记同步错误。


8. 成绩、考试、GPA

Dela 目前只实现课表。成绩/考试/GPA 是社区项目覆盖的,属于 C 级证据:

  • pysjtu:Python 的 i.sjtu 客户端,覆盖课程、选课、课表、考试、GPA 等;部分接口可能已随教务系统变化。
  • SJTU-Geek/sjtu-mcp-server:C# MCP Server,覆盖个人课表、课程成绩、学积分排名、考试信息。
  • openclaw-sjtu / sjtu-agent:也包含成绩/考试相关工具。

使用前必须:

  1. 用本人账号重新抓一次请求。
  2. 确认端点、参数、CSRF/Referer 是否仍然需要。
  3. 确认返回字段和分页。
  4. 遵守「只读本人数据、不自动选课、不抢资源」的边界。

具体端点不写死在这里,避免把过时的社区实现当成契约。需要时看待验证清单。


9. 常见问题

  1. xqm 使用 3/12/16:写成 1/2/3 会返回空课表或错误页。
  2. xnm 是学年开始年:2026-2027 学年传 2026。
  3. 必须带 X-Requested-With 和 Referer:否则可能返回登录页/错误页。
  4. 必须复用同一个 Session:新建 HTTP 客户端不会自动有 Cookie。
  5. 登录失效通常返回 302 或 HTML:先看内容类型和最终 URL,不要只等 401。
  6. 周次是中文表达式:不要按 1-16 硬切;单/双周、中文逗号、括号都要处理。
  7. 学期开始日期可能解析不到:应允许用户手动设置,不要猜测。
  8. 当前学期是本地推断:9 月/1–2 月/3–7 月/8 月四段规则;系统时间错了会推错。
  9. 空课表不一定是没课:可能学期选错、账号没选课、接口返回了错误页。
  10. 不要做自动选课或抢课:这违反学校流程,也不在本 Wiki 范围内。

10. 最小示例

// 伪代码: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 日历。


11. 证据与来源

  • 登录入口、端点、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 标为社区证据。

Clone this wiki locally