feat: 添加根据课表和行政历自动生成ICS日历文件的功能
This commit is contained in:
201
generate_ics.py
Normal file
201
generate_ics.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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()
|
||||||
1239
schedule.ics
Normal file
1239
schedule.ics
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user