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