我的第一个开源项目:Java爬虫爬取旧版正方教务系统课程表、成绩表
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
类:验证码识别类,包括验证码识别的整个过程,**由于验证码识别训练涉及到数据集、测试集、结果集,启动代码时,请根据自己的实际情况,在配置文件执行修改trainSetDir
、trainTestDir
、trainResultDir
这几个目录所在的位置。**验证码识别的训练与使用是分开的,项目运行时只会在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爬虫爬取旧版正方教务系统课程表、成绩表相关推荐
- Java爬虫 爬取某招聘网站招聘信息
Java爬虫 爬取某招聘网站招聘信息 一.系统介绍 二.功能展示 1.需求爬取的网站内容 2.实现流程 2.1数据采集 2.2页面解析 2.3数据存储 三.获取源码 一.系统介绍 系统主要功能:本项目 ...
- Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情
Java爬虫爬取 天猫 淘宝 京东 搜索页和 商品详情 先识别商品url,区分平台提取商品编号,再根据平台带着商品编号爬取数据. 1.导包 <!-- 爬虫相关Jar包依赖 --><d ...
- python java 爬数据_如何用java爬虫爬取网页上的数据
当我们使用浏览器处理网页的时候,有时候是不需要浏览的,例如使用PhantomJS适用于无头浏览器,进行爬取网页数据操作.最近在进行java爬虫学习的小伙伴们有没有想过如何爬取js生成的网络页面吗?别急 ...
- Java爬虫 --- 爬取王者荣耀英雄图片
Java爬虫 - 爬取王者荣耀英雄图片 import org.jsoup.Connection; import org.jsoup.Jsoup; import org.jsoup.nodes.Docu ...
- java爬虫爬取笔趣阁小说
java爬虫爬取笔趣阁小说 package novelCrawler;import org.jsoup.Connection; import org.jsoup.HttpStatusException ...
- Java爬虫爬取wallhaven的图片
Java爬虫爬取wallhaven的图片 参考文章:JAVA Jsoup爬取网页图片下载到本地 需要的jar包:jsuop wallhaven网站拒绝java程序访问,所以要伪装报头. 发送请求时 C ...
- Python爬虫开源项目代码(爬取微信、淘宝、豆瓣、知乎、新浪微博、QQ、去哪网 等等)...
文章目录 1.简介 2.开源项目Github 2.1.WechatSogou [1]– 微信公众号爬虫 2.2.DouBanSpider [2]– 豆瓣读书爬虫 2.3.zhihu_spider [3 ...
- Python爬虫开源项目代码(爬取微信、淘宝、豆瓣、知乎、新浪微博、QQ、去哪网 等等)
文章目录 1.简介 2.开源项目Github 2.1.WechatSogou [1]– 微信公众号爬虫 2.2.DouBanSpider [2]– 豆瓣读书爬虫 2.3.zhihu_spider [3 ...
- 23个Python爬虫开源项目代码:爬取微信、淘宝、豆瓣、知乎、微博
今天为大家整理了32个Python爬虫项目.整理的原因是,爬虫入门简单快速,也非常适合新入门的小伙伴培养信心,所有链接指向GitHub. 1.WechatSogou – 微信公众号爬虫 基于搜狗微信搜 ...
最新文章
- mysql通过查看跟踪日志跟踪执行的sql语句
- 如何用python实现邮箱发送信息
- Git使用教程之本地仓库的基本操作
- Python从菜鸟到高手(4):导入Python模块
- C#中Lock关键字的使用
- 经典SQL语句大全(技巧篇)
- gui界面怎么分页_什么是用户界面和体验设计
- java中的Sort函数,你值得看
- Python PIL库处理图片常用操作,图像识别数据增强的方法
- get、post请求参数乱码解决方法(qq:1324981084)
- b宝塔 centos端口更改_centos修改ssh默认端口号的方法示例
- plsql查询数据显示为乱码解决方案
- 迅雷Bolt界面引擎将于3月19日对外开放
- KITTI数据集下载
- 关于ShadowMap中Shadow acne现象的解释
- Hulu 2020年校招-算法题《Hulu杀》Python
- Java中找朋友的代码_找朋友游戏介绍
- blender 鼠标滑轮配合快捷键
- STM32F103 485通信开发实例
- 华东康桥计算机音乐,感受人文至美 华东康桥2019年第二届音乐飨宴盛大开幕