Power by GeekHades

使用Python制作自己的课程提醒程序

0x1 背景

很快就要上课了,然而我却是一个不知道明天要上啥课的人。以前都是靠舍友提醒,现在自己出来住,那个超级课程表我又导入不了。在这个尴尬的气氛中我决定自己写程序上教务网上面爬我的课表下来,并且制作课表扫描程序提醒第二天需要上的课。这个过程中有一系列问题,例如:我们的教务系统更新过一次,估计是做了防爬虫。登陆的验证码需要点击才会出现,普通爬虫根本无法做到。之后使用selenium去模拟浏览行为,但是遇到验证码识别问题,教务系统用的验证码防破解度还是比较高的。使用tesseract还是比较难破解,可能还处于入门级吧。在这里希望有做过复杂验证码破解的gay友能够热情提供帮助。万分感谢。

教务系统的验证码: (PS: 由于我们学校的教务网经常崩溃导致获取验证码多数请求失败,然后会影响到我自己js的加载,因此这里就不post出来了。验证码就是4个数字和字母做了很大的混淆,脑补一下吧!)

此项目已经上传至我的GitHub中。有更好的建议欢迎联系我,联系方式:geekhades1@gmail.com

0x2 工具

  1. selenium 使用pip3 install selenium==3.9.0 安装即可 PS:使用selenium需要去加载浏览器的驱动,我项目中使用的是Chrome的驱动,如果想要使用其他驱动可以在这里下载

0x3 实现思路

因为无法实现自动识别验证码进行登陆,所以采取折中的方案。

  1. 使用selenium打开教务网登陆页面,然后手动输入验证码。账号密码之类的可以用ConfigParase获取使用send_key()填充
  2. 登陆之后就进入自动工作模式,选取需要抓取的学期,开始周数所对应的课程。我们学校用的是ajax异步加载json。连抓的功夫都省了。
  3. 抓取所有课程之后,另写一个程序对数据进行单独分析。

整个过程就意味着只有验证码那一步是需要人工输入之外,其他的都是自动执行。之后再创建定时任务,例如我是每天晚上10运行分析程序,它会在桌面创建文件来提醒我明天是否有课。(当然你可以选择你喜欢的提醒方式,我的桌面平时是没有东西的,所以这个文件就会很显眼)

0x4 多说无益 Show me the code

  1. 模拟登陆:(这里为了对学校的教务系统做一定的保护选择了配置文件读取服务器地址)
def login_page(driver):

    driver.get(BASE_URL+LOGIN_URL)

    # 读取用户名和密码
    username = config.get("user","username")
    password = config.get("user","password")

    # 输入用户名
    account = get_element(driver,"#account")
    account.send_keys(username)

    # 输入密码
    pswd = get_element(driver,"#password")
    pswd.send_keys(password)

    # 模拟用户点击验证码框,显示验证码
    verifyInput = get_element(driver,"#j_captcha")
    verifyInput.click()

    # 等待登陆成功后的页面元素出现
    try:
        element = WebDriverWait(driver, 100).until(
        EC.presence_of_element_located((By.CLASS_NAME, "c3"))
    )
        return True
    except:
        return False

这里的思路很清晰:就是等待用户输入验证码并且成功跳转之后就会出现相应的元素。如果登陆成功就返回True,失败就会重新加载登陆页面

  1. 查找课表,我们的教务网使用的是Ajax异步加载,所以普通的爬虫根本无法获取数据。查看控制台Networks发现加载的片段: 加载片段

该链接可以直接拼接请求参数获取相应内容。但是这样获取还是会获取不到元素。仔细查看会发现它的数据源: Json

存储数据:

# 遍历[start_week,end_week+1]中所有的数据
for week in range(start_week,end_week+1):
    driver.get(BASE_URL+KBURL.format(sem,week))
    save_curriculum(driver,week)

def save_curriculum(driver,week):
    # 获取Json
    k_json = driver.find_element_by_tag_name("body").text
    with open("json/week{}.json".format(week), "w",encoding="utf-8") as f:
        f.write(k_json)

数据已经拿到了,接下来就是数据分析神器Python的事了

  1. 分析数据,分析数据我们要明确一个点是根据给定时间(例如我就是给第二天的时间)去判定是否属于给定的周。如果在给定周范围内那么就可以直接读取相应课程,如果不是那么就要根据每个week?.json中的时间区间去判定。

  2. 时间判定:

def checkWeek(week):
    """检查读取星期是否合法,并且更新配置文件"""
    t = datetime.now()
    now_time = TIME_FORMAT.format(t.year,t.month,t.day)
    # 与json中的时间格式保持一致
    now_time = datetime.strptime(now_time,"%Y-%m-%d")

    config = ConfigParser()
    config.read("date.config")
    end_time = config.get("date","end_time")
    end_time = datetime.strptime(end_time,"%Y-%m-%d")


    if end_time.timestamp() > now_time.timestamp():
        return True
    else:
        UpateConfig(now_time,week)
        return False

为了节省运行时间,采取配置文件记录上一次读取的周数以及最长时间。如果超过了就代表需要更新

  • 更新函数:
def UpateConfig(now_time, week):
    """更新配置文件的时间"""
    print(">>> 更新配置文件中...")
    end_time = None
    for w in range(week, 20):
        with open(FILE_NAME.format(w), "r") as f:
            data = json.load(f)
            end_time = datetime.strptime(data[1][6]["rq"],"%Y-%m-%d")
            if end_time.timestamp() > now_time.timestamp():
                week = w;
                break;
    writeConfig(str(week),TIME_FORMAT.format(end_time.year,end_time.month,end_time.day))
    print(">>> 当前周更换至 第 %d 周" % (week))
    print(">>> OK!")
  • 获取对应时间的课程内容。并且按照上课时间进行排序(因为数据是乱序的)
def read_class(week, time):
    """获取对应周数的,对应时间的课程"""
    # 当前日期
    targettime = datetime.strptime(time,"%Y-%m-%d")
    data = None
    xq = 1
    curis = []

    # 课程次序排序
    time_key = {
        "01,02": 1,
        "03,04": 2,
        "05,06": 3,
        "07,08": 4,
        "09,10": 5,
        "11,12": 6
    }

    with open(FILE_NAME.format(week), "r") as f:
        data = json.load(f)

    for di in data[1]:
        tdate = datetime.strptime(di["rq"],"%Y-%m-%d")
        if targettime.timestamp() == tdate.timestamp():
            xq = di["xqmc"]
            break;

    for di in data[0]:
        if di["xq"] == xq:
            tmp = {}
            tmp["课程名称"] = di["kcmc"]
            tmp["任课老师"] = di["teaxms"]
            tmp["上课时间"] = di["jcdm2"]
            tmp["教室"] = di["jxcdmc"]
            tmp["key"] = time_key[di["jcdm2"]]
            curis.append(tmp)
    if curis:
        curis.sort(key=lambda a: a["key"])

    notie(curis)
  • 创建提醒文件
def notie(curis):
    """提醒上课"""
    if curis:
        with open("/path/to/Desktop/兄弟明天有课!快看.txt", "w") as f:
            for di in curis:
                for index,key in enumerate(di):
                    # index=4 是排序权重。可以忽略
                    if index < 4:                  
                        f.writelines(key + ":" + di[key] + "\n")
                f.writelines("\n")
    else:
        with open("/path/to/Desktop/明天没课放心浪.txt", "w") as f:
            pass

0x5 结果

  1. 假定查询时间是 2018-3-4 星期日 也就是明天 无课

  2. 假定查询时间是 2018-3-5 星期一 有课

Bingo!目的达到了。但是程序的健壮性并不算很高。还需要面对许多问题。 程序的改造性也比较高,如果有更好的设计方法的请联系我。万分感谢!

最后祝大家学习进步、工作顺利、身体健康



* 如果你对文章有任何意见或建议请发 邮件 给我!
* if you have any suggestion that you could send a E-mail to me, Please!