首页 / 资讯中心 / 文章详情

xlsx课表转ics日历:Python脚本自动处理单双周与调休

xlsx课表转ics日历:Python脚本自动处理单双周与调休 ★ FEATURED ARTICLE
简介这份资源面向需要将Excel课表批量导入日历的学生与教育工作者解决手动逐条录入课程、跨平台日程难以同步的问题。包内共4个文件包含可执行程序、课表样例、JSON配置与说明文档压缩包约9.8MB其中exe负责核心转换xlsx提供课程数据模板json用于定制上课时间与日期规则txt则给出运行与配置指引。已有7304人学习下载说明该方案在课表迁移场景中具备一定实用参考价值。读者可借助它把课程名称、教师、教室、起止时间等信息解析为ics日历事件直接导入Google日历、Apple日历或Outlook实现提醒与时间规划同时可通过配置文件适配不同作息安排并参考说明文档排查格式不兼容等问题适合希望提升时间管理效率、减少重复录入的初、中级用户。1. 从 xlsx 课表到 ics 日历为什么我宁愿写脚本也不手动录每学期开学第一周总有人对着 Excel 课表一行行往手机日历里敲敲到第三门课就发现单双周忘了区分再回头改已经录错的时段。xlsx 课表转 ics 日历格式文件这件事本质是把二维表格里的时间、地点、周次规则翻译成 iCalendar 标准能识别的 VEVENT 事件流。它解决的不是能不能看课表而是课表能不能自动进日历、自动提醒、自动处理单双周和调休。适合谁手里有一份教务系统导出的 xlsx、又不想每周手动核对的人也适合需要把课程表批量同步到多个日历客户端的场景。我拆过好几份不同学校导出的课表字段名五花八门但转换逻辑是通的。2. 拆解 xlsx 课表结构先看懂字段再谈转换2.1 课表 xlsx 的三种典型布局教务系统导出的课表布局逃不出这三类。第一种是格子表行是节次、列是星期单元格里塞着高等数学 张三 教一101 1-16周。第二种是列表表每行一条记录字段拆成课程名、教师、周次、节次、地点。第三种是混合表上半部分是格子下半部分藏着调休说明。转换前必须先判断属于哪种因为格子表需要先做单元格解析列表表可以直接按行读。我一般先用 pandas 把前 10 行打出来看结构别急着写转换逻辑。很多翻车都发生在没看清表头有几行合并单元格。import pandas as pd # headerNone 先不指定表头把原始网格打出来 raw pd.read_excel(schedule.xlsx, headerNone) print(raw.shape) print(raw.iloc[:10, :8].to_string())这段代码的作用是侦察。headerNone避免 pandas 把合并单元格的第一行误当表头shape告诉你表格多大iloc[:10, :8]只看左上角区域足够判断布局类型。参数上如果你的课表第一行确实是干净的表头可以改成header0但我不建议一上来就这么干。2.2 时间、周次、节次三要素的归一化不管哪种布局最终都要归一到三个要素日期时间、周次规则、地点。节次到具体时间的映射每个学校不同常见做法是维护一张节次时间表节次开始时间结束时间第1节08:0008:45第2节08:5509:40第3节10:0010:45第4节10:5511:40第5节14:0014:45周次规则要处理1-16周、1-8,10-16周、单周、双周这几种写法。我一般写一个解析函数把周次字符串转成整数列表后面生成事件时按周循环。import re def parse_weeks(week_str): 把 1-8,10-16周 或 单周 解析成周次整数列表 week_str str(week_str).replace(周, ).strip() if 单 in week_str: return list(range(1, 17, 2)) # 单周1,3,5...15 if 双 in week_str: return list(range(2, 17, 2)) # 双周2,4,6...16 weeks set() for part in week_str.split(,): if - in part: start, end part.split(-) weeks.update(range(int(start), int(end) 1)) elif part.isdigit(): weeks.add(int(part)) return sorted(weeks)逻辑说明先去掉周字判断单双周直接返回固定序列否则按逗号切分遇到a-b展开成区间遇到单个数字直接加入集合。参数上range(1, 17, 2)里的 17 是学期总周数上限如果你的学校是 18 周或 20 周改这个值。这个函数是整个转换的地基周次解析错了后面全错。2.3 用 openpyxl 读取合并单元格的真实值格子表最坑的地方是合并单元格。pandas 读合并单元格时只有左上角有值其余是 NaN。这时候得换 openpyxl它能告诉你哪些格子属于同一个合并区域。from openpyxl import load_workbook wb load_workbook(schedule.xlsx) ws wb.active # 建立合并单元格映射每个坐标 - 合并区域左上角的值 merged_map {} for merged_range in ws.merged_cells.ranges: top_left ws.cell(merged_range.min_row, merged_range.min_col).value for row in range(merged_range.min_row, merged_range.max_row 1): for col in range(merged_range.min_col, merged_range.max_col 1): merged_map[(row, col)] top_left # 读取时优先查映射表 def get_cell(row, col): return merged_map.get((row, col), ws.cell(row, col).value)这段的关键是merged_cells.ranges拿到所有合并区域然后把左上角的值铺满整个区域。get_cell先查映射表查不到再读原始单元格。参数上min_row/min_col/max_row/max_col是 openpyxl 合并区域对象自带的边界属性不用自己算。有了这个格子表里跨行跨列的课程名就不会丢了。3. 生成 ics 文件VEVENT 结构、时区和重复规则3.1 iCalendar 最小可用结构ics 文件本质是纯文本遵循 RFC 5545。一个能被日历客户端识别的最小文件长这样BEGIN:VCALENDAR VERSION:2.0 PRODID:-//MySchedule//CN CALSCALE:GREGORIAN BEGIN:VEVENT UID:course-001local DTSTAMP:20250101T000000Z DTSTART;TZIDAsia/Shanghai:20250224T080000 DTEND;TZIDAsia/Shanghai:20250224T084500 SUMMARY:高等数学 LOCATION:教一101 DESCRIPTION:教师张三 END:VEVENT END:VCALENDAR几个硬性要求UID必须全局唯一否则日历客户端会去重或报错DTSTAMP是创建时间戳用 UTCDTSTART和DTEND带TZID参数指定时区。我一般用课程名加节次加星期拼 UID保证同一门课不同时段不冲突。3.2 用 icalendar 库组装事件手拼字符串容易在换行和转义上翻车用icalendar库更稳。它自动处理 75 字节折行和特殊字符转义。from icalendar import Calendar, Event from datetime import datetime, timedelta import pytz tz pytz.timezone(Asia/Shanghai) cal Calendar() cal.add(prodid, -//MySchedule//CN) cal.add(version, 2.0) def add_course(cal, name, teacher, location, start_dt, end_dt, weeks, weekday): 为一门课生成多个 VEVENT每个上课周一个 for week in weeks: # 计算该周对应的实际日期 event_date start_dt timedelta(weeksweek - 1) ev Event() ev.add(uid, f{name}-{weekday}-{week}local) ev.add(summary, name) ev.add(location, location) ev.add(description, f教师{teacher}) ev.add(dtstart, tz.localize(event_date)) ev.add(dtend, tz.localize(event_date (end_dt - start_dt))) cal.add_component(ev) return cal逻辑说明add_course接收课程基础信息和第一周的起始时间然后按周次循环每次用timedelta(weeksweek-1)推算出该周的实际日期。tz.localize把 naive datetime 转成带时区的时间这一步不能省否则日历客户端可能按 UTC 解释导致时间偏移 8 小时。参数上start_dt传第一周该星期几的日期时间end_dt传对应的下课时间两者差值就是单次课时长。3.3 用 RRULE 还是逐周展开iCalendar 支持RRULE重复规则理论上可以用一条规则表示每周重复 16 次。但课表有单双周、有调休、有期中停课逐周展开反而更可控。我两种都试过逐周展开生成的 ics 文件大一些16 周就是 16 个 VEVENT但导入后不会出现某周该停课却还在的玄学问题。如果你坚持用 RRULE写法是ev.add(rrule, {freq: weekly, count: 16})但单双周就得拆成两条规则维护成本反而高。我的建议是周次规则简单且无调休时用 RRULE否则逐周展开。# RRULE 写法示例仅适用于无调休的连续周 ev.add(rrule, {freq: weekly, count: len(weeks)})3.4 写出文件与编码处理最后一步把 Calendar 对象序列化写盘。注意用二进制模式写icalendar的to_ical()返回 bytes。with open(schedule.ics, wb) as f: f.write(cal.to_ical())如果课程名里有逗号、分号、反斜杠icalendar会自动转义不用手动处理。但如果你是自己拼字符串这些字符必须转义否则日历客户端解析会截断。写完文件后我一般用文本编辑器打开看一眼确认BEGIN:VEVENT和END:VEVENT成对出现。4. 避坑与排查那些让我重导三次日历的坑4.1 时区没带导致所有课程偏移 8 小时现象导入日历后早上 8 点的课显示成下午 4 点。原因DTSTART写成了不带TZID的 UTC 时间或者用了 naive datetime 被客户端按本地时区又转了一次。解决所有时间字段统一用TZIDAsia/Shanghai代码里用pytz.timezone(Asia/Shanghai).localize()处理。别用datetime.now()直接塞进去。4.2 UID 重复导致事件被吞现象导入后课程数量比预期少某些课不见了。原因不同课程生成了相同的 UID日历客户端按 UID 去重只保留一条。解决UID 里拼上课程名、星期、周次、节次保证唯一。我习惯再加一个随机后缀或时间戳。4.3 合并单元格读取为 None 导致课程丢失现象格子表里某些课程完全没进 ics。原因pandas 读合并单元格时非左上角是 NaN直接遍历会跳过。解决用 openpyxl 的merged_cells.ranges建立映射如 2.3 节所示。这个坑我踩过两次第二次才想起来合并单元格这回事。4.4 周次解析把1-16周当成字符串比较现象所有课程都只生成了一周的事件。原因周次字段没做类型转换1-16被当成字符串int()转换失败后静默跳过。解决解析前先str()再strip()对每个分支做异常捕获并打印原始值方便定位。我一般会在解析函数里加一行print(f解析周次: {week_str} - {weeks})调试完再删。4.5 ics 文件换行符不兼容现象某些日历客户端提示文件格式错误。原因Windows 下写文件默认\r\n而 iCalendar 规范要求\r\n作为行分隔符——这本来是对的但如果你用文本模式写入又经过某些编辑器转换可能变成\n。解决用wb二进制模式写icalendar库会输出规范要求的\r\n。别用文本模式手动拼。5. 进阶批量转换、去重与日历订阅的实用技巧5.1 一次转换整个学期的多份课表如果你有多个学期的 xlsx可以写一个批量入口遍历目录下所有 xlsx各自生成 ics再合并成一个总日历。合并时注意 UID 要加学期前缀否则跨学期同名课程会冲突。import glob from icalendar import Calendar def merge_ics(input_files, output_file): 把多个 ics 合并成一个UID 加文件前缀避免冲突 master Calendar() master.add(prodid, -//MySchedule//CN) master.add(version, 2.0) for path in input_files: prefix path.split(/)[-1].replace(.ics, ) with open(path, rb) as f: sub Calendar.from_ical(f.read()) for component in sub.walk(VEVENT): component[UID] f{prefix}-{component[UID]} master.add_component(component) with open(output_file, wb) as f: f.write(master.to_ical())逻辑说明Calendar.from_ical解析已有 icswalk(VEVENT)遍历所有事件给 UID 加前缀后塞进主日历。参数上input_files是 ics 路径列表output_file是合并结果。这个函数对每学期一个文件、想在一个日历里看全部的场景很实用。5.2 用 EXDATE 处理调休和停课遇到国庆调休或期中停课可以在对应 VEVENT 上加EXDATE排除特定日期。逐周展开的写法下直接不生成那一周的事件即可RRULE 写法下才需要 EXDATE。from datetime import datetime # 排除第 5 周假设因调休停课 ev.add(exdate, tz.localize(datetime(2025, 3, 24, 8, 0)))5.3 验证 ics 是否正确的三个检查点写完 ics 别急着导入先做三个检查。第一用文本编辑器搜索BEGIN:VEVENT和END:VEVENT数量是否相等。第二检查DTSTART和DTEND的时区参数是否都在。第三随机挑一条事件手动算一下日期是否落在正确的周次和星期上。我一般会写个校验脚本统计事件总数和预期课程数是否一致。from icalendar import Calendar with open(schedule.ics, rb) as f: cal Calendar.from_ical(f.read()) events list(cal.walk(VEVENT)) print(f事件总数: {len(events)}) for ev in events[:3]: print(ev[SUMMARY], ev[DTSTART].dt, ev[LOCATION])这段校验代码打印事件总数和前三条的摘要、开始时间、地点。如果总数对不上回去查周次解析如果时间偏移查时区如果地点为空查合并单元格映射。三个检查点走完基本能拦住 90% 的问题。从那以后我每次转换完课表都强制走一遍事件数核对 时区抽查 首条事件手动验算这三步再导入日历。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站