前言
从去年寒假开始,学校的一些二级域名就对外网访问进行了限制。而在校内同学们宿舍普遍是用的电信网,要使用内网需要到教学区域连学校的wifi才行。这样是很不方便的。
寒假一直想写个正方教务系统爬虫的,一直拖着拖着的没有完成。开学这一段时间挺闲的,目前完成了正方教务系统爬取成绩绩点和另外两个小项目,一个是正方系统一键报名四六级,一个是物业报修系统一键物业报修。这三个爬虫目前挂在自己的个人服务器上,以某种手段访问内网,后续会转移到内网服务器,转发到外网,提高访问速度。
习惯了,用博客记录自己写的东西,这里简单记录下正方教务系统爬虫成绩绩点的过程。
正文
UI设计
这里提下这个登陆表单和后端的UI界面,这里是借鉴了我在Github上看到的一个类似的项目
https://github.com/wangyufeng0615/bjuthelper
整个项目跟上述的项目有点类似,都是正方系统,仅仅是在一些小小的地方有区别。
login_grade.php
在上面项目中,个别院校的教务系统是有无需验证码的接口的。而在我们学校是没有的,这里我们需要先获取到验证码
这里用到PHP中的CURL进行获取验证码,并把访问页面的cookie保存到本地。并把验证码图片保存到本地。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| $rand_id = rand(100000, 999999); //for verifycode require_verify_code(); //获取验证码 function require_verify_code(){ $cookie = dirname(__FILE__).'/cookie/'.$_SESSION['id'].'.txt'; //cookie路径 $verify_code_url = "http://localhost/CheckCode.aspx"; //验证码地址 $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $verify_code_url); curl_setopt($curl, CURLOPT_COOKIEJAR, $cookie); //保存cookie curl_setopt($curl, CURLOPT_HEADER, 0); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); $img = curl_exec($curl); //执行curl curl_close($curl); global $rand_id; $path_of_verifyCode =dirname(__FILE__).'/verifyCodes/verifyCode_'.$rand_id.'.jpg'; $fp = fopen($path_of_verifyCode,"w"); //文件名 fwrite($fp,$img); //写入文件 fclose($fp); }
|
login_grade后面的页面就是利用WEUI写得一个提交表单了,这里就不要过多的说了。
require_grade.php
抓包分析
这里还是先简单的抓包分析,先抓登录页面的POST请求包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| POST /Default2.aspx HTTP/1.1 Host: localhost Content-Length: 207 Cache-Control: max-age=0 Origin: http://localhost Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://localhost/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: UM_distinctid=161e202e63c6d9-094afdbce42fa2-3b60450b-1fa400-161e202e63d5b7; ASP.NET_SessionId=ht5hadjnbrhcbebuqa3khb3s Connection: close
__VIEWSTATE=dDwtNTE2MjI4MTQ7Oz7j2BjEQ4cDEffr%2BK8yeXHBPnpEJg%3D%3D&txtUserName=username&Textbox1=&TextBox2=password&txtSecretCode=3vrd&RadioButtonList1=%D1%A7%C9%FA&Button1=&lbLanguage=&hidPdrs=&hidsc=
|
简单的看下POST提交的参数有
1 2 3 4 5 6 7 8 9 10 11 12 13
| __VIEWSTATE=
txtUserName=
Textbox1=
TextBox2=
txtSecretCode=
RadioButtonList1=
&Button1=&lbLanguage=&hidPdrs=&hidsc=
|
其中txtUserName是学号,TextBox2是密码,txtSecretCode是验证码,后面的参数直接默认提交就行。
这里重点的是__VIEWSTATE参数,这个参数是在登录页面里的。需要去获取这个参数,来找下这个参数的位置。
这里我们需要得到这个value值,这里我们需要代入login_grade页面里的cookie放入得到响应的vivewstate值
同样利用curl来访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| //function: 构造post数据并登陆 function login_post($url,$cookie,$post){ global $cookie; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_RETURNTRANSFER,1); //不自动输出数据,要echo才行 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); //重要,抓取跳转后数据 curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie); curl_setopt($ch, CURLOPT_REFERER, 'http://localhost/default2.aspx'); //重要,302跳转需要referer,可以在Request Headers找到 curl_setopt($ch, CURLOPT_POSTFIELDS,$post); //post提交数据 $result=curl_exec($ch); curl_close($ch); return $result; }
|
1 2 3
| $url="http://localhost/default2.aspx"; //教务地址 $con1=login_post($url,$cookie,''); //登陆 preg_match_all('/<input type="hidden" name="__VIEWSTATE" value="([^<>]+)" \/>/', $con1, $view); //获取__VIEWSTATE字段并存到$view数组中
|
传入教务网地址,访问登录页面,利用preg_match_all函数正则匹配到页面里的__VIEWSTATE的value值。
参数都准备好了,可以模拟登录了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $_SESSION['xh']=$_POST['account']; $xh=$_POST['account']; $pw=$_POST['password']; $current_year=$_POST['current_year']; $current_term=$_POST['current_term']; $code= $_POST['verify_code']; $cookie = dirname(__FILE__) . '/cookie/'.$_SESSION['id'].'.txt';
$post=array( '__VIEWSTATE'=>$view[1][0], 'txtUserName'=>$xh, 'TextBox2'=>$pw, 'txtSecretCode'=>$code, 'RadioButtonList1'=>iconv('utf-8', 'gb2312', '学生'), 'Button1'=>iconv('utf-8', 'gb2312', '登录'), 'lbLanguage'=>'', 'hidPdrs'=>'', 'hidsc'=>'' ); $con2=login_post($url,$cookie,http_build_query($post));
|
这样我们就成功的登录到教务系统了,接下来我们需要跳转到成绩页面。
同样抓包分析,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| POST /xscjcx.aspx?xh=1111111 HTTP/1.1 Host: localhost Content-Length: 3839 Cache-Control: max-age=0 Origin: http://localhost Upgrade-Insecure-Requests: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 Referer: http://localhost/xscjcx.aspx?xh=208150815&xm=%B3%C9%CF%E9%D4%C0&gnmkdm=N121617 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: UM_distinctid=161e202e63c6d9-094afdbce42fa2-3b60450b-1fa400-161e202e63d5b7; ASP.NET_SessionId=ht5hadjnbrhcbebuqa3khb3s Connection: close
__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=dDw&hidLanguage=&ddlXN=2017-2018&ddlXQ=1&ddl_kcxz=&btn_xq=%D1%A7%C6%DA%B3%C9%BC%A8
|
这里是查询用户选择学期的POST请求包,url里的/xscjcx.aspx?xh=中的xh是用户学号。
POST参数里
1
| __EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=dDw&hidLanguage=&ddlXN=2017-2018&ddlXQ=1&ddl_kcxz=&btn_xq=%D1%A7%C6%DA%B3%C9%BC%A8
|
这几个参数都可以看出他们的意思,重要的是获取__VIEWSTATE的value值,同样利用一个正则去获取这个值
1 2 3 4
| $url2="http://localhost/xscjcx.aspx?xh=".$_SESSION['xh']; $viewstate=login_post($url2,''); preg_match_all('/<input type="hidden" name="__VIEWSTATE" value="([^<>]+)" \/>/', $viewstate, $vs); $state=$vs[1][0]; //$state存放一会post的__VIEWSTATE
|
获取到所有参数,提交post数据报就可以返回值了。
1 2 3 4 5 6 7 8 9 10 11
| $post=array( '__EVENTTARGET'=>'', '__EVENTARGUMENT'=>'', '__VIEWSTATE'=>$state, 'hidLanguage'=>'', 'ddlXN'=>$current_year, //当前学年 'ddlXQ'=>$current_term, //当前学期 'ddl_kcxz'=>'', 'btn_xq'=>'%D1%A7%C6%DA%B3%C9%BC%A8' //“学期成绩”的gbk编码,视情况而定 ); $content=login_post($url2,$cookie,http_build_query($post)); //获取原始数据
|
这里就得到要查询的学期的成绩了,得到的HTML里的table标签的数据,需要转换为数组进行输出。
用到一个函数get_td_array()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function get_td_array($table) { $table = preg_replace("'<table[^>]*?>'si","",$table); $table = preg_replace("'<tr[^>]*?>'si","",$table); $table = preg_replace("'<td[^>]*?>'si","",$table); $table = str_replace("</tr>","{tr}",$table); $table = str_replace("</td>","{td}",$table); //去掉 HTML 标记 $table = preg_replace("'<[/!]*?[^<>]*?>'si","",$table); //去掉空白字符 $table = preg_replace("'([rn])[s]+'","",$table); $table = preg_replace('/ /',"",$table); $table = str_replace(" ","",$table); $table = str_replace(" ","",$table); $table = explode('{tr}', $table); array_pop($table); foreach ($table as $key=>$tr) { $td = explode('{td}', $tr); array_pop($td); $td_array[] = $td; } return $td_array; }
|
从该函数的代码来看,可以看出是利用正则匹配table标签下的tr,td将数据通过遍历存入数组返回。
到这里当前学期的成绩详情都可以获取到了。
下面是绩点的计算,我们学校的系统是把已修学科的学分绩点都列出来了的。我们只需要把数据爬取出来做一个简单的计算就行了。
数据报跟查询学期成绩是差不多的。简单的看下post数据。
1 2 3 4 5 6 7 8 9 10 11 12
| $post_allgrade=array( '__EVENTTARGET'=>'', '__EVENTARGUMENT'=>'', '__VIEWSTATE'=>$state, 'hidLanguage'=>'', 'ddlXN'=>$current_year, //当前学年 'ddlXQ'=>$current_term, //当前学期 'ddl_kcxz'=>'', 'btn_zcj'=>'%C0%FA%C4%EA%B3%C9%BC%A8' //历年成绩-gbk ); $content_allgrade=login_post($url2,$cookie,http_build_query($post_allgrade)); //获取原始数据 $content_allgrade=get_td_array($content_allgrade); //table转array
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| //计算总的加权分数和总的GPA $i = 5; //从array[5]开始是有效信息 $all_value = 0; //总的学分权值 $all_GPA = 0; //总的GPA*分数 $all_number_of_lesson_with_public = 0; $all_score_of_lesson_with_public = 0; //计算总和的东西,学分/GPA while(isset($content_allgrade[$i][4])){ if ($content_allgrade[$i][5] == iconv("utf-8","gb2312//IGNORE","公选")){ //计算公选课课程数和总学分 $all_number_of_lesson_with_public ++; $all_score_of_lesson_with_public += $content_allgrade[$i][6]; $i++; } else{ $all_value += $content_allgrade[$i][6]; //已修总学分 $all_GPA += ($content_allgrade[$i][6] * $content_allgrade[$i][7]); $i++; }
|
上面是一个简单的计算GPA的过程,简单来说就是一个遍历叠加计算的过程。
到这里require_grade页面的PHP部分差不多都说完了,后面的数据的排版输出了。
Github
https://github.com/uknowsec/GPACrawler
Reference
https://github.com/wangyufeng0615/bjuthelper