介绍
本项目内容为使用Python +Selenium爬虫爬取基于青果教系的教务网络管理系统的学生成绩,学生想要查询该教务系统首先需要连接学校的校园网其次需要找到对应的网址最后需要登陆账号并输入验证码,非常的繁琐、恰好学习了Python爬虫所以萌发了运行代码一键爬成绩的想法。目前代码已经实现了基本查询保存功能。
相关库简介
Selenium
Selenium 是一个用于web应用程序自动化测试的工具,直接运行在浏览器当中,支持chrome、firefox等主流浏览器。可以通过代码控制与页面上元素进行交互(点击、输入等),也可以获取指定元素的内容。
为什么不是requests
requests和selenium区别还是很大的,前者模拟http协议得到数据,后者通过调用浏览器,虽说都能得到数据,但当requests遇到JS加密,它就无能为力了,它不是浏览器,没有解析的内核,所以requests只得被放弃。
分析下requests,当然selenium也有个致命的弱点——性能:当selenium使用浏览器模拟取得一条记录的时候,requests差不多已经取得了成千上万条数据。selenium是支持并发的,但它是高消耗应用,非常的耗资源,但是该项目仅仅只是抓少量的数据,倒不会有什么问题,事实上基于现在的“爬虫军队”越发庞大,导致网站不得不将强防御,自然selenium也就在它的防御范围之内了,意思是说,它是有缺陷的,而且特征非常的明显,并且,最重要一点,这个框架操作起来不太人性化,不够方便,相对而言,requests就简单多了,基本上提供请求的所有数据就能轻松完成数据获取。当然以上全部为个人观点。当然还捎带脚的用到了不少selenium库里的方法:譬如显式等待。
selenium库为本项目两个核心的之一
python3-PIL
使用pillow库是为了使用image方法,以截取屏幕中的验证码。
pyfiglet
使用pyfiglet库中的figlet_format,简单来说,就是可以使print打印出的字体看起来很不一样(类似字符画)
muggle_ocr
验证码问题。首先我想到一些常用的OCR库,但是识别效果都惨不忍睹。如果让用户手动输入验证码又觉得太过愚蠢。选题生死存亡之际,我找到了muggle_ocr这款本地化ocr模块。作为一个本地识别的模块,他的体积居然小于10MB,OCR模型仅为4MB,验证码模型仅为2MB,如此小的模型,随便找了几张验证码识别效果:8张验证码全部识别正确且用时仅10ms。目前正常使用打码成功率已经提升到了80% 。
项目核心之二自然就是muggle_ocr
time
time.sleep()为程序中添加等待时间,因为时间原因没能把所有的time.sleep()用selenium的显式等待替换掉。感兴趣的朋友可以全部替换掉,这样可以大大缩短程序的响应时间。提升运行效率。
库的下载
PS:除了muggle_ocr库不可以通过pip下载,其他所有库都可以通过pip install xxx的方式下载。
另外需要注意selenium库如果下载的是最新版,一些方法的用法会报错(但是不影响使用,可以把它当做警告来处理)。不想看见报错的强迫症可以指定下载版本为3.8.0的,具体喜爱在指令如下:
pip install selenium==3.8.0
,或者在相关的IDE内下载,譬如pycharm,再添加库的界面搜索selenium
,然后勾选special version
,之后选择3.8.0版本点击install即可。
muggle_ocr点此下载 密码:ocean
至于xxx.zip.gz如何安装到python库,我就不详细赘述了,直接百度即可
功能分析
(1)登录
我们想要进入教务网络管理系统页面,自首当其冲的目标是解决登录问题。那么我们打开学校的教务网络管理系统官网的“开发者工具”进行分析。观察登录模块的整体源代码,可以发现我们要解决的第一个问题:Frame子页面问题。因为账号登录的整个模块是囊括在Frame子页面中,所以直接获取输入框和按钮是获取不到的。这时,我们可以通过以下代码解决:browser = webdriver.Chrome(),它的作用是进入教务网的子Frame页面。
进入到教务网络管理系统的Frame子页面后,我们就可以直接获取到输入框和按钮了。
登录的具体操作是依靠selenium,这个库主要是用来模拟按键的。通过key对页面元素的一些操作,成功键入账号密码,剩下的验证码部分交给pil和muggle两兄弟去解决,PIL中的image可以对验证码的位置截图并保存到本地,muggle再读取该截图返还验证码的code值。即可实现成功登陆。
如果我们之前运行登陆教务系统成功多次后。那么再次输入验证码登录成功,之前在青果教务网络管理系统登录时的cookies还未过期。那么此时登录会弹出“上次登录已下线”的弹窗。这里我加了个if判断,出现就通过key操作把弹窗给解决掉。
(2)菜单
使用时通过pyfiglet库打印出来欢迎logo(可以自行更改),用户可以通过输入菜单对应的键值实现对应功能。1、成绩查询;2、校历查看;3、课表查询;4、退出。
(3)cjcx-成绩查询
通过浏览器打开开发者工具中的Network,点击“学生成绩”下的“查看成绩”选项,观察到新增加了Stu_MyScore.aspx,于是点击即可找到Request URL。这样,我们登录后只需请求该url就能够获取网页源代码并进行成绩的爬取。接下来就没什么好说的,不过就是通过Selenium进行页面元素的获取完成爬虫。唯一需要注意的是青果教务网络管理系统的学生成绩不是文字的,因此不能直接进行爬取。这里我们通过selenium的屏幕截图代码直接截图解决(brower.save_screenshot())。其他的一些模块如出一辙,无非就是一次次尝试找出包含的frame子页面aspx的名称。就不做一一介绍了。
综上项目模块图:
详细设计
模拟浏览器进行元素交互
该模块是该脚本的核心模块,它囊括了大部分功能模块。
该模块的核心方法就是webdriver,其中的get方式实现了访问教务系统,相关代码(不包含具体的import内容):
# 访问网页功能代码
browser = webdriver.Chrome()
url = "http://jwxt.学校英文缩写.edu.cn/jwweb/home.aspx"
browser.get(url)
其次是获取教务系统页面中具体标签对象并对其定位、修改、点击的实现,譬如find_element_by_id这个函数就是通过id查找页面中的元素,在其后再加上.click()就是找到这个元素并点击它的意思。相关代码:
# 对页面元素操作代码
browser.find_element_by_id('txt_asmcdefsddsd').send_keys(login)
browser.find_element_by_id('txt_asmcdefsddsd').send_keys(Keys.TAB) browser.find_element_by_id('txt_pewerwedsdfsdff').send_keys(password)
browser.find_element_by_id('txt_sdertfgsadscxcadsads').click()
当然想要运用它还需要配套的webdriver,至于如何配置webdriver,我做个简单的介绍:
首先:查看以Chrome为内核的浏览器它的Chrome内核版本为多少,然后到 https://npm.taobao.org/mirrors/chromedriver/ 找到对应Chrome内核版本的webdriver。如下图:找到对应版本下载对应系统的webdriver,Windows就下载chromedriver_win32.zip
然后:把下载好的zip打开,将里边的xx.exe分别放到,有Chrome内核的浏览器安装目录以及python的安装目录(注意是python的目录,而不是IDE比如pycharm的目录)。
推荐用谷歌浏览器。如果是其他浏览器,到时候会被ide抛出引出错误,因为上边的这句代码browser = webdriver.Chrome()
默认调用的是chrome.exe(也就是说谷歌浏览器的主程序)。如果非要用其他浏览器,则要学我这样:把360chrome.exe(假设你也是360极速浏览器)复制粘贴一遍,将粘贴好的重命名为chrome.exe
然后配置环境变量:右键点击我的电脑----->属性--->高级系统设置---->环境变量------>在path路径下添加上文中浏览器文件所在的根目录即可,如上图我就应该把D:\360\Exploer\360Chrome\Chrome\Application
新建添加到path中。
如此,所有步骤都已经完成。我们可以愉快地使用selenium了。
访问时间间隔以及显式等待模块
显式等待是代码中定义等待一定条件发生后再进一步执行你的代码。我用它来等待登陆页面中验证码的出现,以此配合PIL中的image方法来获取验证码的图片,并将之保存。介于教务系统的登陆只有通过代{过}{滤}理、或者校园网。因为我选择了通过校园网的网络环境爬取,而校园网的网速堪忧,所以在程序内部加入了不算少的时间间隔语句time.sleep()。相关代码如下:
# 显式等待代码
from selenium.webdriver.common.by import By #显式等待
from selenium.webdriver.support.ui import WebDriverWait #显式等待
from selenium.webdriver.support import expected_conditions as EC#显式等待
wait = WebDriverWait(browser, 20)
img = wait.until(EC.presence_of_element_located((By.ID, 'imgCode')))
登陆打码模块
验证模块通过第三方库muggle-ocr即可实现,首先导入muggle_ocr包,这个ocr包预置了两个模型:[ModelType.OCR, ModelType.Captcha],ModelType.OCR 用于识别普通印刷文本, ModelType.Captcha 用于识别4-6位简单英数验证码。所以初始化时我们选择后者即可,将验证码读入进行分析。分析后再利用time.time()时间戳的差计算出识别一个验证码的耗时。最后把对应的验证码返回到页面中的对应空位即可。相关代码:
# 显式等待代码
with open(r"check.jpg", "rb") as f:
captcha_bytes = f.read()
sdk = muggle_ocr.SDK(model_type=muggle_ocr.ModelType.Captcha)
st = time.time()
code = sdk.predict(image_bytes=captcha_bytes)
print(code, time.time() - st)
browser.find_element_by_id('txt_sdertfgsadscxcadsads').send_keys(code)
存储图片模块
想要存储验证码首先通过页面交互模块定位到验证码的位置,然后通过screenshot对整个页面截图。通过获得到的验证码的位置,用crop函数对整个页面的截图进行裁剪,于是我们能得到一张不多不少的验证码。然后通过PIL库中的核心方法Image对图片进行保存和打开。
相关代码如下(不包括对应的import内容):其中im.crop()里边的加减数值需要自己来调试更改,我学校的教务系统通过它验证码的id却定位不到具体的验证码的位置。
# 存储图片模块代码
image_file = "check.png"
screenshot = browser.save_screenshot(image_file)
wait = WebDriverWait(browser, 20)
img = wait.until(EC.presence_of_element_located((By.ID, 'imgCode')))
imgCode txt_sdertfgsadscxcadsads
time.sleep(1)
left = img.location['x']
top = img.location['y']
right = img.location['x'] + img.size['width']
bottom = img.location['y'] + img.size['height']
print("验证码位置:", top, bottom, left, right)
im = Image.open(image_file)
im = im.crop((left +10 , top + 126, right , bottom + 126))
im.save(image_file)
综上程序的流程图为
运行截图
可以看到登陆模块顺利打码:fh35 后边是耗时
功能界面,这里查询了某学期成绩:
尾声
这个爬虫系统还是比较简单的一个东西,还有很多更加实用的有益的功能没能添加进去。譬如每年一度的抢课,以后有时间一定会完善这个功能,放在云函数平台上托管定时运行抢课,虽然目前乃至以后都用不到这东西了。程序运行上也有不足,譬如因为时间仓促没能把所有的time.sleep()用显式等待替换掉,以提升程序响应、运行速度。最重要的是验证成功率还是没能做到百发百中以及没能把无头浏览完善。有时间一定完善该项目。
参考文献
[1]Hakutaku. https://www.52pojie.cn/thread-1234502-1-1.html
[2] Selenium-Python中文文档.https://selenium-python-zh.readthedocs.io/en/latest/
[3] Bieberg0n.jwwgl-ocr.https://github.com/bieberg0n/jwgl-ocr
[4] Muggle-ocr说明文档,在muggleocr的压缩包中。见上方muggleocr的下载链接。
[5] Leemboy.Python图像库PIL的类Image及其方法介绍.CSDN
[6]不能说的秘密.有趣的pyfiglet.https://www.cnblogs.com/yunhgu/p/13731365.html
源码(已尽可能注释)
import muggle_ocr
import pyfiglet
from selenium import webdriver
import time
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By #显式等待
from selenium.webdriver.support.ui import WebDriverWait #显式等待
from selenium.webdriver.support import expected_conditions as EC#显式等待
from selenium.webdriver.support.ui import Select # 下拉选择框
from PIL import Image
def Login():
with open('账号密码.txt', 'r') as file: # 账号和密码分别为一行
user = file.readlines() # 调用readlines()一次读取所有内容并按行返回list
login = user[0].strip() # 账号 strip把行尾的'\n'去掉
password = user[1].strip() # 密码
browser.switch_to.frame('frm_login') # 进入教务网的子Frame页面
browser.find_element_by_id('txt_asmcdefsddsd').send_keys(login)
browser.find_element_by_id('txt_asmcdefsddsd').send_keys(Keys.TAB) # TAB键是制表符,输入完账号后按下TAB跳至下一行密码输入框
browser.find_element_by_id('txt_pewerwedsdfsdff').send_keys(password)
browser.find_element_by_id('txt_sdertfgsadscxcadsads').click() # 点击验证码框使验证码显示出来
time.sleep(2)
# 验证码截图获取
image_file = "check.png"
screenshot = browser.save_screenshot(image_file) # 对整个屏幕截图,保存成验证码.jpg或png
wait = WebDriverWait(browser, 20) # 显式等待最长20秒
img = wait.until(EC.presence_of_element_located((By.ID, 'imgCode'))) # 定位图片位置 imgCode txt_sdertfgsadscxcadsads
time.sleep(1)
left = img.location['x'] # location返回验证码图片左上角的坐标 lacation 它是个字典{‘x’,'y'}
top = img.location['y']
right = img.location['x'] + img.size['width'] #size也是字典存储图片的宽高。两者结合即可得到验证码裁剪位置。
bottom = img.location['y'] + img.size['height']
print("验证码位置:", top, bottom, left, right)
im = Image.open(image_file)
im = im.crop((left +10 , top + 126, right , bottom + 126)) # 对屏幕截图进行裁剪,剪出当前验证码
im.save(image_file)# 保存截取后的验证码图片
im = Image.open("check.png") #将png转换为jpg。
rgb_im = im.convert('RGB') #将png转换为jpg。 利用PIL中的convert函数
rgb_im.save('check.jpg') #将png转换为jpg。
#im.show(image_file) # 显示图片
print("欢迎来到教务系统登录页面!")
# 打开验证码图片
with open("check.jpg", 'rb') as f:
captcha_bytes = f.read()
sdk = muggle_ocr.SDK(model_type=muggle_ocr.ModelType.Captcha)
st = time.time()
code = sdk.predict(image_bytes=captcha_bytes)
print(code, time.time() - st)
browser.find_element_by_id('txt_sdertfgsadscxcadsads').send_keys(code)
time.sleep(5)
browser.find_element_by_id('btn_login').click()
time.sleep(1)
# 判断是否出现弹窗,如果出现则进行捕获并点击确认
if browser.switch_to.alert:
get_window = browser.switch_to.alert # 捕获弹窗“您在别处的登录已下线”
time.sleep(1)
get_window.accept() # 点击确认按钮
print("登录成功,现在您已到达教务网络页面!")
def menu():
# 功能菜单!
banner = pyfiglet.figlet_format("你学校的英文缩写-jwxt")
print(banner)
print("\n欢迎使用教务系统辅助脚本!")
print(time.strftime('%Y-%m-%d', time.localtime(time.time())))
while True:
print("\n====== ★功能菜单★ ======\n")
print("1、成绩查询 输入:1")
print("2、校历查看 输入:2")
print("3、课表查询 输入:3")
print("4、退出脚本 输入:4")
gnxz = input("\n[jwxt]等待输入:")
if gnxz == '1':
cjcx()
if gnxz == '2':
ckxl()
if gnxz == '3':
kbcx()
if gnxz == '4':
browser.quit()
break
def cjcx():
url = url1 + "/xscj/Stu_MyScore.aspx"
browser.get(url) # 填写入高校的青果教务网络管理系统的学生成绩里面的ajax地址
time.sleep(5)
while True:
print("\n========== 欢迎使用成绩查询功能 ==========\n")
print("需要选择以下参数:")
print("入学以来请输入:rxylcjcx")
print("学年成绩请输入:xncjcx")
print("学期成绩请输入:xqcjcx")
print("退出功能请输入:q")
ckcj = input("\n[jwxt]等待输入:")
if ckcj == 'rxylcjcx':
# 入学以来成绩
# 只需要原始或有效选项
browser.find_element_by_xpath('//*[@id="SelXNXQ_0"]').click()
browser.find_element_by_id("yx_sj1").click()
browser.find_element_by_name("btn_search").click()
time.sleep(6)
browser.save_screenshot('capture.png')
elif ckcj == 'xncjcx':
# 学年查成绩
# 需要学年和有效成绩
browser.find_element_by_id("SelXNXQ_1").click()
xnxz = Select(browser.find_element_by_name('sel_xn'))
year = input("[jwxt]请输入学年,例:2019-2020输入2019:")
xnxz.select_by_value(year)
browser.find_element_by_id("yx_sj1").click()
browser.find_element_by_name("btn_search").click()
time.sleep(6)
browser.save_screenshot('capture.png')
elif ckcj == 'xqcjcx':
# 按照学期查成绩
# 需要学年、学期、有效成绩
browser.find_element_by_id("SelXNXQ_2").click()
xnxz = Select(browser.find_element_by_name('sel_xn'))
year = input("\n[jwxt]请先输入学年,例:2020-2021输入2020:")
xnxz.select_by_value(year)
xqxz = Select(browser.find_element_by_id('sel_xq'))
xueqi = input("\n[jwxt]第一学期输:0,第二学期输:1:")
xqxz.select_by_value(xueqi)
browser.find_element_by_id("yx_sj1").click()
browser.find_element_by_name("btn_search").click()
time.sleep(6)
browser.save_screenshot('capture.png')
elif ckcj == 'q':
url = url1 + "/MAINFRM.aspx"
browser.get(url)
break
def ckxl():
url = url1 + "/_data/index_lookxl.aspx"
browser.get(url)
time.sleep(2)
while True:
print("\n====== 欢迎使用校历查看功能 ======\n")
xlxn = Select(browser.find_element_by_name('sel_xnxq'))
print('请输入需要查看的学年:2020-2021为2020')
print('退出功能请输入:q')
year = input("\n[jwxt]等待输入:")
if year == 'q':
url = url1 + "/MAINFRM.aspx"
browser.get(url)
break
else:
year = year + '0'
xlxn.select_by_value(year)
browser.find_element_by_class_name("but20").click()
time.sleep(6)
browser.save_screenshot('capture.png')
def kbcx():
url = url1 + "/znpk/Pri_StuSel.aspx"
browser.get(url)
time.sleep(2)
while True:
print("\n====== 欢迎使用课表查询功能 ======\n")
xnxq = Select(browser.find_element_by_name('Sel_XNXQ'))
print('请输入需要查看的学年学期:2020第一学期:20200,第二学期:20201')
print('退出功能请输入:q')
year = input("\n[jwxt]等待输入:")
if year == 'q':
url = url1 + "/MAINFRM.aspx"
browser.get(url)
break
else:
xnxq.select_by_value(year)
browser.find_element_by_id("rad_gs1").click()
browser.find_element_by_name("btnSearch").click()
time.sleep(6)
browser.save_screenshot('课表.png')
if __name__ == '__main__':
browser = webdriver.Chrome()
url = "http://jwxt.你学校的英文缩写.edu.cn/jwweb/home.aspx"
browser.get(url)
url1 = "http://jwxt.你学校的英文缩写.edu.cn/jwweb"
time.sleep(2)
Login()
menu()
如何套用?
修改程序main方法的url为你学校教务系统对应的url
报错信息留言答复