-
Notifications
You must be signed in to change notification settings - Fork 8
Text Import
一句话 TL;DR:把课表文本粘进 Sleepy(或从其他 App 打开文件)后发生的事——格式识别、容错、预览、五种落库模式、错误反馈。普通用户看用法,贡献者看解析器行为。入口、外部打开与完整格式分派的总览见导入格式全解。
「粘贴课表文本」在导入弹窗第二行,点开是一个 160dp 高的输入框加一枚「预览导入」按钮(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:232-285)。粘贴,点按钮,解析成功弹预览对话框。当前一张课表都没有时也能走这条路——目标表按 0 号处理,落库自动建新表(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1235-1236)。
导入弹窗底部「支持格式」列表列出 6 行(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:315-344),每行 ⓘ 点开有识别要求、示例、来源说明(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:574-743)。解析器实际分派 9 个分支,比 UI 列表多出 sleepy-v1 原生格式、新版文件拒绝、Excel Frameset 拦截三条;完整分派表(顺序、嗅探特征、解析函数)与 9 分支逐条行为见导入格式全解的内容嗅探一节(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:109-129)。
判断只看内容,文件扩展名与 MIME 都不参与。粘贴路径与文件路径进同一个 ScheduleParser.parse()(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:265,177)。下文只展开文本类格式的解析深度,不重复分派顺序。
WakeUp 课程表 app 里点「分享课程」生成的整段文字,以【来自WakeUp课程表】开头,核心是 courseDetailJson 字段——一段 URL 编码的 JSON 课程数组,解码后逐门转成课程(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:240-262)。课表名、开学日期、每门课的颜色一并导入;tableInfo.time(Sleepy 自家导出)或 tableInfo.timeList(WakeUp 原生)里的节次时间表也会收割(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:299-321)。
文本前带「【来自Sleepy】\n课程分享:\n\n」之类前缀时,只要出现 { 就从第一个 { 开始读(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:104-107)。
支持 WakeUp 导出(DESCRIPTION 带「第X - Y节」,双周课拆成多条短事件)与 Sleepy 自家导出(INTERVAL=2 表达单双周)两种来源(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:396-411)。学期第 1 周取最早 DTSTART 所在周的周一(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:481-485)。描述里没有节次信息时按 45min 课 + 5min 课间 ≈ 50min/节估算(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:732-747);东北大学等教务导出连描述都没有,解析器从全部事件的起止时间形态反推教学时间块再整表重建节次表,块宽同样按 50min/节折算(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:664-683)。
同一门课按 (课名, 星期, 节次, 教师) 聚合各周区间:逐周连续合并为每周区间;全同奇偶且间距 2 判成单/双周;散周按教室分段输出(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:491-560)。教室不进聚合键——WakeUp 会把每周换教室的同一门课拆成多条事件,进了键就会拆出两个假单双周(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:487-490)。
CSV 表头逐列模糊匹配中英文列名,必需列:课程(课程/课程名/名称/course/name)、星期、节次、周次;教师、教室、类型、备注可选(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:949-967)。节次两种写法:单列 1-2,或「开始节数 + 结束节数」两列。周次支持 2-16、2-5,7-9,11-14 多区间、11,13,15 离散周、单值 5,每个区间展开为一条课程记录(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:1058-1068)。带引号与 "" 转义的标准 CSV 照常解析(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:1071-1101)。
HTML 抽取全部 <table>,找含「课程/course/name」的表头行按列解析;找不到表头就把单元格拍平成文本行,退回纯文本逻辑(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:1158-1221)。列是「周一~周日」的网格课表解析不了,要「一行一门课」的长表格(app/src/main/res/values/strings.xml:181)。
Sleepy 原生格式。第一行固定 #sleepy-v1,其后三种行:T表名|开学日期|总周数|每天节数 表信息、N节号|开始|结束 作息、C课名|星期|节次|周次|教师|教室|颜色|备注|上课时间|组 课程行,固定 10 列 9 个竖线,空列竖线不能少(app/src/main/res/values/strings.xml:186-198)。周次五种形态:1-16(每周)、1-15单、2-16双、3-4定(区间指定)、10定(单周);后缀词表还包括 散/奇/偶/o/odd/e/even(app/src/main/java/com/lingion/sleepy/data/parser/SleepyNativeFormat.kt:179-198)。裸单数字按「只上这一周」处理(type=3),区间无后缀才是每周。
magic 行本身容错:允许 ≤4 字符引用前缀、1-2 个半角或全角井号、大小写混写,须出现在前 32 个非空行内——微信长转发头叠在前面也能命中(app/src/main/java/com/lingion/sleepy/data/parser/SleepyNativeFormat.kt:23-46)。
更早的制表符纯文本仍可导入,一行一课:课程名 老师 教室 星期 节次 周次 类型,字段间制表符、空格或全角逗号分隔,至少 6 列,类型列可省(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:780-849,867)。
手上只有课表截图时,点「纯文本」一行的 ⓘ,详情弹窗底部有一段可一键复制的转换 Prompt(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:669-733)。发给豆包、ChatGPT 等识图 AI,连同截图一起,AI 输出 sleepy-v1 文本,整段复制回输入框即可(app/src/main/res/values/strings.xml:132)。Prompt 里写死了格式规则、周次规则与输出自检(app/src/main/res/values/strings.xml:133)。
粘贴进来的文本大多出自 AI 或手工,格式脏是常态。解析分派前先做两步预处理(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:100-101):
- 全角归一:
1-2→1-2、~→~、全角空格与 BOM 转空格(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:191-208)。 - AI 隔离标识:
<<<SLEEPY-BEGIN>>>与<<<SLEEPY-END>>>之间的才算课表数据,标识外的开场白与结尾废话全部丢弃(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:166-188)。标识写歪也认:2-4 个任意括号、大小写、少横线都容忍;只写 BEGIN 取其后全部,只写 END 取其前全部(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:173-188)。
行级容错集中在纯文本路径:
- Markdown 痕迹剥离:管道表格行转制表符,
|---|分隔行跳过,**加粗**与反引号剥掉(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:834-840)。 - 星期列接受
1-7、周一~周日、Monday/mon(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:1104-1118)。 - 区间反写
16-1自动排序,避免生成永远不显示的隐形课(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:899-901,859-861)。 - day 越界(0/8)钳到 1..7(
app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:855-858)。 - 类型列未知值按「按周次」(type=3)处理而非「每周」——把「周次=6 单次实验」误标成全年每周就是这里防的(
app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:1124-1133)。 - WakeUp JSON 字段名带回退:
name/courseName、position/room;JSON 配置开ignoreUnknownKeys + isLenient,多余字段不炸(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:30,344-351)。
作息时间有三条收割通道:纯文本里混排的作息行(第1节 08:00-09:35,或 <<<SLEEPY-TIME-BEGIN>>> 块,块尾缺失时吞连续作息行自愈,app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:36-84,803-832)、CSV 的「开始时间/结束时间」两列(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:998-1007)、ICS 的 DTSTART/DTEND(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:574-605)。
节次数取「作息声明」与「课程实际到达最大节」的较大者:WakeUp 分享里 startNode=11 step=3 的课到达 13 节,即使文本没带任何作息声明,nodesPerDay 也是 13,不会被老表 10 节压掉(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:276-292)。没有作息来源时 timeJson 留空,不伪造时间(app/src/test/java/com/lingion/sleepy/data/parser/WakeUpShareNodesLosslessTest.kt:28-34)。
容错有一条硬边界:新版 Sleepy 导出的文件(v2+)收到明确报错「请升级 Sleepy 后导入」,拒绝静默尽力解析——丢 v2 专有字段应该是升级决策,由你来做(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:111-114)。
两门课冲突 = 同一天 + 周区间重叠 + 节次区间重叠,三个条件全中(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1520-1528)。
同名课合并在 groupId 层做。非 sleepy-v1 来源落库时,课名经 trim + 空格归一 + 小写后相同的行共用一个 groupId,渲染为同一张课程卡(app/src/main/java/com/lingion/sleepy/data/repository/ScheduleRepository.kt:336-343)。sleepy-v1 来源自带解析端权威 groupId(第 10 列组 token 非空按 token 分组,空则按课名),落库绕过再分配,分区关系原样保留(app/src/main/java/com/lingion/sleepy/data/parser/SleepyNativeParser.kt:12-14,223;app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1307-1311)。
同一门课出现 2 个以上不同地点时,预览给一条提示,导入后按独立节次展示,不阻塞(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1249-1260)。
五种模式的枚举值、按钮文案、逐条行为、按钮排布与红底标识,详见导入格式全解的应用模式表(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:747-755,991-1061)。这里只补文本视角特有的行为:
- 「仅追加无冲突」叠三层闸门,采用相对判定:只拦「因这条候选而让某天新超 2 层」的课,原表本来就有超层天时不连坐(
app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1362-1365)。被剔的课所在日期在 toast 里点名;全冲突时报「没有可追加课程,全部与现有课表冲突」(app/src/main/res/values/strings.xml:103)。 - 写当前表的两种追加模式都会做节次无损延伸:老表作息与导入作息逐节合并,节次数拓到导入课程实际到达的最大节(
app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1379-1392,1469-1481)。 - 新建表重名自动加序号:
高数已存在则新表叫高数2,再撞继续递增(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1530-1537)。
预览对话框七区块、确认对话框(改名/日期/节次编辑器播种/三道校验)的完整清单见导入格式全解的导入预览一节(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:776-1069,1100-1223)。落库前最后两步:
确认框里的开学日期归一到所在周一(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1293);整个导入(不管写几张表多少门课)包成单个撤回批,顶栏撤回一次回到导入前(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:1290,1487)。确认即落库,之后停在原处弹「导入成功」snackbar,不跳转任何带「保存」按钮的页面(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:356-362,444-458)。
失败信息全部走导入弹窗内的 SnackbarHost——解析失败「导入失败:{原因}」、空内容「内容为空」、读文件失败带异常消息(app/src/main/java/com/lingion/sleepy/ui/screen/imports/ImportSheet.kt:350-355,189-194;app/src/main/res/values/strings.xml:77-78,106)。
部分失败不上 toast,进预览对话框的对应区块:行级失败走「未识别行」(有 %1$d 行没能识别,每条截前 40 字符),表级问题走「提示」区(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:847,1316-1318;app/src/main/res/values/strings.xml:199-201)。Excel Frameset HTML 是特例:直接报「这是 Excel 导出的网页(课表在附属文件 xxx 里,本应用无法读取),请在 Excel 中另存为 CSV 后重新导入」,点名出路(app/src/main/java/com/lingion/sleepy/data/parser/ScheduleParser.kt:159-164)。
app/src/test/java/com/lingion/sleepy/data/parser/ 下 19 个测试文件锁解析行为,贡献者改解析器前先看这些:
-
SleepyMarkerTest.kt— AI 隔离标识与行级容错全矩阵:标识写歪、忘写 END、代码围栏、Markdown 表格、中文星期、全角数字、区间反写、day 越界、加粗剥离、垃圾行上报、作息行收割(app/src/test/java/com/lingion/sleepy/data/parser/SleepyMarkerTest.kt:17-307)。 -
RealUserAITest.kt— 真实用户 AI 产出样本:39 条课程行全量导入,0 dropped 0 warnings;11 列行尾部多列静默忽略;散周/双周/转义括号/同名同组逐项断言(app/src/test/java/com/lingion/sleepy/data/parser/RealUserAITest.kt:51-103)。 -
WakeUpShareNodesLosslessTest.kt— 节次无损:分享文本课程到达 13 节必须报 13(app/src/test/java/com/lingion/sleepy/data/parser/WakeUpShareNodesLosslessTest.kt:27-34)。 -
ScheduleParserTypeTest.kt— 类型列缺失/未知值得 type=3 的端到端语义(app/src/test/java/com/lingion/sleepy/data/parser/ScheduleParserTypeTest.kt:20-82)。 -
ExportImportRoundTripTest.kt— 导出→再导入的往返一致性(app/src/test/java/com/lingion/sleepy/data/parser/ExportImportRoundTripTest.kt:49-83)。
Sleepy Wiki
入门
界面与视图
课程管理
导入导出
小组件与提醒
数据与设置
项目
社区