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()