Java爬虫爬取旧版正方教务系统课程表、成绩表

一、项目展示

1.正方教务系统

  • 首页

2.爬虫系统

  • 首页:

  • 成绩查询:

  • 课表查询:

二、项目实现

1.爬取思路描述

无论是成绩查询或课表查询亦或者其它的信息查询,都必须是要在登录状态下才能进行。而要登录教务系统,就要先获取登录的验证码,然后输入学号密码和验证码,向教务系统发起登录请求,登录成功后,需要保存登录状态,即记录cookie。有了登录成功后的cookie,就能对其他页面发起请求,旧版的正方系统返回的是Html,所以拿到请求结果后,还要再进行Html的解析,进而筛选出自己所需要的信息。

2.代码实现的总体思路

(1)爬虫、数据解析工具

  • HttpClient:用于像浏览器发起http请求,支持长连接
  • Jsoup:用于解析Html,支持DOM,CSS以及类似于jQuery的操作方法来取出和操作数据
  • 正则表达式:按需求提取字符串的特定部分,也是用于解析Html

(2)项目框架

项目用的是Springboot搭建项目,因为当时简单用Vue搭了个前台,所以数据传输都是用的Json,实现了前后端的分离,主要是用到了Spring的IoC容器管理bean还有控制器类。第三方依赖是用Maven来管理。实际上,这个项目不一定要用SpringBoot,可以根据自己的需要进行迁移。代码包结构如下:

(3)核心类简介

  • GloabalConstant类:全局常量类,存放了所有的请求URL,包括教务系统首页、登录请求地址、验证码请求地址等,这些URL需要根据自己的实际情况进行手动更改,把域名部分换成自己学校正方系统首页的地址就行。另外就是登录页的错误信息,为了方便调试代码,也进行了保存。
  • HttpService类:Http服务类,封装了get请求、post请求,以及HttpClient的初始化,同时所有关于爬取逻辑的代码都是在这个类里,包括登录、验证码获取与识别、课表表获取、成绩表获取等。
  • JavaOCR类:验证码识别类,包括验证码识别的整个过程,**由于验证码识别训练涉及到数据集、测试集、结果集,启动代码时,请根据自己的实际情况,在配置文件执行修改trainSetDirtrainTestDirtrainResultDir这几个目录所在的位置。**验证码识别的训练与使用是分开的,项目运行时只会在HttpService中读取训练结果集,如果要自己进行验证码的训练(理论上测试集验证码图片越多,识别率越高,我总共用了近700张,识别率稳定在62%左右),在src.test.java.*下有代码示例。

(4)配置文件说明

配置文件用的是yml格式,application-dev.yml是开发环境的配置文件,application-prod.yml是生产环境(linux下)的配置文件,可以自定义端口以及JavaOCR目录。

(5)要注意的细节

  • 在获取Cookies后,以后的每一次请求都要把Cookies带上。

  • 请求时要注意目标请求是否需要Referer。Referer告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理,有网页会限定请求的上一个地址。

3.模拟登录

(1)分析登录页面

我用的Google Chorm,在首页按F12打开浏览器自带的页面审查工具,随便输入学号密码和验证码,点击登录后,浏览器会向服务器提交一个post请求,请求地址为:http://xxxxxxxxxx/default2.aspx。

仔细观察上面的Form Data表单,发现有以下几个关键表单项:

  • __VIEWSTATE:一个隐藏表单项,可以在页面源码中找到
  • txtUserName:学生学号
  • TextBox2:登录密码
  • txtSecretCode:验证码
  • RadioButtonList1:结合登陆页面知,这是身份选项,value值为%D1%A7%C9%FA(”学生”经过以Gb2312格式URL编码后的字符串 )

其他像Textbox1、Button1这些表单项的value值都是空白的,说明在登录中并不起作用。

(2)登陆前的准备:获取cookie和__VIEWSTATE

获取到cookie和__VIEWSTATE后要进行保存,项目中是采用session的方式,存放在服务器端,在之后的请求中,每次请求都要带上cookie,比如获取验证码。HttpService类已经封装好了get请求和post请求,每次请求都会自动带上cookie。

 /*** 初始化,主要用于收集cookie和viewState*/public HttpBean init() {CloseableHttpResponse requestResponse = sendGetRequest(GlobalConstant.INDEX_URL, "");String cookie = requestResponse.getFirstHeader("Set-Cookie").getValue();//  获取cookieHttpBean httpBean = new HttpBean();try {String html = EntityUtils.toString(requestResponse.getEntity(), "utf-8");httpBean.setViewState(getViewState(html));//提取页面表单中的__VIEWSTATE的值httpBean.setCookie(cookie);} catch (IOException e) {e.printStackTrace();}System.out.println("完成初始化,获取到的cookie为" + httpBean.getCookie()+ ",获取到的viewState为" + httpBean.getViewState());return httpBean;}/*** @param html 登录页面源码* @return 登录页的__VIEWSTATE*/public String getViewState(String html) {return Jsoup.parse(html).select("input[name=__VIEWSTATE]").val();}

(3)验证码的获取与自动识别

     /*** 获取验证码** @return 验证码图片*/public byte[] getCheckImg() {String url = GlobalConstant.SECRETCODE_URL;byte[] imgByte = null;try {CloseableHttpResponse requestResponse = sendGetRequest(url, "");imgByte = EntityUtils.toByteArray(requestResponse.getEntity());} catch (Exception e) {e.printStackTrace();}return imgByte;}/*** * @return 验证码识别结果*/public String getCheckImgText() {String ocrResult = "";try {BufferedImage image = ImageIO.read(new ByteArrayInputStream(getCheckImg()));BufferedImage imageBinary = javaOCR.getImgBinary(image);ocrResult = javaOCR.getOcrResult(imageBinary, map);ImageIO.write(image, "png", new File(trainRecordDir + ocrResult + ".png"));} catch (IOException e) {e.printStackTrace();}return ocrResult;}

(4)发起模拟登录请求

 /*** 登陆** @param user 用户信息* @return 返回登陆成功或登录错误信息*/public String login(User user) {HttpSession session = request.getSession();// 初始化HttpBean httpBean = init();// 将信息保存进新创建的session中session.setAttribute("httpBean", httpBean);// 组织登陆请求参数ArrayList<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();params.add(new BasicNameValuePair("__VIEWSTATE", httpBean.getViewState()));//__VIEWSTATE,不可缺少这个参数params.add(new BasicNameValuePair("txtUserName", user.getUserNumber()));//学号params.add(new BasicNameValuePair("TextBox1", ""));//密码params.add(new BasicNameValuePair("TextBox2", user.getUserPassword()));//密码params.add(new BasicNameValuePair("txtSecretCode", getCheckImgText()));//验证码params.add(new BasicNameValuePair("RadioButtonList1", "学生"));//登陆用户类型params.add(new BasicNameValuePair("Button1", ""));params.add(new BasicNameValuePair("lbLanguage", ""));params.add(new BasicNameValuePair("hidPdrs", ""));params.add(new BasicNameValuePair("hidsc", ""));String loginErrorMsg = "no error";try {UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, "GB2312"); //封装成参数对象CloseableHttpResponse requestResponse = sendPostRequest(GlobalConstant.LOGIN_URL, null, entity);//发送请求String html = EntityUtils.toString(requestResponse.getEntity(), "utf-8");// 检测是否有登陆错误的信息,有则记录信息,若返回的状态码是302则表示登陆成功if (html.contains(GlobalConstant.CHECKCODE_ERROR)) {loginErrorMsg = GlobalConstant.CHECKCODE_ERROR;} else if (html.contains(GlobalConstant.CHECKCODE_NULL)) {loginErrorMsg = GlobalConstant.CHECKCODE_NULL;} else if (html.contains(GlobalConstant.PASSWORD_ERROR)) {loginErrorMsg = GlobalConstant.PASSWORD_ERROR;} else if (html.contains(GlobalConstant.USERNUMBER_NULL)) {loginErrorMsg = GlobalConstant.USERNUMBER_NULL;} else if (html.contains(GlobalConstant.USERNUMBER_ERROR)) {loginErrorMsg = GlobalConstant.USERNUMBER_ERROR;} else if (requestResponse.getStatusLine().getStatusCode() == 302) {// 登陆成功,保存已登录的用户的信息httpBean.setUser(user);// 保存主页面的查询链接httpBean = saveQueryURL(httpBean);// 更新session中的信息session.setAttribute("httpBean", httpBean);return "登录成功";// 返回登陆成功信息} else {loginErrorMsg = "未知错误";}} catch (IOException e) {e.printStackTrace();}return loginErrorMsg;}

(5)登录成功后,爬取主页面内容,查找并保存查询各种信息的URL

    /*** 访问系统首页,查找并保存查询各种信息的URL** @param httpBean*/public HttpBean saveQueryURL(HttpBean httpBean) throws IOException {CloseableHttpResponse response = sendGetRequest(GlobalConstant.MAIN_URL + httpBean.getUser().getUserNumber(),GlobalConstant.LOGIN_URL);String html = EntityUtils.toString(response.getEntity(), "utf-8");// 信息查询的URLString regex_url = "<a href=\"(\\w+)\\.aspx\\?xh=(\\d+)&xm=(.+?)&gnmkdm=N(\\d+)\" target='zhuti' οnclick=\"GetMc\\('(.+?)'\\);\">(.+?)</a>";// 提取URL中的姓名String regex_name = "&xm=(\\S+)&";Pattern pattern1 = Pattern.compile(regex_url);Pattern pattern2 = Pattern.compile(regex_name);Matcher matcher = pattern1.matcher(html);while (matcher.find()) {// <a href="xskbcx.aspx?xh=xxxxxxxxx&xm=某某某&gnmkdm=N121603" target='zhuti' οnclick="GetMc('学生个人课表');">学生个人课表</a>String res = matcher.group();// xskbcx.aspx?xh=xxxxxxxxx&xm=某某某&gnmkdm=N121603" target='zhuti' οnclick="GetMc('学生个人课表');">学生个人课表</a>String url = res.substring(res.indexOf("href=\"") + 6);// xskbcx.aspx?xh=xxxxxxxxx&xm=某某某&gnmkdm=N121603url = url.substring(0, url.indexOf("\""));// 姓名为中文,需要进行编码 URLEncoder.encode(userName, "GB2312")Matcher matcher2 = pattern2.matcher(url);if (matcher2.find()) {url = url.replaceAll(regex_name, "&xm=" + URLEncoder.encode(matcher2.group(1)) + "&");if (StringUtils.isEmpty(httpBean.getUser().getUserName()))httpBean.getUser().setUserName(matcher2.group(1));}if (res.contains("学生个人课表")) {httpBean.setQueryStuCourseListUrl(url);continue;}/*  有两种成绩查询,名称相同,但实际URL不同xscjcx_dq.aspx?xh=xxxxxxxxx&xm=%DD%B6%CE%B0%BD%DC&gnmkdm=N121617xscjcx.aspx?xh=xxxxxxxxx&xm=%DD%B6%CE%B0%BD%DC&gnmkdm=N121618*/if (res.contains("成绩查询") && res.contains("N121617")) {httpBean.setQueryStuScoreListUrl(url);}if (res.contains("成绩查询") && res.contains("N121618")) {httpBean.setQueryStuScoreListUrl2(url);}}return httpBean;}

4.以爬取课表信息为例

可以按照前面的分析登录页面那样,来分析查询课表页面。正方教务系统,查询当前学期的课表时,发送的是Get请求,这时不需要填写表单数据。当指定查询某个学年或某个学期的课表时,发送的就是post请求了,这时要携带上表单数据。同时需要注意的就是,每个页面都会有自己的__VIEWSTATE值,在爬取一个页面时,要相应的更新Session中的VIEWSTATE 值为当前页面的VIEWSTATE值。

 /*** __VIEWSTATE字段不能和查询的学期相同* 查询非本学期的课程时,用post方法* 查询本学期的课程时,用get方法** @param xn* @param xq* @throws IOException*/public ArrayList<CourseBean> queryStuCourseList(String xn, String xq) {HttpSession session = request.getSession();HttpBean httpBean = (HttpBean) session.getAttribute("httpBean");String queryCourseUrl = GlobalConstant.INDEX_URL + httpBean.getQueryStuCourseListUrl();CloseableHttpResponse requestResponse = null;//没有学年度和学期的的信息,则发送get请求,否则发送post请求if (xn == null || xq == null) {requestResponse = sendGetRequest(queryCourseUrl, GlobalConstant.MAIN_URL + httpBean.getUser().getUserNumber());} else {List<NameValuePair> courseForms = new ArrayList<>();courseForms.add(new BasicNameValuePair("__EVENTTARGET", ""));courseForms.add(new BasicNameValuePair("__EVENTARGUMENT", ""));courseForms.add(new BasicNameValuePair("__VIEWSTATE", httpBean.getViewState()));courseForms.add(new BasicNameValuePair("xnd", xn));courseForms.add(new BasicNameValuePair("xqd", xq));try {requestResponse = sendPostRequest(queryCourseUrl, queryCourseUrl, new UrlEncodedFormEntity(courseForms, "utf-8"));} catch (UnsupportedEncodingException e) {e.printStackTrace();}}String courseListSourceCode = null;try {courseListSourceCode = EntityUtils.toString(requestResponse.getEntity(), "utf-8");} catch (IOException e) {e.printStackTrace();}// 更新__VIEWSTATE值httpBean.setViewState(getViewState(courseListSourceCode));// 更新session中的信息session.setAttribute("httpBean", httpBean);// 解析HTMLreturn ParseUtil.parseCourseTableHtml(courseListSourceCode);}

5.项目开源

(1)Github项目地址

  • 项目地址:https://github.com/James0608/ZhengFangJWSystemBackend

    欢迎Fork,喜欢的话,给个Star呗 hiahia~

(2)如何启动项目?

  • 请先阅读代码实现的总体思路
  • windows用户,在D盘创建image目录,然后把项目src/resource/ocr/目录下的train_set(数据集)、train_test(测试集)、train_result(结果集)、record(每次登录记录验证码识别结果)这四个文件夹复制到image下,这样子就不用修改application-dev.yml。反过来,也可以通过修改配置文件来自定义加载路径。Linux用户请参考application-prod.yml配置文件的路径来创建。
  • 更改GlobalConstant类下的URL为自己学校正方教务管理系统的地址,一般是只需要更改域名部分,后面的子路径即使是不同学校也不会有变化。

(3)项目无法启动怎么办?

  • 请检查是否是路径错误,是否已经正确的按要求创建了所需要的目录

  • 请检查GlobalConstant类下的URL与教务系统上的请求URL是否一致

  • 请检查正方教务管理系统FormData(post请求的body)的key是否与项目代码中的一致

    不同学校的系统,可能在表单参数的名称上有所差异,请根据自己的实际情况更改HttpService类里对应的代码。

  • 可以在Github上提issiue,也可以直接到博客文章下进行评论,详细描述错误现象,错误是否可重现等。

(4)特别鸣谢

  • 本项目的验证码识别部分是在Allenhua的自动识别验证码项目的基础上完成的,特此鸣谢。

  • 参考文章:

    [1]:用java模拟登录正方教务系统,抓取课表和个人成绩等数据

    [2]:爬取正方教务管理系统获取学生信息

感谢为开源工作做出奉献的每一个开发者,开源意味着更多的交流机会和学习机会,同样希望自己这个项目能帮到有需要的人。

我的第一个开源项目:Java爬虫爬取旧版正方教务系统课程表、成绩表相关推荐

  1. Java爬虫 爬取某招聘网站招聘信息

    Java爬虫 爬取某招聘网站招聘信息 一.系统介绍 二.功能展示 1.需求爬取的网站内容 2.实现流程 2.1数据采集 2.2页面解析 2.3数据存储 三.获取源码 一.系统介绍 系统主要功能:本项目 ...

  2. Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情

    Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情 先识别商品url,区分平台提取商品编号,再根据平台带着商品编号爬取数据. 1.导包 <!-- 爬虫相关Jar包依赖 --><d ...

  3. python java 爬数据_如何用java爬虫爬取网页上的数据

    当我们使用浏览器处理网页的时候,有时候是不需要浏览的,例如使用PhantomJS适用于无头浏览器,进行爬取网页数据操作.最近在进行java爬虫学习的小伙伴们有没有想过如何爬取js生成的网络页面吗?别急 ...

  4. Java爬虫 --- 爬取王者荣耀英雄图片

    Java爬虫 - 爬取王者荣耀英雄图片 import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Docu ...

  5. java爬虫爬取笔趣阁小说

    java爬虫爬取笔趣阁小说 package novelCrawler;import org.jsoup.Connection; import org.jsoup.HttpStatusException ...

  6. Java爬虫爬取wallhaven的图片

    Java爬虫爬取wallhaven的图片 参考文章:JAVA Jsoup爬取网页图片下载到本地 需要的jar包:jsuop wallhaven网站拒绝java程序访问,所以要伪装报头. 发送请求时 C ...

  7. Python爬虫开源项目代码(爬取微信、淘宝、豆瓣、知乎、新浪微博、QQ、去哪网 等等)...

    文章目录 1.简介 2.开源项目Github 2.1.WechatSogou [1]– 微信公众号爬虫 2.2.DouBanSpider [2]– 豆瓣读书爬虫 2.3.zhihu_spider [3 ...

  8. Python爬虫开源项目代码(爬取微信、淘宝、豆瓣、知乎、新浪微博、QQ、去哪网 等等)

    文章目录 1.简介 2.开源项目Github 2.1.WechatSogou [1]– 微信公众号爬虫 2.2.DouBanSpider [2]– 豆瓣读书爬虫 2.3.zhihu_spider [3 ...

  9. 23个Python爬虫开源项目代码:爬取微信、淘宝、豆瓣、知乎、微博

    今天为大家整理了32个Python爬虫项目.整理的原因是,爬虫入门简单快速,也非常适合新入门的小伙伴培养信心,所有链接指向GitHub. 1.WechatSogou – 微信公众号爬虫 基于搜狗微信搜 ...

最新文章

  1. mysql通过查看跟踪日志跟踪执行的sql语句
  2. 如何用python实现邮箱发送信息
  3. Git使用教程之本地仓库的基本操作
  4. Python从菜鸟到高手(4):导入Python模块
  5. C#中Lock关键字的使用
  6. 经典SQL语句大全(技巧篇)
  7. gui界面怎么分页_什么是用户界面和体验设计
  8. java中的Sort函数,你值得看
  9. Python PIL库处理图片常用操作,图像识别数据增强的方法
  10. get、post请求参数乱码解决方法(qq:1324981084)
  11. b宝塔 centos端口更改_centos修改ssh默认端口号的方法示例
  12. plsql查询数据显示为乱码解决方案
  13. 迅雷Bolt界面引擎将于3月19日对外开放
  14. KITTI数据集下载
  15. 关于ShadowMap中Shadow acne现象的解释
  16. Hulu 2020年校招-算法题《Hulu杀》Python
  17. Java中找朋友的代码_找朋友游戏介绍
  18. blender 鼠标滑轮配合快捷键
  19. STM32F103 485通信开发实例
  20. 华东康桥计算机音乐,感受人文至美 华东康桥2019年第二届音乐飨宴盛大开幕

热门文章

  1. 图示机构受力f作用_工程力学1 -
  2. RFID无人机之智能仓库--RFID智能仓库管理--新导智能
  3. 冲压工艺按其变形性质可以分为材料的分离与成型两大类
  4. Linux驱动开发 | 模块驱动
  5. zip4j加密压缩、解压缩文件、文件夹
  6. 2.2 网络接口与互联网层安全
  7. Android Studio - 北极狐 | 2020.3.1 补丁 3 现已推出
  8. nginx菜鸟教程二
  9. WIN32API讲座7
  10. HashMap?面试?我是谁?我在哪?