实现-超级课程表——校园登录(1)

来源:互联网 发布:电工证模拟考试软件 编辑:程序博客网 时间:2024/04/20 13:59

如果你是在校大学生,或许你用多了各种课程表,比如课程格子,超级课程表。它们都有一个共同点就是可以一键导入教务处的课程。那么一直都是用户的我们,没有考虑过它是如何实现的。那么现在就来模仿一款”超级课程表“。

PS:由于超级课程表是商用软件,原本提取了一些图片,但是为了避免涉及侵权问题,所有图片均已使用一张绿色圆圈代替,背景图片也以颜色代替,缺乏美观,如果你觉得太丑,可以自己寻找图片代替。

那么说了这么久,先来看看这款高仿的软件长什么样子。本文的代码做过精简,所以界面可能有出入。

\

好了,界面太丑,不忍直视,先暂时忽略,本文的重点不是UI,而是如何提取课程。

先做下准备工作。

  1. HttpWatch抓包分析工具。此工具的使用后文介绍

  2. Litepal数据持久化orm,郭大神的大作,挺好用的orm,用法详见郭霖博客。

  3. Async-android-http 数据异步请求框架,这里主要用到这个框架的异步请求以及session保持的功能,或许大多数人没有使用过这个框架的会话保持功能,反正个人觉得就是一神器,操作十分简单,就1句话,不然用HttpClient可能就没那么简单了,要自己写好多内容。具体用法参见github

  4. Jsoup网页内容解析框架,可支持jquery选择器。可以支持从本地加载html,远程加载html,支持数据抽取,数据修改等功能,如果能灵活运用这个框架,那么你想抓取什么东西都不在话下。


    既然要导入课程表,那么一定要登录教务处,结论是需要教务处的账号密码,这个好办,每个学生都有账号密码。那么怎么登录呢,这个当然不是我们人工登录了,只要提供账号密码,由程序来帮我们完成登录过程以及课程的提取过程。如果登录?首先打开教务处登录界面,打开HttpWatch进行跟踪。输入账号,密码,验证码(验证码视具体学校不同,有些学校不含验证码,有些学校含验证码,验证码的处理后文进行说明),输入完成后点击登录,再点击查看课程的菜单,之后停止HttpWatch录制,把文件保存一下进行分析。打开保存后的文件,查看登录时提交的参数及一些信息,记录下来,同时记录查看课程页提交的参数及信息。

    \

     

    先看登录页面提交的参数,参数均是POST提交,这可以通过HttpWatch看到提交方式

    __VIEWSTATE:有这个值页面生成的,这里我直接使用这个固定值而不去抓取,这个值是.net根据表单参数自动生成的。理论上同一个页面是不会变动的。

    Button1:传空值即可

    hidPdrs:传空值即可

    lbLanguage:传空值即可

    RadioButtonList1:图上是乱码,通过查看网页源代码可知该值是学生,因为我们是以学生的角色登录的

    TextBox2:这个值是密码,传密码即可

    txtSecrect:这个值是验证码,传对应的验证码即可

    txtUserName:这个值是学号,传学号即可

    你以为只要提交这些参数就好了吗,那么你就错了,我们还有设置请求头信息,如下图\

     

    我们不必设置所有请求头信息,只需要设置Host,Referer,User-Agent(可不设)。

     

    请求头设置完毕了,那么来说一个重大的问题,就是验证码的问题,这里有三种方式供选择。

    1. 在登录之前抓取验证码,显示出来,供用户输入。

    2. 使用正方的bug1,为什么是bug1呢,因为后面一种方法利用了bug2,bug1,bug2不一定所有学校适用,正方的默认登录页面是default2.aspx,如果这个页面有验证码,你可以试试default1.aspx-default6.aspx六个页面,运气好的话可能会有不需要验证码的页面。这时候你使用该页面进行登录即可(提交参数会不同,具体自己抓包分析)

    3. 使用正方的bug2,不得不说这个bug2,大概是某个程序猿在某男某月某日无意间留下的把,那么怎么使用这个bug呢,很简单,登录的时候直接传验证码为空值或者空字符串过去就好了,有人说,你他妈逗我,这都行,恩,真的行。为什么行呢,原因可能是正方后台程序没有判断传过来的值是不是空。我们模拟登录的时候并没有去请求验证码的页面,所有不会产生验证码(此时为空字符串或者空值)和cookie,当我们提交空验证码时,后台接收到的值就是空字符串,两个空字符串做比较当然相等了,以上只是猜测,毕竟正方是.net的,.net的处理机制本人不是很清楚。

       

      说了这么多理论知识,来点实际的把,先完成登录界面的代码

       

      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      <relativelayout android:layout_height="match_parent"android:layout_width="match_parent"tools:context="${relativePackage}.${activityClass}"xmlns:android="https://schemas.android.com/apk/res/android"xmlns:tools="https://schemas.android.com/tools">
       
           
          <imageview android:background="@drawable/icon"android:id="@+id/logo"android:layout_alignparenttop="true"android:layout_centerhorizontal="true"android:layout_height="wrap_content"android:layout_margintop="30dp"android:layout_width="wrap_content">
          <edittext android:drawableleft="@drawable/username"android:hint="教务处账号"android:id="@+id/username"android:layout_below="@id/logo"android:layout_height="wrap_content"android:layout_margintop="50dp"android:layout_width="match_parent"android:text="/">
       
          <edittext android:drawableleft="@drawable/password"android:hint="教务处密码"android:id="@+id/password"android:layout_below="@id/username"android:layout_height="wrap_content"android:layout_width="match_parent"android:text="android:password=true">
       
          <linearlayout android:id="@+id/ll_code"android:layout_below="@id/password"android:layout_height="wrap_content"android:layout_width="match_parent"android:orientation="horizontal"android:visibility="gone">
       
              <edittext android:hint="验证码"android:id="@+id/secrectCode"android:layout_height="wrap_content"android:layout_width="100dp">
       
              <imageview android:id="@+id/codeImage"android:layout_height="36dp"android:layout_marginleft="10dp"android:layout_marginright="10dp"android:layout_width="72dp"android:scaletype="fitStart"><button android:background="@drawable/btn_login_selector"android:id="@+id/getCode"android:layout_height="40dp"android:layout_width="100dp"android:text="刷新验证码"android:textcolor="#fff"></button><button android:background="@drawable/btn_login_selector"android:id="@+id/login"android:layout_alignparentbottom="true"android:layout_below="@drawable/password"android:layout_centerhorizontal="true"android:layout_height="45dp"android:layout_marginbottom="100dp"android:layout_width="180dp"android:text="登录"android:textcolor="#fff"></button></imageview></edittext></linearlayout></edittext></edittext></imageview></relativelayout>

       

      很简单,就是账号,密码,以及验证码,这里验证码被我隐藏了,因为我使用了bug2,不需要请求验证码,对应的界面隐藏掉,但是如果你把他显示出来,获取验证码让用户输入也是可以的。

      在登录之前先初始化一下cookie,这一步必须在请求之前设置。


      ?
      1
      2
      3
      4
      5
      6
      7
      8
      /**
           * 初始化Cookie
           */
          privatevoid initCookie(Context context) {
              //必须在请求前初始化
              cookie = newPersistentCookieStore(context);
              HttpUtil.getClient().setCookieStore(cookie);
          }

      那么HttpUtil又是什么呢,很简单,就是一个请求用的工具类

       

      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      packagecn.lizhangqu.kb.util;
        
      importorg.apache.http.Header;
        
      importandroid.app.ProgressDialog;
      importandroid.content.Context;
      importandroid.widget.Toast;
      importcn.lizhangqu.kb.service.LinkService;
        
      importcom.loopj.android.http.AsyncHttpClient;
      importcom.loopj.android.http.AsyncHttpResponseHandler;
      importcom.loopj.android.http.BinaryHttpResponseHandler;
      importcom.loopj.android.http.RequestParams;
        
      /**
       * Http请求工具类
       * @author lizhangqu
       * @date 2015-2-1
       */
      /**
       * @author Administrator
       *
       */
      publicclass HttpUtil {
          privatestatic AsyncHttpClient client = newAsyncHttpClient(); // 实例话对象
          // Host地址
          publicstatic final String HOST = ***.***.***.***;
          // 基础地址
          publicstatic final String URL_BASE = https://***.***.***.***/;
          // 验证码地址
          publicstatic final String URL_CODE = https://***.***.***.***/CheckCode.aspx;
          // 登陆地址
          publicstatic final String URL_LOGIN = https://***.***.***.***/default2.aspx;
          // 登录成功的首页
          publicstatic String URL_MAIN = https://***.***.***.***/xs_main.aspx?xh=XH;
          // 请求地址
          publicstatic String URL_QUERY = https://***.***.***.***/QUERY;
        
          /**
           * 请求参数
           */
          publicstatic String Button1 = ;
          publicstatic String hidPdrs = ;
          publicstatic String hidsc = ;
          publicstatic String lbLanguage = ;
          publicstatic String RadioButtonList1 = 学生;
          publicstatic String __VIEWSTATE = dDwyODE2NTM0OTg7Oz7YiHv1mHkLj1OkgkF90IvNTvBrLQ==;
          publicstatic String TextBox2 = null;
          publicstatic String txtSecretCode = null;
          publicstatic String txtUserName = null;
        
          // 静态初始化
          static{
              client.setTimeout(10000);// 设置链接超时,如果不设置,默认为10s
              // 设置请求头
              client.addHeader(Host, HOST);
              client.addHeader(Referer, URL_LOGIN);
              client.addHeader(User-Agent,
                      Mozilla/5.0(Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko);
          }
        
          /**
           * get,用一个完整url获取一个string对象
           *
           * @param urlString
           * @param res
           */
          publicstatic void get(String urlString, AsyncHttpResponseHandler res) {
              client.get(urlString, res);
          }
        
          /**
           * get,url里面带参数
           *
           * @param urlString
           * @param params
           * @param res
           */
          publicstatic void get(String urlString, RequestParams params,
                  AsyncHttpResponseHandler res) {
              client.get(urlString, params, res);
          }
        
          /**
           * get,下载数据使用,会返回byte数据
           *
           * @param uString
           * @param bHandler
           */
          publicstatic void get(String uString, BinaryHttpResponseHandler bHandler) {
              client.get(uString, bHandler);
          }
        
          /**
           * post,不带参数
           *
           * @param urlString
           * @param res
           */
          publicstatic void post(String urlString, AsyncHttpResponseHandler res) {
              client.post(urlString, res);
          }
        
          /**
           * post,带参数
           *
           * @param urlString
           * @param params
           * @param res
           */
          publicstatic void post(String urlString, RequestParams params,
                  AsyncHttpResponseHandler res) {
              client.post(urlString, params, res);
          }
        
          /**
           * post,返回二进制数据时使用,会返回byte数据
           *
           * @param uString
           * @param bHandler
           */
          publicstatic void post(String uString, BinaryHttpResponseHandler bHandler) {
              client.post(uString, bHandler);
          }
        
          /**
           * 返回请求客户端
           *
           * @return
           */
          publicstatic AsyncHttpClient getClient() {
              returnclient;
          }
        
          /**
           * 获得登录时所需的请求参数
           *
           * @return
           */
          publicstatic RequestParams getLoginRequestParams() {
              // 设置请求参数
              RequestParams params = newRequestParams();
              params.add(__VIEWSTATE, __VIEWSTATE);
              params.add(Button1, Button1);
              params.add(hidPdrs, hidPdrs);
              params.add(hidsc, hidsc);
              params.add(lbLanguage, lbLanguage);
              params.add(RadioButtonList1, RadioButtonList1);
              params.add(TextBox2, TextBox2);
              params.add(txtSecretCode, txtSecretCode);
              params.add(txtUserName, txtUserName);
              returnparams;
          }
        
          /**
           * 接口回调
           * @author lizhangqu
           *
           * 2015-2-22
           */
          publicinterface QueryCallback {
              publicString handleResult(byte[] result);
          }
        
          /**
           * 登录后查询信息封装好的函数
           * @param context
           * @param linkService
           * @param urlName
           * @param callback
           */
          publicstatic void getQuery(finalContext context, LinkService linkService,
                  finalString urlName, finalQueryCallback callback) {
              finalProgressDialog dialog = CommonUtil.getProcessDialog(context,
                      正在获取 + urlName);
              dialog.show();
              String link = linkService.getLinkByName(urlName);
              if(link != null) {
                  HttpUtil.URL_QUERY = HttpUtil.URL_QUERY.replace(QUERY, link);
              }else{
                  Toast.makeText(context, 链接出现错误, Toast.LENGTH_SHORT).show();
                  return;
              }
              HttpUtil.getClient().addHeader(Referer, HttpUtil.URL_MAIN);
              HttpUtil.getClient().setURLEncodingEnabled(true);
              HttpUtil.get(HttpUtil.URL_QUERY,newAsyncHttpResponseHandler() {
                  @Override
                  publicvoid onSuccess(intarg0, Header[] arg1, byte[] arg2) {
                      if(callback != null) {
                          callback.handleResult(arg2);
                      }
                      Toast.makeText(context, urlName + 获取成功!!!, Toast.LENGTH_LONG)
                              .show();
                      dialog.dismiss();
                  }
        
                  @Override
                  publicvoid onFailure(intarg0, Header[] arg1, byte[] arg2,
                          Throwable arg3) {
                      dialog.dismiss();
                      Toast.makeText(context, urlName + 获取失败!!!, Toast.LENGTH_SHORT)
                              .show();
                  }
              });
          }
      }

       

      地址信息被我处理掉了,替换成对应的地址即可,都是几个简单的函数,其中最后一个函数做了一个封装,代码自己读吧,这里就不讲了。。。。。

      现在查看登录的代码。

      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      /**
           * 登录
           */
          privatevoid login() {
              HttpUtil.txtUserName = username.getText().toString().trim();
              HttpUtil.TextBox2 = password.getText().toString().trim();
              //需要时打开验证码注释
              //HttpUtil.txtSecretCode = secrectCode.getText().toString().trim();
              if(TextUtils.isEmpty(HttpUtil.txtUserName)
                      || TextUtils.isEmpty(HttpUtil.TextBox2)) {
                  Toast.makeText(getApplicationContext(), 账号或者密码不能为空!,
                          Toast.LENGTH_SHORT).show();
                  return;
              }
              finalProgressDialog dialog =CommonUtil.getProcessDialog(LoginActivity.this,正在登录中!!!);
              dialog.show();
              RequestParams params = HttpUtil.getLoginRequestParams();// 获得请求参数
              HttpUtil.URL_MAIN = HttpUtil.URL_MAIN.replace(XH,
                      HttpUtil.txtUserName);// 获得请求地址
              HttpUtil.getClient().setURLEncodingEnabled(true);
              HttpUtil.post(HttpUtil.URL_LOGIN, params,
                      newAsyncHttpResponseHandler() {
        
                          @Override
                          publicvoid onSuccess(intarg0, Header[] arg1, byte[] arg2) {
                              try{
                                  String resultContent = newString(arg2, gb2312);
                                  if(linkService.isLogin(resultContent)!=null){
                                      String ret = linkService.parseMenu(resultContent);
                                      Log.d(TAG, login success:+ret);
                                      Toast.makeText(getApplicationContext(),
                                              登录成功!!!, Toast.LENGTH_SHORT).show();
                                      jump2Main();
        
                                  }else{
                                      Toast.makeText(getApplicationContext(),账号或者密码错误!!!, Toast.LENGTH_SHORT).show();
                                  }
        
                              }catch(UnsupportedEncodingException e) {
                                  e.printStackTrace();
                              }finally{
                                  dialog.dismiss();
                              }
                          }
                          @Override
                          publicvoid onFailure(intarg0, Header[] arg1, byte[] arg2,
                                  Throwable arg3) {
                              Toast.makeText(getApplicationContext(), 登录失败!!!!,
                                      Toast.LENGTH_SHORT).show();
                              dialog.dismiss();
                          }
                      });
          }
      通过抓取关键字,判断是否登录成功,登录成功则解析菜单,对应的逻辑被我封装在service层里了
      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      packagecn.lizhangqu.kb.service;
        
      importjava.util.List;
        
      importorg.jsoup.Jsoup;
      importorg.jsoup.nodes.Document;
      importorg.jsoup.nodes.Element;
      importorg.jsoup.select.Elements;
      importorg.litepal.crud.DataSupport;
        
      importcn.lizhangqu.kb.model.Course;
      importcn.lizhangqu.kb.model.LinkNode;
        
      /**
       * LinNode表的业务逻辑处理
       * @author lizhangqu
       * @date 2015-2-1
       */
      publicclass LinkService {
          privatestatic volatile LinkService linkService;
          privateLinkService(){}
          publicstatic LinkService getLinkService() {
              if(linkService==null){
                  synchronized(LinkService.class) {
                      if(linkService==null)
                          linkService=newLinkService();
                  }
              }
        
              returnlinkService;
          }
        
          publicString getLinkByName(String name){
              List<linknode> find = DataSupport.where(title=?,name).limit(1).find(LinkNode.class);
              if(find.size()!=0){
                  returnfind.get(0).getLink();
              }else{
                  returnnull;
              }
          }
          publicboolean save(LinkNode linknode){
              returnlinknode.save();
          }
          /**
           * 查询所有链接
           *
           * @return
           */
          publicList<linknode> findAll() {
              returnDataSupport.findAll(LinkNode.class);
          }
          publicString parseMenu(String content) {
              LinkNode linkNode =null;
              StringBuilder result = newStringBuilder();
              Document doc = Jsoup.parse(content);
              Elements elements = doc.select(ul.nav a[target=zhuti]);
              for(Element element : elements) {
                  result.append(element.html() +
       + element.attr(href) +
       
      );
                  linkNode=newLinkNode();
                  linkNode.setTitle(element.text());
                  linkNode.setLink(element.attr(href));
                  save(linkNode);
              }
              returnresult.toString();
        
          }
          publicString isLogin(String content){
              Document doc = Jsoup.parse(content, UTF-8);
              Elements elements = doc.select(span#xhxm);
              try{
                  Element element=elements.get(0);
                  returnelement.text();
              }catch(IndexOutOfBoundsException e){
                  //e.printStackTrace();
              }
              returnnull;
          }
      }</linknode></linknode>

      判断是否登录成功的判断依据是看页面上是否有某某同学,欢迎你,这段信息在id为xhxm的span里,成功后解析菜单,因为不一定只是抓课表,也可能抓成绩,各种抓,所以这里把链接都记录下来,对应页面的源代码我会和代码一同上传。

      如果你要使用验证码,则获取验证码即可,对应代码如下,就是获得验证码后显示在界面上

      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      /**
           * 获得验证码
           */
          privatevoid getCode() {
              finalProgressDialog dialog =CommonUtil.getProcessDialog(LoginActivity.this,正在获取验证码);
              dialog.show();
              HttpUtil.get(HttpUtil.URL_CODE,newAsyncHttpResponseHandler() {
                  @Override
                  publicvoid onSuccess(intarg0, Header[] arg1, byte[] arg2) {
        
                      InputStream is = newByteArrayInputStream(arg2);
                      Bitmap decodeStream = BitmapFactory.decodeStream(is);
                      code.setImageBitmap(decodeStream);
                      Toast.makeText(getApplicationContext(), 验证码获取成功!!!,Toast.LENGTH_SHORT).show();
                      dialog.dismiss();
                  }
        
                  @Override
                  publicvoid onFailure(intarg0, Header[] arg1, byte[] arg2,
                          Throwable arg3) {
        
                      Toast.makeText(getApplicationContext(), 验证码获取失败!!!,
                              Toast.LENGTH_SHORT).show();
                      dialog.dismiss();
        
                  }
              });
          }

      LinkUtil里面是一些常量

       

      ?
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      packagecn.lizhangqu.kb.util;
        
      /**
       * 首页菜单接口
       * 用于定义linknode表中的标题
       * @author lizhangqu
       * @date 2015-2-1
       */
      publicinterface LinkUtil {
          publicstatic final String ZYXXK=专业选修课;
          publicstatic final String QXXGXK=全校性公选课(通识限选);
          publicstatic final String SYXK=实验选课;
          publicstatic final String DJKSBM=等级考试报名;
          publicstatic final String GRXX=个人信息;
          publicstatic final String MMXG=密码修改;
          publicstatic final String XSGRKB=学生个人课表;
          publicstatic final String XSKSCX=学生考试查询;
          publicstatic final String CJCX=成绩查询;
          publicstatic final String DJKSCX=等级考试查询;
          publicstatic final String JCSYXX=教材使用信息;
          publicstatic final String XSXKQKCX=学生选课情况查询;
          publicstatic final String XSBKKSCX=学生补考考试查询;
          publicstatic final String XSXXYPJ=学生信息员评价;
          publicstatic final String FKJGCX=反馈结果查询;
          publicstatic final String JWGG=教务公告;
          publicstatic final String BMJSKBCX=部门教师课表查询;
          publicstatic final String QXKBCX=全校课表查询;
          publicstatic final String JXRLCX=教学日历查询;
      }

       

      接下来是文章的重点,即如何解析课表。