Skip to content

snowjujube/crawldeanoffice

Repository files navigation

crawldeanoffice

正方教务系统爬虫

PHP Curl + Simple HTML Dom 把母校教务处放进自己的服务器

概述

  • CURL的应用
  • 正方教务Web请求分析
  • 需要的Web请求的类的创建
  • 教务验证码识别
  • Simple HTML Dom 处理CURL到的HTML

CURL的应用

  1. PHP CURL的介绍

    CURL是PHP中一个库。这个库可以帮助你可以通过URL与不同类型的服务器进行通讯,支持众多运输层/网络层协议。可能笔者本人并没有太多对CURL的深入了解,因此可能不能详细介绍,日后会好好深入学习CURL库。不过我从Segmentfault找到了一段介绍,把URL Copy给大家,有想深入了解的朋友可以去看看.

    PHP supports libcurl, a library created by Daniel Stenberg, that allows you to connect and communicate to many different types of servers with many different types of protocols. libcurl currently supports the http, https, ftp, gopher, telnet, dict, file, and ldap protocols. libcurl also supports HTTPS certificates, HTTP POST, HTTP PUT, FTP uploading (this can also be done with PHP's ftp extension), HTTP form based upload, proxies, cookies, and user+password authentication.

    https://segmentfault.com/a/1190000006220620

  2. CURL的配置

    macOS上好像没有什么需要配置的内容。我使用MAMP Pro的环境以及Mac本身对PHP的支持,我没有做任何配置便已可以使用。各位使用Windows的朋友可以上Google或者Baidu去查查具体应该怎么配置,在此不做详细叙述。

  3. 我们即将用到的CURL类的创建

    类中的这些方法其实也并不复杂,大家先稍作浏览。

    class curl
    {
    public function curl_request($url,$post='',$referer=''){
        $curl = curl_init();
            //初始化curl
        curl_setopt($curl,CURLOPT_URL,$url);
            //设置CURLOPT_URL
        curl_setopt($curl,CURLOPT_USERAGENT,$_SERVER["HTTP_USER_AGENT"]);
            //设置user agent,设置为浏览器默认的user agent
        curl_setopt($curl,CURLOPT_FOLLOWLOCATION,0);
            //设置默认允许重定向
        curl_setopt($curl,CURLOPT_AUTOREFERER,1);
            //当返回的信息头含有转向信息时,自动设置前向连接
        curl_setopt($curl,CURLOPT_REFERER,"http://202.200.206.54/xs_main.aspx?xh=".$referer);
            //设置referer
        if ($post){
            curl_setopt($curl,CURLOPT_POST,1);
            //如果有Post数据,则开启Post
            curl_setopt($curl,CURLOPT_POSTFIELDS,http_build_query($post));
            //设置查询Post数据
        }
        curl_setopt($curl,CURLOPT_TIMEOUT, 500);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        //以文件流形式返回而不是直接输出
        $data = curl_exec($curl);
        //执行查询
        if (curl_errno($curl)) {
            return curl_error($curl);
            //如果有错返回错误并终止操作
        }
        curl_close($curl);
        return $data;
    }
    }
    

    在该类中我声明了一个curl_request()方法,正方教务好像现在有两种访问方式。第一种是通过登陆返回Cookie串来完成本次访问操作的方式;第二种则是通过一个随机目录(后边会讲到)以及创建名为ViewState的隐藏域的方式。

    第一种方式其实只需要在curl_request()中加入Cookiecurl_setopt即可;第二种方式则相对复杂一点,我数学不太好,借助网上大神的帮助找到了一个创建ViewState的方法,有能看懂的朋友可以私我e-mail: jung.jessica@outlook.com.

    public function viewStage($inputurl,$result){
        $url = $inputurl;
        $pattern = '/<input type="hidden" name="__VIEWSTATE" value="(.*?)" \/>/is';
        preg_match_all($pattern, $result, $matches);
        $res = $matches[1][0];
         // $pattern = '/<input type="hidden" name="__VIEWSTATEGENERATOR" value="(.*?)" \/>/is';
        //  preg_match_all($pattern, $result, $matches);
       // $res[1] = $matches[1][0];
        return $res;
    }

由于和数据爬虫有关,我把它也归类在了Curl类中。

总结

通过CURL请求函数的定义和简单的类的创建,可能我们还不能直观的感受到他的强大和方便之处,于是我想试着做一个查分数的Web应用,开始研究如何爬进学校的教务处(学校保密 非985/211😑)。


正方教务系统的Web请求分析

分析HTTP请求的APP

我使用的是macOS上一款名为Charles的数据抓包软件,谷歌Chrome浏览器Inspect中的Network功能应该也能完成类似的抓包操作。

charles

大家可以根据需求自行选择喜欢的数据抓包软件。

分析登陆的URL

首先我们来到教务处这个丑陋的界面。

全国大多数高校应该都在用这套系统吧。然后我们将目光转移到地址栏上,观察一下当前我们访问的URL。

出于隐私不泄漏学校教务的地址。但是多次访问后我发现,每一次进入教务系统,我都会被分配一个随机24位的路径。我试着在网上找了很多种方式破译都了了收场。

如果我不能破译,那干脆直接利用这个地址好啦💁🏻。

以下代码可以查询到我们所需要的那个每次访问教务生成的随机路径,正是根据这个随机路径才保证了此次查询是由当前用户所进行的,我们正是要通过爬虫来模仿这样的操作。

<?PHP
    $url = "http://就不告诉你";
    $headers = get_headers($url, TRUE);
    $url = $headers["Location"];
    $result = array();
    preg_match_all("/(?:\()(.*)(?:\))/i",$url, $result);
    $_SESSION["fuckingcode"] = $result[1][0];
?>

我为URL使用get_headers()方法,第二个参数是返回数组的类型(0为索引数组,1为关联数组)。在返回的数组中取其中的["Location"],正好就是每次访问需要的随机路径啦,在正则匹配后得到一个我们希望的结果,加入$_SESSION中。

至此,URL的分析算是大功告成了。


需要的Web请求的类的创建

分析登陆时用到的GET/POST请求

做一次对教务系统的登陆操作并在Charles中检索后,我们便可以清楚的得到访问一次教务系统需要的请求。

且不说验证码,我们先看一下做一次登陆向Default.aspx中的请求内容。

两张图片已经可以说明问题,登陆表单的验证采用了Post的方式,Post请求到Default.aspx页面。

需要提交的数据刚刚我们在CURL类中做了这样的参数设置:

 if ($post){
            curl_setopt($curl,CURLOPT_POST,1);
            //如果有Post数据,则开启Post
            curl_setopt($curl,CURLOPT_POSTFIELDS,http_build_query($post));
            //设置查询Post数据
        }

Post的默认值为空,但是如果有表单需要提交,开启CURLOPT_POST,并设置做一次标准的HTTPPOST请求,只需要一个参数的设置,真的方便。AJAX无法进行跨域请求,局限性可想而知,而CURL库为PHP真的带来了无限的乐趣。

而提交的表单参数也非常清晰。__VIEWSTATE刚刚的CURL类中利用正则表达式帮我们自动生成。我们需要提供给生成__VIEWSTATE的参数是当前访问的URL以及CURL请求当前URL一次的结果。

其余的参数则一目了然:

KEY VALUE
txtUserName 学生学号
Textbox1 密码的一个隐藏域(教务为了安全性设置,其实很愚蠢)
TextBox2 密码域 验证发现Textbox1 和 Textbox2 都输入密码时方能正确提交
txtSecretCode 验证码(暂时不考虑)
RadioButtonList1 这里是GB2312编码导致的乱码 因此提交时这里应填GB2312编码的”学生“
Button1 同上,GB2312编码的“登陆二字”
lbLanguage 留空
hidPdrs 留空
hidsc 留空

分析完这一切,我们便可以在进行一些设置后,开始一次愉快的本地教务处登陆行为了。

为了方便查询,我根据上述表单创建了一个Userinfo类。代码如下:

<?php
/**
 * Created by PhpStorm.
 * User: snowjujube
 * Date: 24/01/2018
 * Time: 10:28 PM
 */

class userinfo
{
    function loginpost($res,$str,$username,$password){
        $post['__VIEWSTATE'] = "$res";
        $post['txtUserName'] = "$username";
        $post['TextBox2'] = "$password";
        $post['TextBox1'] = "$password";
        $post['txtSecretCode'] = "$str";
        $post['lbLanguage'] = '';
        $post['RadioButtonList1'] = iconv('utf-8', 'gb2312', '学生');
        $post['Button1'] = iconv('utf-8', 'gb2312', '登录');
        return $post;
    }
}

四个参数分别为CURL类中生成的viewstate的绑定,验证码的字符串,学号,密码。

将生成好的请求内容返回即可。

在做一次请求前,我们还缺少最后一样重要的事情要做:匹配验证码。

教务验证码识别

两种解决方案

之所以把验证码的处理留在最后,我找到了两种关于验证码处理的解决方案。

1. 手动输入
2. Github上大佬写好的验证码识别插件

第一种方式是传统的教务查询方式,但显然大家没有人喜欢传统的输入验证码的累赘的方式,但这种方式除非教务处崩溃,通过我们自己服务器访问教务处的成功率可以是100%;第二种方式的缺陷就在于AI图像识别验证码,成功率在2/3左右(顺利的话一次就能成功,我也在验证过程中遇到过连续十次失败的情况)。@git:https://github.com/Kuri-su/CAPTCHA_Reader_by_zhengfang ,大家如果有什么更好的方法也可以私我。

一些在模拟登陆过程中需要了解的注意事项。

  1. 保证此次所有操作在同样的分配好的随机目录中,这样才可以保证登陆以及后续比如查成绩查课表的操作的准确。
  2. 一定要使用UTF-8编码。
  3. 验证登陆是否成功可以通过正则来实现。
  4. 本地服务器支持CURLOPT_FOLLOWLOCATION,也就意味着假许我们的请求成功,服务器返回的数据是一个要跳转的页面,我们也可以爬到当前页面并通过CURLOPT_RETURNTRANSFER的打开来返回文件流操作;而部分云服务器无法打开CURLOPT_FOLLOWLOCATION

一次完整的localhost登陆过程

<?
session_start();
//开启session
header("Content-Type:text/html;charset=gb2312");
//注意GB2312编码
    include_once "fuckingcode.php";
    //包含刚刚生成的随机目录,将其加入session
    require_once "curl.php";
    //刚刚创建的curl类    
    require_once "userinfo.php";
  //同 刚刚创建的Post请求类  
    require_once "zhengfang/PIN Identify by fangzheng.php";
  //验证码识别插件
  
     $status = 0;
     //如果查询成功将status置1
    $curl = new curl();
    $userinfo = new userinfo(); 
    $username = $_GET["usr"];
    $password = $_GET["pwd"];
    //获取接收到的请求
    $login_url = "http://I am a good boy/({$_SESSION["fuckingcode"]})/Default2.aspx";
    //echo $login_url;
    $login_referer = "http://I love my college/xs_main.aspx?xh=$username"; 
    //设置需要爬虫的url和重定向referer的url
    $str = $_SESSION["letteri"];
    $str .= $_SESSION["letterii"];
    $str .= $_SESSION["letteriii"];
    $str .= $_SESSION["letteriv"];
    //愚蠢的设置验证码
    $res_login = $curl->viewStage($login_url, $login_result);
      //设置VIEWSTATE参数
    $post_login = $userinfo->loginpost($res_login, $str, $username, $password);
    //设置一下即将post的内容
    $main = $curl->curl_request($login_url, $post_login, $username);
    //进行一次完整的登陆操作
    $mainhtml = str_get_html($main);
    //这里是用到了`Simple HTML Dom` 处理`CURL`到的`HTML`
    foreach ($mainhtml->find("a") as $item){
        $name = $item->plaintext;
    }
    if ($name == "here") {
        $status = 1;
        $len = strlen($name[1]) / 2;
        $user = mb_substr($name[1], 0, $len - 2, "GB2312");
        $urlname = urlencode($user);
    }
    //这里判断是否登陆成功,若成功status置1
    
?>

总结

一次完整的本地登陆操作也不过如此,但是登陆成功后我们虽然可以看到页面加载的内容或者提示请求你跳转到新页面的Object,但当我们重新定向的时候,会出现404 not found 的错误,这是因为如果我们想跳转页面,那便是一次新的请求(比如需要查成绩或者课表),请求的内容不一样,那么我们只要在抓包的过程中去仔细看看如果我们想要得到自己在教务系统中想执行的操作应该怎么办就好,照猫画虎是每个人最喜欢的事情。

Simple HTML Dom 处理CURL到的HTML

使用场景

这里假设我已经取到了一个完整的HTML页面。
CURL返回的是一个长长的String,操作起来可以说是非常它妈的苦难了😣。Git上有一个叫做Simple HTML Dom的插件,不仅可以遍历HTML还可以遍历url,大家可以去试试看。我这里做一个简单的介绍。

一次完整的爬成绩操作

定义查询成绩的返回完整Post请求的函数:

    function scorepost($res){
        $post["__EVENTTARGET"] = "";
        $post["__EVENTARGUMENT"] = "";
        $post["__VIEWSTATE"] = "$res";
        $post["hidLanguage"] = "";
        $post["ddlXN"] = "2017-2018";
        $post["ddlXQ"] = "1";
        $post["ddl_kcxz"] = "01";
        $post["btn_zcj"] = iconv('utf-8', 'gb2312', '历年成绩');
        return $post;
    }

一次完整的查询过程:

$info_url = "http://Never told you my babe/({$_SESSION["fuckingcode"]})/xscjcx.aspx?xh=$username&xm=$urlname&gnmkdm=N121623";
    $info_result = $curl->curl_request($info_url);
    $res_info = $curl->viewStage($info_url, $info_result);
    $post_info = $userinfo->scorepost($res_info);
    $info = $curl->curl_request($info_url, $post_info, $login_referer);
    preg_match('/<span id=\"lbl_zymc\">(.*)<\/span>/', $info, $label);
//print_r($label);
    if ($label == "") {
        $status = 0;
    }

使用插件遍历html:

    foreach ($html->find("table#Datagrid1") as $table) {
        foreach ($table->find("tr") as $k => $tr) {
            $score[$k]['year'] = $tr->find('td', 0)->plaintext;//学年
            $score[$k]['term'] = $tr->find('td', 1)->plaintext;//学期
            $score[$k]['code'] = $tr->find('td', 2)->plaintext;//课程编号
            $score[$k]['name'] = $tr->find('td', 3)->plaintext;//课程名
            $score[$k]['nature'] = $tr->find('td', 4)->plaintext;//课程性质
            $score[$k]['credit'] = $tr->find('td', 6)->plaintext;//学分
            $score[$k]['point'] = $tr->find('td', 7)->plaintext;//绩点
            $score[$k]['first_score'] = $tr->find('td', 8)->plaintext;//成绩
            $score[$k]['second_score'] = $tr->find('td', 10)->plaintext;//补考成绩
            $score[$k]['third_score'] = $tr->find('td', 11)->plaintext;//重修成绩
            $score[$k]['studentid'] = $username;
        }
    }
    unset($score[0]);

我们便可以这样轻轻松松把教务处搬到我们自己的服务器,再稍稍做一些前端,一个第三方成绩查询功能也就实现了。

最后附上张成果图:

不要吐槽学渣

About

正方教务系统爬虫

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages