Files
class-schedule/generate_ics.py

202 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import sys
import re
import argparse
from datetime import datetime, timedelta
import pandas as pd
def get_start_date_and_row(df_admin):
"""
在行政历中找到代表第一周的那一行,并解析出“第一周星期一”的日期。
"""
start_row = -1
for i in range(len(df_admin)):
val = str(df_admin.iloc[i, 0]).strip()
if val == '':
start_row = i
break
if start_row == -1:
raise Exception("无法在行政历中找到 '周次''' 的行")
# 在第一周这一行从第1列星期一到第7列星期日找到第一个带有完整日期的单元格
for j in range(1, 8):
cell = df_admin.iloc[start_row, j]
if pd.notna(cell):
if hasattr(cell, 'date'):
# 如果pandas解析为了datetime对象
date_val = cell.date()
return date_val - timedelta(days=j-1), start_row
elif isinstance(cell, str) and len(str(cell)) >= 10 and '-' in cell:
# 可能是形如 '2026-03-09' 的字符串
date_val = pd.to_datetime(cell[:10]).date()
return date_val - timedelta(days=j-1), start_row
raise Exception("在行政历的第一周中未找到明确的开学日期(带年月日格式的单元格)")
def is_holiday(df_admin, start_row, week_num, weekday_idx):
"""
检查某周某天是否为节假日(行政历的单元格内包含中文字符,如'4清明'
"""
row_idx = start_row + (week_num - 1)
if row_idx >= len(df_admin):
return False
col_idx = weekday_idx + 1 # 星期一从第1列开始
cell = df_admin.iloc[row_idx, col_idx]
if pd.isna(cell):
return False
cell_str = str(cell)
# 只要包含任意中文字符,就认为是节假日调休等特殊情况,跳过该日课程
if re.search(r'[\u4e00-\u9fa5]', cell_str):
return True
return False
def parse_schedule(df_schedule):
"""
解析课表,返回数组格式,每个元素表示一节课的基础信息
"""
col_names = list(df_schedule.columns)
schedule_data = []
for i in range(len(df_schedule)):
period_str = str(df_schedule.iloc[i, 0])
# 用正则提取例如 8:30~10:00 的时间段
match = re.search(r'(\d{1,2}:\d{2})\s*(?:~|-)\s*(\d{1,2}:\d{2})', period_str)
if not match:
continue
start_time_str = match.group(1).zfill(5)
end_time_str = match.group(2).zfill(5)
# 遍历星期一到星期五查找课程
for day_idx, day_name in enumerate(['星期一', '星期二', '星期三', '星期四', '星期五']):
# 动态获取星期的列索引
c_idx = -1
for idx, c in enumerate(col_names):
if day_name in str(c):
c_idx = idx
break
if c_idx == -1:
# 兼容处理:兜底默认索引
c_idx = day_idx + 1
cell = df_schedule.iloc[i, c_idx]
if pd.isna(cell):
continue
text = str(cell).strip()
if not text:
continue
# 以换行符切割 课程名称、班级、教室
lines = [l.strip() for l in text.split('\n') if l.strip()]
if not lines:
continue
title = lines[0]
# 中间的是描述(班级等),最后一行是地点
desc = " ".join(lines[1:-1]) if len(lines) >= 3 else ""
location = lines[-1] if len(lines) >= 2 else ""
schedule_data.append({
'weekday': day_idx,
'start_time': start_time_str,
'end_time': end_time_str,
'title': title,
'description': desc,
'location': location
})
return schedule_data
def generate_ics(start_date, start_row, df_admin, schedule_data, output_file, total_weeks=18):
"""
根据课表模板生成1-18周的日历事件跳过节假日
"""
ics_lines = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//Schedule Generator//CN",
"CALSCALE:GREGORIAN",
"X-WR-TIMEZONE:Asia/Shanghai" # 设置全局国内时区
]
# 转换为 ICS 所需的时间格式: 20260309T083000
def fmt_dt(d, time_str):
return d.strftime("%Y%m%d") + "T" + time_str.replace(":", "") + "00"
uid_counter = 1
for week in range(1, total_weeks + 1):
for course in schedule_data:
wd = course['weekday']
# 计算该周该天上课的具体日期
event_date = start_date + timedelta(weeks=week-1, days=wd)
# 若这天在行政历里有中文(比如 “清明”),则表明是节假日,跳过
if is_holiday(df_admin, start_row, week, wd):
continue
dtstart = fmt_dt(event_date, course['start_time'])
dtend = fmt_dt(event_date, course['end_time'])
now_dt = datetime.now().strftime("%Y%m%dT%H%M%SZ")
ics_lines.extend([
"BEGIN:VEVENT",
f"UID:schevt_{week}_{wd}_{uid_counter}@schedule",
f"DTSTAMP:{now_dt}",
f"DTSTART;TZID=Asia/Shanghai:{dtstart}",
f"DTEND;TZID=Asia/Shanghai:{dtend}",
f"SUMMARY:{course['title']}",
f"LOCATION:{course['location']}",
f"DESCRIPTION:{course['description']}",
"END:VEVENT"
])
uid_counter += 1
ics_lines.append("END:VCALENDAR")
with open(output_file, 'w', encoding='utf-8') as f:
# ICS格式规范要求每行以 \r\n 结束
f.write("\r\n".join(ics_lines) + "\r\n")
print(f"==> 成功生成日历文件: {output_file}")
print(f"==> 共生成了 {uid_counter - 1} 节日历事件(已自动跳过行政历上标注的节假日)。")
def main():
parser = argparse.ArgumentParser(description="根据课表和行政历生成 .ics 日历文件")
parser.add_argument('schedule_file', help="课表Excel文件路径 (如 课表.xlsx)")
parser.add_argument('admin_cal_file', help="行政历Excel文件路径 (如 行政历.xls)")
parser.add_argument('-o', '--output', default="schedule.ics", help="输出的日历文件名,默认为 schedule.ics")
parser.add_argument('-w', '--weeks', type=int, default=18, help="默认生成前18周可通过本参数调整如 16")
args = parser.parse_args()
try:
# 1. 读取行政历和课表
print(f"读取行政历:{args.admin_cal_file}")
df_admin = pd.read_excel(args.admin_cal_file)
print(f"读取课表:{args.schedule_file}")
df_schedule = pd.read_excel(args.schedule_file)
# 2. 找到开学日期
start_date, start_row = get_start_date_and_row(df_admin)
print(f"解析到 第 {args.weeks} 周的开课周期第1周星期一为 {start_date}")
# 3. 提取基础课表模板
schedule_data = parse_schedule(df_schedule)
print(f"成功萃取 {len(schedule_data)} 条基础周课表数据")
# 4. 生成 ICS 日历
generate_ics(start_date, start_row, df_admin, schedule_data, args.output, total_weeks=args.weeks)
except Exception as e:
print(f"发生错误: {e}")
sys.exit(1)
if __name__ == "__main__":
main()