Java模拟登陆教务系统抓取课表成绩

  • 内容
  • 评论
  • 相关

项目已上传GitHub , 欢迎吐槽 : https://github.com/sunflyer/CQUTEduSystemGrabber

不要问我为什么写这东西。。。我纯粹是跟(zhuang)风(bi)来着的。

我发4这是我做过的最蛋疼的一件事情。我居然花了整整一个晚上加今天一个下午尝试各种方法抓教务系统的数据应对各种BUG模拟各种操作!

学校这教务系统我也是无力吐槽,当年各种BUG满天飞,如今真要做正事的时候,你TM居然给我来一堆BUG,我了个大擦。。。。。。皇上,你还记得当年改下GET请求就能扒全校学生所有信息的事儿吗?

话不多说,开始正题。

所谓抓课表抓成绩无非也就是模拟浏览器登录、提交、获取数据罢了,然而学校教务系统简直坑爹到了一个地步。昨天一晚上我尝试各种方法模拟从大门(http://i.cqut.edu.cn)进去,然后抓成绩。

然而这家伙上来就给了我一个下马威。在登陆的时候cookie中保存了两个JSESSIONID,而且不提供这两个ID,再怎么POST也一直返回登录页面。

好嘛,慢慢抓。Chrome开着,F12,抓网络,刷新了三次后发现学校数字化校园连续3~4次302跳转,跳转一次多一个COOKIE,于是两个JSESSIONID就这么来了。

有办法获取服务器返回头么?在JAVA里面HttpURLConnection负责网络请求操作,在请求完毕以后可以通过getHeaderFields来获取服务器返回的头部数据。

因此我的想法是这样的:模拟发送请求,获取COOKIE,然后连通表单数据一起发给服务器。

抓取Header的代码如下

public static List<DataVal> getConnectionHeader(HttpURLConnection h){
		if(h != null){
			List<DataVal> pData = new ArrayList<DataVal>();
			Set<Entry<String,List<String>>> pHeaderFields = h.getHeaderFields().entrySet();
			for(Entry<String,List<String>> x:pHeaderFields){
				//System.out.println("Name : " + x.getKey() + " , Value : " + x.getValue());
				pData.add(new DataVal(x.getKey() , ((List<String>)x.getValue()).get(0)));
			}
			return pData;
		}
		return null;
	}

登陆代码:

public static void login(String no,String key){
		try {
			String cookie = getCookieSessionId();
			
			HttpURLConnection pHucStart = SHttpUtil.getConnection("i.cqut.edu.cn/portal.do?service=http%3A%2F%2Fi.cqut.edu.cn%2Fportal.do");
			pHucStart.setRequestProperty("Cookie", cookie);
			
			String postData = "useValidateCode=0&isremenberme=0&ip=&username=" + no + "&password=" + key + "&losetime=30&lt=" 
					+ getLt(SHttpUtil.getHtmlContent(pHucStart))
					+ "&_eventId=submit&submit1=+";
			
			//TODO : Login With Params
			
			HttpURLConnection pHuc = SHttpUtil.getConnection("i.cqut.edu.cn/zfca/login");
			
			pHuc.setRequestProperty("Cookie", cookie);
			pHuc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
			pHuc.setRequestProperty("Origin", "http://i.cqut.edu.cn");
			pHuc.setRequestProperty("Host", "i.cqut.edu.cn");
			pHuc.setRequestProperty("Referer", "http://i.cqut.edu.cn/zfca/login");
			
			pHuc.setRequestProperty("Content-Length", postData.length() + "");
			pHuc.setRequestMethod("POST");
			pHuc.setDoOutput(true);
			pHuc.getOutputStream().write(postData.getBytes());
			
			//TODO : GET COOKIE FOR LOGIN
			//获取COOKIE
			String pParamCookie = SHttpUtil.getResponseCookie(pHuc);		
			//获取TICKET
			String pParamTicket = pHuc.getURL().toString();
			pParamTicket = pParamTicket.substring(pParamTicket.indexOf("&ticket="));
			
			//进入用户数字化校园
			HttpURLConnection pHucUserCenter = SHttpUtil.getConnection("i.cqut.edu.cn/portal.do?caUserName=" + no + pParamTicket);
			SHttpUtil.setBasicReq(pHucUserCenter, pParamCookie);
			
			String pDataUserCenter = SHttpUtil.getInputStreamContent(pHucUserCenter.getInputStream(),"GBK");
			
			System.out.println(pDataUserCenter);
			
			System.out.println(pHucUserCenter.getURL().toString());
			
			SHttpUtil.printResponseHeader(pHucUserCenter);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

对了,数字化校园登陆的页面有一个lt数据项,这个抓首页获取。代码如下

public static String getLt(String con){
		if(con != null && !"".equals(con)){
			int pPath = con.indexOf("name="lt"");
			if(pPath < 0) return null;
			String pTar[] = con.substring(pPath,pPath+100).split(" ");
			for(String x:pTar){
				if(x.contains("value"))
					return x.substring(x.indexOf(""") + 1, x.length() - 1);
			}
			
		}
		return null;
	}

当我喜出望外的说,“嗯,应该进去了吧”的时候,教务系统给了我一锤子。因为返回的页面里面压!根!就!没!有!访!问!链!接!!!!!

What the fuck?

这尼玛不合天理啊喂,我特么折腾了这么久你连连接都不给我一个?那个链接里面有个变参数我没法确定啊喂,你这不是逗我嘛。

折腾半天后发现后续请求压根就是坑爹中的战斗机。。。。。。后面的情况是,抓到链接了,但是莫名其妙出现的COOKIE字段让我没法接受,一时半会儿找不到源头。所以这方法算是废了。

怎么办?甲烷曾经说过,教务系统是不需要验证码的。(页面里面的验证码,只要不去请求他,验证码字段就可以不管)

好吧,这尼玛是逼我从这里下手咯。

再折腾了半个小时之后总算是折腾进了系统,于是就有了下面这段代码

public static UserInfo login(String no,String key){
		DataVal pDv = getViewState();
		
		try {
			String pFormData = "__VIEWSTATE=" + URLEncoder.encode(pDv.getData().toString(), "UTF-8").replace("+", "%20") +
			"&txtUserName="+ no +
			"&TextBox2="+ key +
			"&txtSecretCode=&RadioButtonList1=%D1%A7%C9%FA&Button1=&lbLanguage=&hidPdrs=&hidsc=";
			
			HttpURLConnection pHuc = SHttpUtil.getConnection(pDv.getName().toString());
			SHttpUtil.setBasicReq(pHuc, null);
			pHuc.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
			pHuc.setRequestProperty("Host", "jwxt.i.cqut.edu.cn");
			pHuc.setRequestMethod("POST");
			pHuc.setDoOutput(true);
			pHuc.getOutputStream().write(pFormData.getBytes());
			
			String pResponseData = SHttpUtil.getInputStreamContent(pHuc.getInputStream(),"GBK");
			
			if(pResponseData.indexOf(no) >= 0){
				//登陆成功
				int pNamePath = pResponseData.indexOf("<span id="xhxm">");
				if(pNamePath < 0){
					System.out.println("登陆失败!请检查用户名和密码,然后重试");
					return null;
				}
				
				String pName = pResponseData.substring(pNamePath + "<span id="xhxm">".length() ,pResponseData.indexOf("同学</span>",pNamePath));
				
				String pAddr = pHuc.getURL().toString().substring(0 , pHuc.getURL().toString().indexOf(")/")) + ")/";
				
				return new UserInfo(pAddr,pName,no);
			}
		
			//抓名字,后面要用
			
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e){
			e.printStackTrace();
		}
		System.out.println("登陆失败!检查输入的用户名和密码,然后重试");
		return null;
	}

UserInfo包含了访问时的地址(因为教务系统的地址是TM动态的,动态的!形如下面这段

http://jwxt.i.cqut.edu.cn/(nstl2ujf05ogynenpehnpb3t)/default2.aspx

中间括号那段就是动态的。。。然而登陆以后这个东西你就可以直接用。可以说是SESSION这类的东西吧),还有用户学号和姓名(貌似上次说了那个随便抓的问题以后莫名其妙开始检查姓名学号了卧槽。。。。)

好了,折腾了这么久进来了,看了下成绩的地址构成简直简单的不行,模拟了几次操作后发现是修改学年和学期后都会修改viewstate,然后查询的时候发送这玩意儿给服务器做检查。

那就原模原样来一次嘛~可惜。。。。。。他妈的没用啊!!!!!!!!!!

先是一直给我返回服务器错误。然后今天中午问kebe7jun,这家伙跟我说用GBK编码参数。。。。。

GBK。。。我擦你二大爷,什么时候了还GBK。。。用UTF能死吗喂

然后我问了句我说viewstate要不要来一次,他说不要。。。。。。

然而当老子再一次失败后编码viewstate再重来的成功了的时候,这玩意儿直接打了这家伙的脸= =

说好的不要编码呢?????

当然。。。。我说的成功是指。。。。他不返回错误了。。。。

他还是只返回这学期的成绩。然后查了大半天发现viewstate压根没变,我也不知道怎么回事,该请求的请求了,该发的也发了,就是TM的不变。

B了狗了啊QQ图片20150320211443

正当老子要爆炸了的时候突然想起来成绩统计里面貌似可以查到。。。。。。。。。。。。

于是兴致冲冲的开始抓,好嘞,单纯的抓一次viewstate然后发出去就可以了。这尼玛不是容易得多吗

然后就有了。。。。这段代码

	/**
	 * 抓取成绩
	 * */
	public static void getGrade(UserInfo i){
		if(i != null){			
			
			//抓取viewstate
			String pCurrentData = getContent(getConnection("xscjcx.aspx" , "&gnmkdm=N121604" , i));
			String pv = catchKey(pCurrentData  , "name="__VIEWSTATE" value="" ,"" />" );
			
			try {
				String postdata = "__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=" + URLEncoder.encode(pv,"GB2312") + "&hidLanguage=&ddlXN=&ddlXQ=&ddl_kcxz=&btn_zcj=%C0%FA%C4%EA%B3%C9%BC%A8";				
				pCurrentData = getScoreContent(postdata,i);
					
				//System.out.println(data);
				if(pCurrentData.indexOf("<tr class="datelisthead">") < 0 || pCurrentData.contains("错误") || pCurrentData.contains("ERROR")){
					System.out.println("获取信息失败。");
					return;
				
				}
				
				String pData = catchKey(pCurrentData , "<tr class="datelisthead">" , "</table>");
				String[] pClassList = pData.replace("<tr>","").replace("<tr class="alt">","").split("</tr>");
				
				ArrayList<GradeInfo> pGrade = new ArrayList<GradeInfo>();
				
				for(int z = 1 ; z < pClassList.length ; z++){
					String pRep[] = pClassList[z].trim().replace("<td></td>","<td>无</td>").replace("&nbsp;", "无").replace("<td>", "").split("</td>");
					if(pRep.length >= 10)
						pGrade.add(new GradeInfo(pRep));
				}
				
				System.out.println("课程学年t课程学期t课程代码t课程学分t课程绩点t课程成绩t是否重修t课程名称");
				
				for(GradeInfo x : pGrade){
					//System.out.println(x.toString());
					System.out.println(x.stuYear + "t" + x.stuPeriod  + "t" + x.classCode + "t" + x.credit + "t" + x.gradeNorm + "t" + x.gradeMid + "t" + x.isRestudy + "t" + x.className);
				}
				
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			/**
			//当前页面抓到的成绩
			if(pCurrentData.indexOf("<tr class="datelisthead">") < 0 || pCurrentData.contains("错误") || pCurrentData.contains("ERROR")){
				System.out.println("获取信息失败。");
				return;
			}
			
			String pData = catchKey(pCurrentData , "<tr class="datelisthead">" , "</table>");
			String[] pClassList = pData.replace("<tr>","").replace("<tr class="alt">","").split("</tr>");
			
			ArrayList<GradeInfo> pGrade = new ArrayList<GradeInfo>();
			
			for(int z = 1 ; z < pClassList.length ; z++){
				String pRep[] = pClassList[z].trim().replace("&nbsp;", "无").replace("<td>", "").split("</td>");
				if(pRep.length >= 17)
					pGrade.add(new GradeInfo(pRep));
			}
			
			for(GradeInfo x : pGrade){
				System.out.println(x.toString());
			}
			
			//System.out.println(pCurrentData);
			
			
			//尝试后续抓取
			try {
				String pVis = catchKey(pCurrentData , "name="__VIEWSTATE" value="" ,"" />" );
				pVis = getScoreContent(getScoreContentPost("ddlxn","",URLEncoder.encode(pVis , "GB2312"),"%C8%AB%B2%BF","2",null),i);
				pVis = catchKey(pVis, "name="__VIEWSTATE" value="" ,"" />");
				
				String x = pVis;
				
				pVis = getScoreContent(getScoreContentPost("ddlxq","",URLEncoder.encode(pVis , "GB2312"),"%C8%AB%B2%BF","%C8%AB%B2%BF",null),i);
				pVis = catchKey(pVis, "name="__VIEWSTATE" value="" ,"" />");
				
				String y = pVis;
				System.out.println(x.equals(y));
				
				pVis = getScoreContent(getScoreContentPost("","",URLEncoder.encode(pVis , "GB2312"),"%C8%AB%B2%BF","%C8%AB%B2%BF","&btnCx=+%B2%E9++%D1%AF+"),i);
				
				String z = catchKey(pVis, "name="__VIEWSTATE" value="" ,"" />");
				System.out.println(z.equals(x));
				
				System.out.println(pVis);
					
			} catch(Exception e){
				e.printStackTrace();
			}
			**/
		}
	}

(注释部分是之前尝试用的代码,然而并没有什么用。。。。。。。)

这次倒是成功了。全部抓了下来。。。。。。包括重修的T T

QQ截图20150527223318

(别TM吐槽我,我只是一个挂过科的学渣而已。。。。。。。。。Eclipse控制台字体简直不忍直视。。。忍忍吧= =)

妈的。。。。好坑爹啊 T T

评论

12条评论
  1. Gravatar 头像

    潘达

    卧槽,评论还被删了,我服,不想说可以直接拒绝

    • Gravatar 头像

      CrazyChen

      @潘达 只是有段时间没上来看了而已,评论是审核才会通过的,没必要这么认真吧。

  2. Gravatar 头像

    潘达

    邮件写错了,希望学长指点一二,e....mm我不是伸手党,

  3. Gravatar 头像

    潘达

    我也在研究这个东西,就是写java程序给服务器发报文,但是验证码那关一直过不去,他妈的,原来验证码就没用啊,那我是哪里出了问题,F12之后,出现了学号密码用百分号连接的一个特殊数字,掺杂在报文里,所有js都看了,就是验证码加密程序在服务器里面看不见,所以进不去,希望能看看楼主完整的代码,最好Git项目吗

  4. Gravatar 头像

    孙越

    还忘了说,解析HTML页面推荐用Jsoup,超好用。曾经写安卓程序爬取校内网新闻,特别方便!

    • Gravatar 头像

      CrazyChen

      @孙越 以前封装过一个XML的操作类,后面不知道扔哪里去了,然后就算了 - -

  5. Gravatar 头像

    孙越

    之前做过一个抓取学年学期平均绩点的小程序。不过那个程序有时候可以用有时候又会报异常。。现在已经完全不能用了。

    抓取数据推荐用HttpWatcher,只能在IE下面使用,不过感觉功能还是蛮强大的 我来测试a标签可用否

    然后模拟程序我用的是第三方jar包,HttpClient,感觉很不错的说,但是不要用最新版本的,最新版本的我感觉比较坑。用旧版的(用3.x.x,不要用4.x.x)。

    • Gravatar 头像

      CrazyChen

      @孙越 Chrome自带的F12够用了。至少目前对我来说是。

  6. Gravatar 头像

    KEBE

    肯定要urldecode哈,我擦,里面有一系列特殊符号。?

    • Gravatar 头像

      crazychen

      @KEBE 当初谁说viewstate不需要的

  7. Gravatar 头像

    Jiavan

    是的,这个GBK编码当时我也看醉了,数字化校园也应该是可以登录的,和那个ViewState差不多,每次请求换viewstate也是坑,先请求一次才能得到最后post的url。。。

    • Gravatar 头像

      crazychen

      @Jiavan 数字化校园是可以登录,但是老子抓不到后续链接,而且Chrome抓包显示就算是抓到链接了还有一堆的cookie不知道从何而来。。。坑死人