编辑代码

# - *- coding: utf-8 -* -
import requests
from bs4 import BeautifulSoup
from icalendar import Calendar, Event
from datetime import datetime, timedelta
import pytz
import re
# 配置区(需要用户自行修改)=============================================
CONFIG = {
    "BASE_URL": "http://jwxt.xjau.edu.cn",  # 教务系统地址
    "LOGIN_URL": "/login",                  # 登录接口路径
    "SCHEDULE_URL": "/student/schedule",    # 课表页面路径
    "USERNAME": "你的学号",
    "PASSWORD": "你的密码",
    "TIMEZONE": pytz.timezone('Asia/Shanghai'),  # 时区设置
    # 时间表配置(需要根据实际课程时间修改)
    "TIME_TABLE": {
        1: ("08:30", "10:05"),   # 第1节课
        2: ("10:25", "12:00"),   # 第2节课
        3: ("12:30", "14:05"),   # 第3节课(午间)
        4: ("15:30", "17:05"),   # 第4节课
        5: ("19:30", "21:05")    # 第5节课(晚间)
    }
}

# 核心功能类============================================================
class XJAU_Schedule:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })

    def login(self):
        """处理登录流程(注意:需要自行处理验证码)"""
        try:
            # 先获取登录页面获取token(示例)
            login_page = self.session.get(CONFIG["BASE_URL"] + CONFIG["LOGIN_URL"])
            soup = BeautifulSoup(login_page.text, 'html.parser')
            token = soup.find('input', {'name': '_token'})['value']

            # 构造登录数据(根据实际表单结构调整)
            login_data = {
                '_token': token,
                'username': CONFIG["USERNAME"],
                'password': CONFIG["PASSWORD"],
                'captcha': '自行处理验证码'  # 需要OCR或手动输入
            }
            response = self.session.post(
                CONFIG["BASE_URL"] + CONFIG["LOGIN_URL"],
                data=login_data
            )
            if '登录失败' in response.text:
                raise Exception("登录失败,请检查账号密码或验证码")
            return True
        except Exception as e:
            print(f"登录异常: {str(e)}")
            return False

    def fetch_schedule(self):
        """获取课表数据"""
        try:
            response = self.session.get(CONFIG["BASE_URL"] + CONFIG["SCHEDULE_URL"])
            response.encoding = 'utf-8'
            return self.parse_html(response.text)
        except Exception as e:
            print(f"获取课表失败: {str(e)}")
            return []

    def parse_html(self, html):
        """解析课表页面"""
        soup = BeautifulSoup(html, 'html.parser')
        courses = []

        # 示例解析逻辑(需要根据实际页面结构调整CSS选择器)
        for row in soup.select('#kbtable tr'):
            cols = row.select('td')
            if len(cols) >= 6:
                day = int(re.search(r'\d', cols[0].text).group())
                for i in range(1, 6):  # 假设1-5节
                    if cols[i].text.strip():
                        course_info = self.parse_course_text(cols[i].text)
                        courses.append({
                            'name': course_info['name'],
                            'day': day,
                            'section': i,
                            'weeks': course_info['weeks'],
                            'location': course_info['location']
                        })
        return courses

    def parse_course_text(self, text):
        """解析课程单元格文本"""
        # 示例格式: "高等数学\n1-16周\n三号教学楼205"
        parts = [p.strip() for p in text.split('\n') if p.strip()]
        return {
            'name': parts[0],
            'weeks': self.parse_weeks(parts[1]),
            'location': parts[2] if len(parts)>2 else ""
        }

    def parse_weeks(self, week_str):
        """解析周次信息"""
        # 示例格式: "1-16周" 或 "1,3,5-8周"
        weeks = []
        for part in week_str.replace('周', '').split(','):
            if '-' in part:
                start, end = map(int, part.split('-'))
                weeks.extend(range(start, end+1))
            else:
                weeks.append(int(part))
        return weeks

    def generate_ics(self, courses):
        """生成iCalendar文件"""
        cal = Calendar()
        cal.add('prodid', '-//XJAU Schedule//mxm.dk//')
        cal.add('version', '2.0')

        for course in courses:
            for week in course['weeks']:
                # 计算具体日期(假设2025年春季学期从2025-03-24开始)
                base_date = datetime(2025, 3, 24)  # 需要调整实际开学日期
                target_date = base_date + timedelta(
                    weeks=(week-1),
                    days=(course['day']-1)
                )
                # 转换时间
                start_time = datetime.strptime(
                    CONFIG["TIME_TABLE"][course['section']][0], "%H:%M"
                ).time()
                end_time = datetime.strptime(
                    CONFIG["TIME_TABLE"][course['section']][1], "%H:%M"
                ).time()

                event = Event()
                event.add('summary', f"{course['name']} @ {course['location']}")
                event.add('dtstart', CONFIG["TIMEZONE"].localize(
                    datetime.combine(target_date, end_time)
                ))
                event.add('location', course['location'])
                cal.add_component(event)

        with open('xjau_schedule.ics', 'wb') as f:
            f.write(cal.to_ical())
                    print("课表文件已生成: xjau_schedule.ics")

# 执行流程==============================================================
if __name__ == "__main__":
    print("新疆农业大学课表转换工具")
    print("正在初始化...")
    tool = XJAU_Schedule()

    if tool.login():
        print("登录成功!")
        courses = tool.fetch_schedule()
        if courses:
            print(f"成功获取{len(courses)}条课程记录")
            tool.generate_ics(courses)
            print("请将生成的 xjau_schedule.ics 通过小米运动APP导入小爱同学")
        else:
            print("未获取到课程数据,请检查页面解析逻辑")
    else:
        print("登录失败,请检查网络或配置")
# 在login()方法中添加:
def handle_captcha(self):
    # 方案1:人工识别
    captcha_url = CONFIG["BASE_URL"] + "/captcha"
    response = self.session.get(captcha_url)
    with open("captcha.jpg", "wb") as f:
        f.write(response.content)
    return input("请输入验证码(查看captcha.jpg):")

    # 方案2:使用第三方OCR API
    # 示例(需注册平台获取API_KEY):
    # import ddddocr
    # ocr = ddddocr.DdddOcr()
    # return ocr.classification(response.content)