0x1 背景
很快就要上课了, 然而我却是一个不知道明天要上啥课的人以前都是靠舍友提醒, 现在自己出来住, 那个超级课程表我又导入不了在这个尴尬的气氛中我决定自己写程序上教务网上面爬我的课表下来, 并且制作课表扫描程序提醒第二天需要上的课这个过程中有一系列问题, 例如: 我们的教务系统更新过一次, 估计是做了防爬虫登陆的验证码需要点击才会出现, 普通爬虫根本无法做到之后使用 selenium 去模拟浏览行为, 但是遇到验证码识别问题, 教务系统用的验证码防破解度还是比较高的使用 tesseract 还是比较难破解, 可能还处于入门级吧在这里希望有做过复杂验证码破解的 gay 友能够热情提供帮助万分感谢
教务系统的验证码:
此项目已经上传至我的 GitHub 中有更好的建议欢迎联系我, 联系方式: geekhades1@gmail.com
0x2 工具
selenium 使用
pip3 install selenium==3.9.0
安装即可 PS: 使用 selenium 需要去加载浏览器的驱动, 我项目中使用的是 Chrome 的驱动, 如果想要使用其他驱动可以在这里下载
0x3 实现思路
因为无法实现自动识别验证码进行登陆, 所以采取折中的方案
使用 selenium 打开教务网登陆页面, 然后手动输入验证码账号密码之类的可以用 ConfigParase 获取使用 send_key()填充
登陆之后就进入自动工作模式, 选取需要抓取的学期, 开始周数所对应的课程我们学校用的是 ajax 异步加载 json 连抓的功夫都省了
抓取所有课程之后, 另写一个程序对数据进行单独分析
整个过程就意味着只有验证码那一步是需要人工输入之外, 其他的都是自动执行之后再创建定时任务, 例如我是每天晚上 10 运行分析程序, 它会在桌面创建文件来提醒我明天是否有课(当然你可以选择你喜欢的提醒方式, 我的桌面平时是没有东西的, 所以这个文件就会很显眼)
0x4 多说无益 Show me the code
模拟登陆:(这里为了对学校的教务系统做一定的保护选择了配置文件读取服务器地址)
- 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, 失败就会重新加载登陆页面
查找课表, 我们的教务网使用的是 Ajax 异步加载, 所以普通的爬虫根本无法获取数据查看控制台 Networks 发现加载的片段:
该链接可以直接拼接请求参数获取相应内容但是这样获取还是会获取不到元素仔细查看会发现它的数据源:
存储数据:
- # 遍历 [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 的事了
分析数据, 分析数据我们要明确一个点是根据给定时间 (例如我就是给第二天的时间) 去判定是否属于给定的周如果在给定周范围内那么就可以直接读取相应课程, 如果不是那么就要根据每个 week?.json 中的时间区间去判定
时间判定:
- 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 结果
假定查询时间是 2018-3-4 星期日 也就是明天
假定查询时间是 2018-3-5 星期一
Bingo! 目的达到了但是程序的健壮性并不算很高还需要面对许多问题 程序的改造性也比较高, 如果有更好的设计方法的请联系我万分感谢!
最后祝大家学习进步工作顺利身体健康
来源: https://juejin.im/entry/5a9ad80851882555731b9172