用爬虫模拟登陆获取数据。
课表界面
成绩查询界面
考场查询界面
上个寒假的时候闲的蛋疼做的一个手机网站用来在手机上查询教务系统里面的信息。手机这上面是用一个WebView加载这个网页而已……是不是很坑爹!踏马的我也没办法啊,当时自己一不会美工,二又不会Android开发,答应给我做客户端的小伙伴结果还是没做出来。
不过有1500多个用户还是不错的战绩吧~
===========================================
应用最外层是安卓构架的B/S应用,一个WebView插件解决,没什么好说的,在这里我只想说说模拟登录提取数据的过程。
首先来看看百度百科对PHP curl的释义:
PHP支持的由Daniel Stenberg创建的libcurl库允许你与各种的服务器使用各种类型的协议进行连接和通讯。
libcurl目前支持http、https、ftp、gopher、telnet、dict、file和ldap协议。libcurl同时也支持HTTPS认证、HTTP POST、HTTP PUT、 FTP 上传(这个也能通过PHP的FTP扩展完成)、HTTP 基于表单的上传、代理、cookies和用户名+密码的认证。
PHP中使用cURL实现Get和Post请求的方法
这些函数在PHP 4.0.2中被引入。
函数
描述
curl_close()
关闭一个cURL会话。
curl_copy_handle()
复制一个cURL句柄和它的所有选项。
curl_errno()
返回最后一次的错误号。
curl_error()
返回一个保护当前会话最近一次错误的字符串。
curl_escape()
返回转义字符串,对给定的字符串进行URL编码。
curl_exec()
执行一个cURL会话。
curl_file_create()
创建一个 CURLFile 对象。
curl_getinfo()
获取一个cURL连接资源句柄的信息。
curl_init()
初始化一个cURL会话。
curl_multi_add_handle()
向curl批处理会话中添加单独的curl句柄。
curl_multi_close()
关闭一组cURL句柄。
curl_multi_exec()
运行当前 cURL 句柄的子连接。
curl_multi_getcontent()
如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流。
curl_multi_info_read()
获取当前解析的cURL的相关传输信息。
curl_multi_init()
返回一个新cURL批处理句柄。
curl_multi_remove_handle()
移除curl批处理句柄资源中的某个句柄资源。
curl_multi_select()
等待所有cURL批处理中的活动连接。
curl_multi_setopt()
设置一个批处理cURL传输选项。
curl_multi_strerror()
返回描述错误码的字符串文本。
curl_pause()
暂停及恢复连接。
curl_reset()
重置libcurl的会话句柄的所有选项。
curl_setopt_array()
为cURL传输会话批量设置选项。
curl_setopt()
设置一个cURL传输选项。
curl_share_close()
关闭cURL共享句柄。
curl_share_init()
初始化cURL共享句柄。
curl_share_setopt()
设置一个共享句柄的cURL传输选项。
curl_strerror()
返回错误代码的字符串描述。
curl_unescape()
解码URL编码后的字符串。
curl_version()
获取cURL版本信息。
整体框架采用的是ThinkPHP。
首先进教务系统登录界面,在控制台中查看cookie
发现cookie无论怎么刷新都是这个,觉得这应该是教务系统的验证没有做好,试着加这个cookie来模拟登录。成功,放在其他服务器上也成功,于是登录就采用固定cookie了(经过现在长时间的检验,这个方法确实可行)。
给自己封装的一个Curl:
$cookie = null;
/**
* curl的模拟登陆封装
* @param [string] $url [所请求的URL地址]
* @param [string] $post [请求类型]
* @param [array] $header [表头数组]
* @param [string] $cookie [请求的cookie字符串]
* @param [array] $data [请求的表单数据]
* @param [boolean] $retHeader [是否返回header(比如需要获取header中的cookie)]
* @return [type] [description]
*/
function myCurl($url, $post, $header, $cookie, $data, $retHeader = false){
$ch = curl_init();
if($url != null)
curl_setopt($ch, CURLOPT_URL, $url);
if($header != null)
curl_setopt($ch, CURLOPT_HTTPHEADER,$header);
if($cookie != null)
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_HEADER, $retHeader);
if($post == "post" || $post == "POST")
curl_setopt($ch, CURLOPT_POST, true);
if($data != null){
foreach ($data as $key => $value) {
$dstr[] = $key.'='.$value;
}
$datafileds = implode('&', $dstr);
curl_setopt($ch, CURLOPT_POSTFIELDS,$datafileds);
}
$ret = curl_exec($ch);
curl_close($ch);
return $ret;
}
/**
* 获取网页中的header
* @param [string] $content [网页内容(包含header)]
* @return [string] [header的串]
*/
function getHeader($content){
if($content)
list($header, $body) = explode("\r\n\r\n", $content);
else
$header = null;
return $header;
}
/**
* 获取网页中的cookie
* @param [string] $content [网页内容(包含header)]
* @return [array] [cookie数组]
*/
function getCookie($content){
$cookie = null;
$header = getHeader($content);
preg_match_all("/set\-cookie:([^\n\r]*)/i", $header, $matches,PREG_SET_ORDER);
foreach ($matches as $key) {
$cookie[] = $key[1];
}
return $cookie;
}
登录函数:
/**
* 登录
* @param [type] $param [description]
* @param [type] $id [description]
* @param [type] $password [description]
* @return [type] [description]
*/
function login($id,$password){
$ch = curl_init();
$ref = 'http://jwgl.hbpu.edu.cn/default2.aspx?xh='.$id;
$url = $ref;
$data = array(
'__VIEWSTATE' => 'dDwtMjEzNzcwMzMxNTs7PmjPp%2FwmsphWK9gdUIMNu%2FAMGuy%2F',
'__VIEWSTATEGENERATOR' => '92719903',
'TextBox1' => $id,
'TextBox2' => $password,
'RadioButtonList1' => '%D1%A7%C9%FA',
'Button1' => '+%B5%C7+%C2%BC+'
);
global $cookie;
$cookie = changeCookie($id, $password);
$ret = myCurl($url,"post", null, $cookie, $data, true);
echo $ret;die;
if(strpos($ret, '<span id="xhxm">')){
$pos_begin = strpos($ret, 'xm=');
$pos_end = strpos($ret, '&gnmkdm',$pos_begin);
$str = substr($ret, $pos_begin+3,$pos_end-$pos_begin-3);
$str = iconv('GB2312', 'UTF-8//IGNORE', $str);
return $str;
}else{
return false;
}
//print_r($ret);
}
登录包的header中的referes和URL是一致的,然后把表单信息以POST方式发出,CURLOPT_RETURNTRANSFER选项为内容不直接显示,而是$ret获取返回的内容。判断返回的内容中是否有登录后的界面HTML元素中独有的元素,然后找这个字符串,根据是否有这个字符串判断是否登录成功(当时暂且还不会用正则表达式= =),并返回。
获取查课表表单函数:
/**
* 获取课表表单参数
* @param [type] $param [description]
* @param [type] $id [description]
* @return [type] [description]
*/
function get_schedule_form($id){
global $cookie;
$url = 'http://jwgl.hbpu.edu.cn/tjkbcx.aspx?xh='.$id;
$header = array('Referer: http://jwgl.hbpu.edu.cn/tjkbcx.aspx?xh='.$id);
$ret = myCurl($url, "get", $header, $cookie, null, true);
$end = 0;
$i = 0;
while ($i++<6) {
$begin = strpos($ret, '<option selected="selected"',$end);
$end = strpos($ret, '">',$begin);
$array[] = substr($ret, $begin+35,$end-$begin-35);
}
$pos1 = strpos($ret, "__VIEWSTATE");
$pos2 = strpos($ret, "/>",$pos1);
$array[] = substr($ret, $pos1+20,$pos2-$pos1-22);
$pos1 = strpos($ret, "__VIEWSTATEGENERATOR",$pos2);
$pos2 = strpos($ret, "/>",$pos1);
$array[] = substr($ret, $pos1+29,$pos2-$pos1-31);
//print_r($array);
return $array;
}
获取到信息后改变返回数组,使其成为所想查询的学期的表单
获取课表所在网页内容函数:
/**
* 获取课表结果
* @param [type] $param [description]
* @param [type] $id [description]
* @param [type] $xn [description]
* @param [type] $xq [description]
* @return [type] [description]
*/
function getschedule($id,$array){
global $cookie;
$array[6] = str_replace("+", "%2B", $array[6]);
$array[6] = str_replace("=", "%3D", $array[6]);
$header = array('Referer: http://jwgl.hbpu.edu.cn/tjkbcx.aspx?xh='.$id);
$url = $ref;
$data = array(
'__VIEWSTATE' => $array[6],
'__VIEWSTATEGENERATOR' =>$array[7],
'xn' =>$array[0],
"xq" =>$array[1],
"nj" =>$array[2],
"xy" =>$array[3],
"zy" =>$array[4],
"kb" =>$array[5]
);
$ret = myCurl($url, "post", $header, $cookie, $data, true);
//print_r($ret);
return $ret;
}
在手动查课表时右键查看源代码,可以看到课表在一个<table>元素中,我们可以先返回这个网页内容,由分解函数分解出数组
分解函数:
/**
* 分解课表table
* @param [type] $str [description]
* @return [type] [description]
*/
function divide_info($str){
// $str = str_replace("\r\n", "", $str);
// $str = str_replace(" ", "", $str);
$str = str_replace(" ", "", $str);
$str = iconv('GB2312', 'UTF-8//IGNORE', $str);
$class_str = array("第一节","第三节","第五节","第七节");
for ($j=0; $j <4 ; $j++) {
$end_td =strpos($str, $class_str[$j]);
for ($i=0; $i < 7; $i++) {
$pos_td = strpos($str, "<td",$end_td);
$mid_td = strpos($str, ">",$pos_td);
$end_td = strpos($str, "</td>",$mid_td);
$ret_str[$j][$i] = substr($str, $mid_td+1,$end_td-$mid_td-1);
$divide = explode("<br>", $ret_str[$j][$i]);
$ret_str[$j][$i] = $divide;
//print_r($divide);
}
}
return $ret_str;
}
现在看起来当然是很蠢的做法, 没办法当时不会正则分解……好丢脸= =
通过分解函数的分解,可以返回一个PHP数组,然后调用写数据库函数来讲数组写入数据库。建立数据库的原因是课表本来就是静态不变的,放在数据库中有利于减少CURL次数,减少系统开销。
数据库写入函数:
/**
* 根据数组写入数据库
* @param [type] $array [description]
* @param [type] $cid [description]
* @return [type] [description]
*/
function write_kb($array,$cid){
//header("content-type:text/json;charset=utf-8");
//print_r($array);
//echo count($array);
//echo count($array[0]);
for ($i=0; $i <count($array) ; $i++) {
for ($j=0; $j <count($array[$i]) ; $j++) {
//print_r($array[$i][$j]);
if (count($array[$i][$j]) < 11 && count($array[$i][$j]) > 1) {
write_db($cid, $i+1 , $j+1 , $array[$i][$j][0] , $array[$i][$j][1] , $array[$i][$j][2] , $array[$i][$j][3] ,0);
} else if(count($array[$i][$j]) == 11){
write_db($cid, $i+1 , $j+1 , $array[$i][$j][0] , $array[$i][$j][1] , $array[$i][$j][2] , $array[$i][$j][3] ,0);
write_db($cid, $i+1 , $j+1 , $array[$i][$j][6] , $array[$i][$j][7] , $array[$i][$j][8] , $array[$i][$j][9] ,1);
}
}
}
}
/**
* 数据库插入函数
* @param [type] $cid [description]
* @param [type] $num [description]
* @param [type] $day [description]
* @param [type] $cname [description]
* @param [type] $cstep [description]
* @param [type] $cteacher [description]
* @param [type] $croom [description]
* @param [type] $cdouble [description]
* @return [type] [description]
*/
function write_db($cid,$num,$day,$cname,$cstep,$cteacher,$croom,$cdouble){
$schedules = M('schedules');
//$kb_id = $schedule->getLastInsID()+1;
$data = array(
//'kb_id' => (int)$kb_id,
'cid' => $cid,
'day' => (int)$day,
'num' => (int)$num,
'cname' => $cname,
'cteacher' => $cteacher,
'croom' => $croom,
'cstep' => $cstep,
'cdouble' => (int)$cdouble
);
$ret = $schedules->data($data)->add();
}
当然,学生信息也有一个表,记录学生的账号和密码,并在浏览器中设置相关COOKIE,方便下一次免登陆。
然后是从数据库中调取出数据,并首先初始化一个数组,并将这个数组中插入数据库查询出的数据,这样,有课的地方就显示课程,没课的地方为空,这个数组就可以传给前台页面使用了。
function go($id,$device){
$cid =substr($id, 0,10);
//初始化一个数组
for ($i=0; $i <7 ; $i++) {
for ($j=0; $j < 4; $j++) {
$array[$i][$j] = array();
}
}
$schedules = M('schedules');
//将两节合并的课查找出来并加入数组
$ret1 = $schedules->where('cid='.$cid.' and cdouble = 0')->select();
foreach ($ret1 as $key) {
$day = $key[('day')]-1;
$num = $key[('num')]-1;
$array[$day][$num] = $key;
}
$ret2 = $schedules->where('cid='.$cid.' and cdouble = 1')->select();
//将两节课分离的课表找出来并加入数组
foreach ($ret2 as $key) {
$day = $key[('day')]-1;
$num = $key[('num')]-1;
$array[$day][$num][1] = $key;
}
if($device == 'cs'){
header('Content-type: application/json');
if($ret2 != null)
array_push($ret1,$ret2);
print_r(json_encode($ret1));
}else{
cookie('id',$id,'expire=1555200'); //设置cookie
$this->assign('array',$array)->display('go');
}
}
这样,前台就能获得数组,并显示在网页上。网页的设计方式当然不是重点就不说了,看个人喜好设计就行。
<for start="0" end="7" name="j">
<div id="day{$j+1}" class="kb">
<table class="table">
<for start="0" end="4" step="1" name="i">
<if condition="count($array[$j][$i]) eq 9">
<tr>
<td class="num">{$i*2+1}~{$i*2+2}</td>
<td colspan="2">
{$array[$j][$i]['cname']}<br>
{$array[$j][$i]['cstep']}<br>
{$array[$j][$i]['cteacher']}<br>
{$array[$j][$i]['croom']}
</td>
</tr>
</if>
<if condition="count($array[$j][$i]) eq 0">
<tr>
<td class="num">{$i*2+1}~{$i*2+2}</td>
<td colspan="2">
 <br>
 <br>
 <br>
 
</td>
</tr>
</if>
<if condition="count($array[$j][$i]) gt 9">
<tr>
<td class="num">{$i*2+1}~{$i*2+2}</td>
<td>
{$array[$j][$i]['cname']}<br>
{$array[$j][$i]['cstep']}<br>
{$array[$j][$i]['cteacher']}<br>
{$array[$j][$i]['croom']}
</td>
<td>
{$array[$j][$i][1]['cname']}<br>
{$array[$j][$i][1]['cstep']}<br>
{$array[$j][$i][1]['cteacher']}<br>
{$array[$j][$i][1]['croom']}
</td>
</tr>
</if>
</for>
</table>
</div>
</for>
我在这里写了个javascript,判断星期几,然后标签直接转到星期几,这样做显得人性化了些。
成绩查询的方式大同小异,也是一样的通过获取内容然后模式分解发送给前台显示。不多说。
没有美工做什么东西都好丑啊!!!
我特么只会写代码啊!!!!
愿得一美工,白首……
哎呀不行,有女朋友了不能说这话= =
愿得一美工,搞基到终老!!!!!