从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程

前言

之前写过一篇JAVA使用HttpClient模拟登录正方教务系统,爬取学籍信息和课程表成绩等,超详细登录分析和代码注解的教程,在移植到移动平台时候,发现了如下问题:

  • 抓取课表偶尔会不完全,出现全部乱码的情况
  • HttpClient相关包与SDK冲突,导致移植安卓出现问题
  • 教务系统偶尔会弹出验证码,导致登陆失败
  • 没有现成的课程表界面

在经过详细分析和调试后完美解决上述问题后,写下本篇文章,供大家交流,也避免后来人重走我走过的弯路,以此共勉;

本项目已上传GitHub Android-JAVA-ZFeducation-system,相关说明可以参看下文后记,欢迎大家一起讨论、共同学习进步!


文章较长请按照目录直接跳转到需要的部分阅读

目录

  • 前言
  • 问题分析
    • 解决抓取课程乱码
    • 解决HttpClient与SDK冲突
    • 解决教务系统验证码
    • 使用`HttpClient5`抓取验证码:
    • 发送验证码结果
    • 解决没有课程表界面
  • 最终代码
    • 定义Course对象
    • 建立SQLite数据表
    • 封装模拟登录工具类
    • 显示课程表
      • 1、获取参数
      • 2、获取当前周数
      • 3、读取数据库中的课程信息
      • 4、判断是否非本周并给课程表上色
      • 5、定位并显示课程
      • 6、内部方法:dp转px
  • 效果演示
  • 后记

问题分析

解决抓取课程乱码

抓取课表偶尔会不完全,出现全部乱码的情况

造成这个问题的原因是因为当时使用的HttpClient3已经过时,官方已经停止维护,因为年过许久部分功能已经不在适应当前的HTTP协议,导致部分传输不完全或者丢包从而造成乱码,解决方法也很简单粗暴,直接换用HttpClient5,抛弃之前的Post/getmode,换用新的HttpGet/Post,以及结果集;
为了避免乱码,这里使用了阿里巴巴的JSON库Alibaba来解析得到的响应JSON

  response=httpClient.execute(httpPost);
   HttpEntity kcb=response.getEntity();
    jsonObject = JSON.parseObject(EntityUtils.toString(kcb,"UTF-8"));
 	JSONArray timeTable = JSON.parseArray(jsonObject.getString("kbList"));
        for (Iterator iterator = timeTable.iterator(); iterator.hasNext();){
         JSONObject lesson = (JSONObject) iterator.next();
         Course course=new Course();
         course.setId(usernam);
         course.setName(lesson.getString("kcmc"));
            ……
        //这里都是一样的,太长了,为优化阅读,此次省略,参见下文
         courses.add(course);
        }

注意:这里的Course为一个课程对象,方便对课程进行操作,具体结构会在下文列出

解决HttpClient与SDK冲突

HttpClient相关包与SDK冲突,导致移植安卓出现问题

造成这个问题的原因是由于谷歌抛弃了阿帕奇的Http架构,将其移除了SDK,并且现有SDK包内方法与其重名,这个问题困扰了我很长时间,我也在CSDN和其他平台查阅了许多于此相关资料,均无从解决,后来在阿帕奇的官方资料上找到了一篇关于这个的解决方案,直接在Gradle中添加依赖即可

dependencies {
    api 'com.github.ok2c.hc5.android:httpclient-android:0.1.0'
}

具体可以参考我的另一篇文章在安卓9.0以上版本使用HttpClient

解决教务系统验证码

教务系统偶尔会弹出验证码,导致登陆失败

解决这个问题,先手动模拟登录出现验证码的情况试一试
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第1张图片
用控制台查看验证码相关的HTML代码
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第2张图片
可以看到其指向了一个页面captcha.html?ts=557,在前面加上登录页面的前缀得到以下这个网址https://auth.wtu.edu.cn/authserver/captcha.html?ts=557
经过反复刷新和测试,发现s=偶尔变化偶尔不变,猜想这个ts可能是一个无关的时间参数,直接去掉ts访问网页试一试从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第3张图片
果然,验证码出现了,查看网络请求:
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第4张图片
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第5张图片
是一个带上Cookie的Get请求,没有其他参数
虽然无法得知验证码为什么会出现,但是已经得到了如何获取验证码,那么直接简单粗暴在的每一次请求中都获取验证码,这样就可以避免偶尔出现验证码时导致登录失败的情况发生;

使用HttpClient5抓取验证码:

同之前一样,直接用GET方法带上之前的Cookie发送请求,拿到响应内容

String chakUrl = "https://auth.wtu.edu.cn/authserver/captcha.html";
        httpGet = new HttpGet(chakUrl);
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析验证码错误");
        }
        HttpEntity entityCheck = response.getEntity();

响应内容是一张图片,那就直接转换为字符流显示出来

        InputStream inputStream = null;//获取字符流
        try {
            inputStream = entityCheck.getContent();
            checkIMG =BitmapFactory.decodeStream(inputStream);//读取图像数据
            inputStream.close();
        } catch (IOException e) {
            return toEffexecute("加载验证码失败");
        }
        checkIMG.setImageBitmap(helper.getCheckIMG());

以上是在Android上使用字符流保存图片然后直接显示在屏幕上 非移动平台可以采用以下方法将字符流转换为字节流写入到文件直接保存到本地,然后手动读取的方法:

 InputStream inputStream=entityCheck.getContent();//获取字符流
        OutputStream os = new FileOutputStream("C:\\chakOK.jpg");//写入流
        byte[] b = new byte[1024];//缓冲区
        int temp = 0;//长度
        while ((temp = inputStream.read(b)) != -1) {
            os.write(b, 0, temp);
        }
        os.close();
        inputStream.close();

发送验证码结果

上述是抓取验证码,那拿到了验证码后如何将验证码的结果告诉教务系统呢?继续手动登录模拟看一看:
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第6张图片
对比之前的模拟登录,可以看到参数中多了一个captchaResponse字段,很明显这传输的就是验证码,那么与之前模拟登录一样,只是多了个验证码字段,将它填入即可

 String loURI = "https://auth.wtu.edu.cn/authserver/login;jsessionid=" + JSESSION + "?service=http%3A%2F%2Fjwglxt.wtu.edu.cn%2Fsso%2Fjziotlogin";
        ;
        httpPost = new HttpPost(loURI);
        //请求头
        httpPost.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
        httpPost.setHeader("Accept-Encoding", "gzip, deflate, br");
        httpPost.setHeader("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
        httpPost.setHeader("Connection", "keep-alive");
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
        httpPost.setHeader("Cookie", "route=" + ROUTE + "; JSESSIONID_auth=" + JSESSION);
        httpPost.setHeader("Host", "auth.wtu.edu.cn");
        httpPost.setHeader("Origin", "https://auth.wtu.edu.cn");
        httpPost.setHeader("Referer", url);
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0");
        //参数
        List<NameValuePair> pairs = new ArrayList<>();//创建List集合,封装表单请求参数
        pairs.add(new BasicNameValuePair("username", usernam));
        pairs.add(new BasicNameValuePair("password", password));
        pairs.add(new BasicNameValuePair("captchaResponse", cheack));
        pairs.add(new BasicNameValuePair("lt", lt));
        pairs.add(new BasicNameValuePair("dllt", "userNamePasswordLogin"));
        pairs.add(new BasicNameValuePair("execution", "e1s1"));
        pairs.add(new BasicNameValuePair("_eventId", "submit"));
        pairs.add(new BasicNameValuePair("rmShown", "1"));
        //创建表单的Entity对象,将表单存入其中用UTF-8编码
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(pairs, Charset.forName("UTF-8"));
        //写入参数
        httpPost.setEntity(formEntity);

        //执行
        try {
            response = httpClient.execute(httpPost);
        } catch (IOException e) {
            return toEffexecute("发送请求失败");
        }

至此,关于验证码的问题解决了,又能链上教务系统了!

解决没有课程表界面

没有现成的课程表界面

遇到了这个问题,没有别的解决办法,就直接造一个吧!简单梳理多款相关软件的布局,由于自己就是学生,所以自己也很明确学生的需求,那就按照自己的需求造一个吧!
界面刨析如下:
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第7张图片
布局如上,一个很简单的三段布局,但是有一个很致命的问题出现了,课程不一定是连续的,并且可能会有课程重合例如部分有些课程时间一样,但是起止周不相同,这个时候使用典型的线性布局肯定不可以,在思考后,发现了帧布局FrameLayout这么个玩意儿,我的解决方法如下:

1.整个全局为一个LinearLayout,最上方的时间处用一个横向的LinearLayout平均8等分,用于之后的天数定位
2.下方为一个ScrollView,包裹一个LinearLayout和一个FrameLayout,按照之前天数的划分分别占1份和7份,在LinearLayout添加若干高度相同的子块用于表示节次;
3.之后直接在运行时动态新建view宽度和之前划分的每一份相同,高度为节次数*子块高度,根据课程的时间设置Margins:,
上边距=子块高度*(上课节-1)
左边距=份数大小*(星期数-1)

具体布局代码如下:

"1.0" encoding="utf-8"?>


xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    android:id="@+id/Top"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#3F51B5"
        android:orientation="horizontal">

        android:id="@+id/okWeek"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#8BC34A"
            android:orientation="vertical">

            android:id="@+id/weekNums"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FFEB3B"
            android:orientation="vertical">

            android:id="@+id/weekOne"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周一" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FF9800"
            android:orientation="vertical">

            android:id="@+id/weekTwo"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周二" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FFEB3B"
            android:orientation="vertical">

            android:id="@+id/weekSan"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周三" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FF9800"
            android:orientation="vertical">

            android:id="@+id/weekSi"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周四" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FFEB3B"
            android:orientation="vertical">

            android:id="@+id/weekWU"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周五" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FF9800"
            android:orientation="vertical">

            android:id="@+id/weekLiu"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周六" />
        

        android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="#FFEB3B"
            android:orientation="vertical">

            android:id="@+id/weekDAY"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="周日" />
        
    

    android:id="@+id/body"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1">

        android:id="@+id/JCK"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="horizontal">


            android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:background="#2196F3"
                android:orientation="vertical">

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView16"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="1" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView17"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="2" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView18"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="3" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView19"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="4" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView20"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="5" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView22"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="6" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView21"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="7" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView23"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="8" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView24"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="9" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView25"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="10" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:id="@+id/textView27"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="11" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:id="@+id/textView28"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="12" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#FFEB3B"
                    android:orientation="vertical">

                    android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="13" />
                

                android:layout_width="match_parent"
                    android:layout_height="65dp"
                    android:background="#03A9F4"
                    android:orientation="vertical">

                    android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:gravity="center"
                        android:text="14" />
                

            


            android:id="@+id/PFF"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="7" />
        
    

    android:id="@+id/reset"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:background="?attr/panelBackground"
        android:gravity="center"
        android:orientation="vertical">

        android:id="@+id/About"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:gravity="center"
            android:text="长按此处导入或添加课表" />

    



自此,刚刚遇到的4个问题都已经全部解决,现在可以开始构建最终的APP吧!


最终代码

定义Course对象

为了存储课程信息方便后续使用,定义一个Course对象;

  	private String id="";//课程唯一编号
    private String name="";//课程名
    private String campus="";//校区
    private String room="";//教室
    private String week="";//星期
    private int tMin=1;//开始节
    private int tMax=2;//结束节
    private int wMin=1;//开始周
    private int wMax=2;//结束周
    private String teacher ="";//老师
    private String job="";//职务
    private String test="";//考试方法

其中第一getCV()方法用于将对象转换为ContentValues键值对,方便写数据库使用:

public ContentValues getCV(){
        ContentValues values=new ContentValues();
        values.put("id",id);
        values.put("name",name);
        values.put("campus",campus);
        values.put("room",room);
        values.put("week",week);
        values.put("tMin",tMin);
        values.put("tMax",tMax);
        values.put("wMin",wMin);
        values.put("wMax",wMax);
        values.put("teacher", teacher);
        values.put("job",job);
        values.put("test",test);
        return values;
    }

同理定义一个静态getCourses(Cursor cursor)方法,直接将Cursor转换为对象数组,方便读取数据库显示课表使用:

 public static List<Course> getCourses(Cursor cursor){
        List<Course> courses=new ArrayList<>();
        while (cursor.moveToNext()){
            Course course=new Course();
            course.setId(cursor.getString(0));
            course.setName(cursor.getString(2));
            course.setCampus(cursor.getString(3));
            course.setRoom(cursor.getString(4));
            course.setWeek(cursor.getString(5));
            course.settMin(cursor.getInt(6));
            course.settMax(cursor.getInt(7));
            course.setwMin(cursor.getInt(8));
            course.setwMax(cursor.getInt(9));
            course.setTeacher(cursor.getString(10));
            course.setJob(cursor.getString(11));
            course.setTest(cursor.getString(12));
            courses.add(course);
        }
        return courses;
    }

建立SQLite数据表

避免每次启动都有登录教务系统,直接将课程信息保存到本地数据库,登录一次后每次就可以直接跳转到课表

public class DBhelp extends SQLiteOpenHelper {
    public static final String T_COURSE="t_course";
    public class ContactTable implements BaseColumns {
        public static final  String ID="id";
        public static final  String NAME="name";//课程名
        public static final  String CAMPUS="campus";//校区
        public static final  String ROOM="room";//教室
        public static final  String WEEK="week";//星期
        public static final  String TMIN="tMin";//开始节
        public static final  String TMAX="tMax";//下课节
        public static final  String WMIN="wMin";//开始周
        public static final  String WMAX="wMax";//结束周
        public static final  String TEACHER="teacher";//老师
        public static final  String JOB="job";//职务
        public static final  String TEST="test";//考试方法
    }

    public DBhelp(@Nullable Context context) {
        super(context, "course.db", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql="Create table "+T_COURSE+" (_id integer  PRIMARY KEY AUTOINCREMENT,"
                +ContactTable.ID+" text,"
                +ContactTable.NAME+" text,"
                +ContactTable.CAMPUS+" text,"
                +ContactTable.ROOM+" text,"
                +ContactTable.WEEK+" text,"
                +ContactTable.TMIN+" integer,"
                +ContactTable.TMAX+" integer,"
                +ContactTable.WMIN+" integer,"
                +ContactTable.WMAX+" integer,"
                +ContactTable.TEACHER +" text,"
                +ContactTable.JOB+" text,"
                +ContactTable.TEST+" text)";
        db.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

封装模拟登录工具类

为了方便调用和日后扩展,我将所有与模拟登录相关的方法全部封装于一个JWhelper类中,供之后直接调用,在这里使用的是HttpClient5与上一篇文章的实现大致相仿,只不过实现方法的细节不同,因此就不再在此次进行原理分析,具体原理可以参考我的上一篇文章,代码末尾会有相关的方法解释和说明

public class JWhelper {
    private static String usernam = "";//用户名
    private static String password = "";//密码
    private static String cheack = "";//验证码
    private static String lt = "";//页面lt
    private static String JSESSION = "";//cookies 1/2
    private static String ROUTE = ""; //cookies 2/2
    private static String iPlanetDirectoryPro = null;  //真实cookies
    private static String loginuri = "http://jwglxt.wtu.edu.cn"; //真实登录页地址
    //登录页面
    private static String url = "https://auth.wtu.edu.cn/authserver/login?service=http%3A%2F%2Fjwglxt.wtu.edu.cn%2Fsso%2Fjziotlogin";
    private static CloseableHttpClient httpClient = null; //浏览器
    private static CloseableHttpResponse response = null;//响应
    private static HttpGet httpGet = null;//GET请求
    private static HttpPost httpPost = null;//Post请求
    private static int statusCode = 0;//状态码
    private static String tepUri = "";//临时存储重定向页面
    private static String eff = "";//错误原因
    private static Bitmap checkIMG = null;//验证码
    private static List<Course> courses=new ArrayList<>();
    public JWhelper() {

    }

    //获取错误信息
    public String getEff() {
        return eff;
    }

    //获取验证码
    public Bitmap getCheckIMG() {
        return checkIMG;
    }


    //打开一个页面,解析验证码图片
    public boolean isCheckIMG() {
        RequestConfig config = RequestConfig.custom().setRedirectsEnabled(false).build();//不允许重定向
        httpClient = HttpClients.custom().setDefaultRequestConfig(config).build();//配置浏览器
        //解析网页
        httpGet = new HttpGet(url);
        try {
            response = httpClient.execute(httpGet);//执行
        } catch (IOException e) {
            return toEffexecute("网络错误");
        }
        statusCode = response.getCode();//获取状态码
        if (statusCode != 200) {
            return toEffexecute("错误代码:Get-1-" + statusCode);
        }
        //获取响应内容
        HttpEntity entity = response.getEntity();
        //将内容编码为字符串
        String vlue = null;
        try {
            vlue = EntityUtils.toString(entity, "UTF-8");
        } catch (IOException e) {
            return toEffexecute("解析错误:Get-1");
        } catch (ParseException e) {
            return toEffexecute("解析错误:Get-1");
        }
        Document document = Jsoup.parseBodyFragment(vlue);//转换为文档树
        Element body = document.body();//获取主体
        //找到lt
        lt = body.select("[name=lt]").attr("value");
        //得到Cookies
        Header[] headers = response.getHeaders("Set-Cookie");
        ROUTE = headers[0].getValue().split(";")[0].split("=")[1];
        JSESSION = headers[1].getValue().split(";")[0].split("=")[1];

        Log.i("JWXTEFF", "Cookies GET1 Jsession:" + JSESSION);
        Log.i("JWXTEFF", "lt: " + lt);
        Log.i("JWXTEFF", "Cookies GET1 Route:" + ROUTE);
        /*
         * 获取验证码
         * */
        String chakUrl = "https://auth.wtu.edu.cn/authserver/captcha.html";
        httpGet = new HttpGet(chakUrl);
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析验证码错误");
        }
        HttpEntity entityCheck = response.getEntity();
        InputStream inputStream = null;//获取字符流
        try {
            inputStream = entityCheck.getContent();
            checkIMG = BitmapFactory.decodeStream(inputStream);//读取图像数据
            inputStream.close();
        } catch (IOException e) {
            return toEffexecute("加载验证码失败");
        }
        return true;
    }

    /*
     * 第二步:POST登录 获取Location和iPlanetDirectoryPro
     * */
    public boolean login(String id, String pass, String code) {
        this.usernam = id;
        this.password = pass;
        this.cheack = code;
        String loURI = "https://auth.wtu.edu.cn/authserver/login;jsessionid=" + JSESSION + "?service=http%3A%2F%2Fjwglxt.wtu.edu.cn%2Fsso%2Fjziotlogin";
        ;
        httpPost = new HttpPost(loURI);
        //请求头
        httpPost.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
        httpPost.setHeader("Accept-Encoding", "gzip, deflate, br");
        httpPost.setHeader("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
        httpPost.setHeader("Connection", "keep-alive");
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded");
        httpPost.setHeader("Cookie", "route=" + ROUTE + "; JSESSIONID_auth=" + JSESSION);
        httpPost.setHeader("Host", "auth.wtu.edu.cn");
        httpPost.setHeader("Origin", "https://auth.wtu.edu.cn");
        httpPost.setHeader("Referer", url);
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0");
        //参数
        List<NameValuePair> pairs = new ArrayList<>();//创建List集合,封装表单请求参数
        pairs.add(new BasicNameValuePair("username", usernam));
        pairs.add(new BasicNameValuePair("password", password));
        pairs.add(new BasicNameValuePair("captchaResponse", cheack));
        pairs.add(new BasicNameValuePair("lt", lt));
        pairs.add(new BasicNameValuePair("dllt", "userNamePasswordLogin"));
        pairs.add(new BasicNameValuePair("execution", "e1s1"));
        pairs.add(new BasicNameValuePair("_eventId", "submit"));
        pairs.add(new BasicNameValuePair("rmShown", "1"));
        //创建表单的Entity对象,将表单存入其中用UTF-8编码
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(pairs, Charset.forName("UTF-8"));
        //写入参数
        httpPost.setEntity(formEntity);

        //执行
        try {
            response = httpClient.execute(httpPost);
        } catch (IOException e) {
            return toEffexecute("发送请求失败");
        }

        //获取状态码,判断请求是否成功
        statusCode = response.getCode();
        if (statusCode != 302)
            return toEffexecute("用户名或密码错误 错误代码:" + statusCode);

        //拿到重定向地址
        tepUri = response.getHeaders("Location")[0].getValue();
        Log.i("JWXTEFF", "\nGET Uri2:" + tepUri);
        //拿到iPlanetDirectoryPro
        Header[] headerspost = response.getHeaders("Set-Cookie");
        iPlanetDirectoryPro = headerspost[2].getValue().split(";")[0];
        Log.i("JWXTEFF", "iPlanetDirectoryPro:" + iPlanetDirectoryPro);
        return true;
    }

    //获取真实请求地址
    public boolean getURI() {
        /*
         * 第三步:第二次GET,获取下一步的uri和中间cooki
         * */
        Log.e("JWXTEFF", "\n——————————————————————第二次GET——————————————————");
        String JSESSIONID2 = null;
        //第二次GET请求进入登录界面,拿到第二个JSESSIONID_iPlanetDirectoryPro
        httpGet = new HttpGet(tepUri);
        httpGet.setHeader("Cookie", iPlanetDirectoryPro);

        //执行
        try {
            response = httpClient.execute(httpGet);//执行
        } catch (IOException e) {
            return toEffexecute("解析失败 Get-2");
        }

        //获取状态码,判断请求是否成功
        statusCode = response.getCode();
        if (statusCode != 302)
            return toEffexecute("错误代码 Get-2:" + statusCode);

        Log.i("JWXTEFF", "Status code Get2:" + statusCode);
        //取得重定向地址
        tepUri = response.getHeaders("Location")[0].getValue();
        Log.i("JWXTEFF", "Get Uri 3 :" + tepUri);
        //得到set-Cookie 拿到JSESSIONID2
        JSESSIONID2 = response.getHeaders("Set-Cookie")[0].getValue().split(";")[0];
        Log.i("JWXTEFF", "JSESSIONID2: " + JSESSIONID2);

        /*
         * 第四步 第三次GET 获取下一步uri
         * */
        Log.e("JWXTEFF", "\n——————————————————————第三次GET——————————————————");
        httpGet = new HttpGet(tepUri);
        httpGet.setHeader("Cookie", JSESSIONID2);

        //执行
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析失败 Get-3");
        }

        //获取状态码,判断请求是否成功
        statusCode = response.getCode();
        if (statusCode != 302)
            return toEffexecute("错误代码 Get-3:" + statusCode);

        Log.i("JWXTEFF", "Status code Get3:" + statusCode);
        //获取重定向地址
        tepUri = response.getHeaders("Location")[0].getValue();
        Log.i("JWXTEFF", "Get Uri 4:" + tepUri);




        /*
         * 第五步 第四次GET 获取下一步uri和真实的Cookie
         * */
        Log.e("JWXTEFF", "\n——————————————————————第四次GET——————————————————");
        httpGet = new HttpGet(tepUri);
        httpGet.setHeader("Cookie", iPlanetDirectoryPro);
        //执行
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析失败 Get-4");
        }

        //获取状态码,判断请求是否成功
        statusCode = response.getCode();
        if (statusCode != 302)
            return toEffexecute("错误代码 Get-4-" + statusCode);

        Log.i("JWXTEFF", "Status code Get4:" + statusCode);
        //获取重定向地址
        tepUri = "http://jwglxt.wtu.edu.cn" + response.getHeaders("Location")[0].getValue();
        Log.i("JWXTEFF", "Get Uri 5:" + tepUri);
        //得到set-Cookie 拿到真实
        JSESSION = response.getHeaders("Set-Cookie")[0].getValue().split(";")[0];
        Log.i("JWXTEFF", "JSESSIONID: " + JSESSION);

        /*
         * 第六部 第五次GET 获取最终的登录页面
         * */
        Log.e("JWXTEFF", "\n——————————————————————第五次GET——————————————————");
        httpGet = new HttpGet(tepUri);
        httpGet.setHeader("Cookie", iPlanetDirectoryPro + ";" + JSESSION);
        //执行
        try {
            response = httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析失败 Get-5");
        }

        //获取状态码,判断请求是否成功
        statusCode = response.getCode();
        if (statusCode != 302)
            return toEffexecute("错误代码 Get-5-" + statusCode);

        Log.i("JWXTEFF", "Status code Get5:" + statusCode);
        //得到真实请求地址
        loginuri += response.getHeaders("Location")[0].getValue();
        Log.i("JWXTEFF", "Loginuri :" + loginuri);

        /*
         * 第七部 第六次GET 登录教务系统
         * */
        Log.e("JWXTEFF", "\n——————————————————————登录页面——————————————————");
        httpGet=new HttpGet(loginuri);
        httpGet.setHeader("Cookie", iPlanetDirectoryPro + ";" + JSESSION);
        //获取状态码,判断请求是否成功
        try {
            response=httpClient.execute(httpGet);
        } catch (IOException e) {
            return toEffexecute("解析失败 Get-End");
        }
        statusCode=response.getCode();
        if(statusCode!=200){
            return toEffexecute("错误代码 Get-End-"+statusCode);
        }
        Log.i("JWXTEFF", "Status code Getlog:" + statusCode);
        return true;
    }

    //解析课表-----注意 2020年的上学期参数是 2019,12  (即选课的时候)  2020下学期是 2020,3
    public boolean isCourse(String year,String semester){
        Log.e("JWXTEFF", "\n——————————————————————课表——————————————————");
        String kcbUri = "http://jwglxt.wtu.edu.cn/kbcx/xskbcx_cxXsKb.html?gnmkdm=N253508";
        httpPost=new HttpPost(kcbUri);
        //设置请求头
        httpPost.setHeader("Accept", "*/*");
        httpPost.setHeader("Accept-Encoding", "gzip, deflate, br");
        httpPost.setHeader("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
        httpPost.setHeader("Connection", "keep-alive");
        httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
        httpPost.setHeader("Cookie", iPlanetDirectoryPro + ";" + JSESSION);
        httpPost.setHeader("Host", "auth.wtu.edu.cn");
        httpPost.setHeader("Origin", "https://auth.wtu.edu.cn");
        httpPost.setHeader("Referer", "http://jwglxt.wtu.edu.cn/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N253508&layout=default&su=" + usernam);
        httpPost.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0");
        httpPost.setHeader("X-Requested-With", "XMLHttpRequest");
        //表单数据
        List<NameValuePair> pairs = new ArrayList<>();
        pairs.add(new BasicNameValuePair("xnm", year));
        pairs.add(new BasicNameValuePair("xqm", semester));
        //创建表单的Entity对象,将表单存入其中用UTF-8编码
        UrlEncodedFormEntity formEntity =new UrlEncodedFormEntity(pairs, Charset.forName("UTF-8"));

        //写入参数
        httpPost.setEntity(formEntity);
        try {
            response=httpClient.execute(httpPost);
        } catch (IOException e) {
           return toEffexecute("获取课表错误");
        }
        statusCode=response.getCode();
        if(statusCode!=200){
            return toEffexecute("错误代码-PostKCB-" + statusCode);
        }
        Log.i("JWXTEFF", "PostKCB-" + statusCode);
        HttpEntity kcb=response.getEntity();
        //解析Json
        JSONObject jsonObject = null;
        try {
            jsonObject = JSON.parseObject(EntityUtils.toString(kcb,"UTF-8"));
        } catch (Exception e) {
            return toEffexecute("解析课表错误-JSON");
        }
        if(jsonObject==null||jsonObject.get("kbList") == null){
            return toEffexecute("暂时没有发现课表");
        }
        JSONArray timeTable = JSON.parseArray(jsonObject.getString("kbList"));
        for (Iterator iterator = timeTable.iterator(); iterator.hasNext();){
            JSONObject lesson = (JSONObject) iterator.next();
            Course course=new Course();
            course.setId(usernam);
            course.setName(lesson.getString("kcmc"));
            course.setCampus(lesson.getString("xqmc"));
            course.setRoom(lesson.getString("cdmc"));
            course.setWeek(lesson.getString("xqjmc"));
            //————————————————————————设置开始和结束节——————————————————————————
            String str=lesson.getString("jcs");
            //仅保留 -和数字
            String[] strs=str.replaceAll("[^\\d-]", "").split("-");
            int min=1;
            try {
                 min=Integer.parseInt(strs[0]);
            } catch (Exception e) {
                min=1;
            }
            int max=min;
            try {
                max=Integer.parseInt(strs[1]);
            } catch (Exception e) {
                max=min;
            }
            course.settMin(min);
            course.settMax(max);
            //——————————————————————设置开始和结束周————————————————————————
            str=lesson.getString("zcd");
            strs=str.replaceAll("[^\\d-]", "").split("-");
            try {
                min=Integer.parseInt(strs[0]);
            } catch (Exception e) {
                min=1;
            }
            try {
                max=Integer.parseInt(strs[1]);
            } catch (Exception e) {
                max=min;
            }
            course.setwMin(min);
            course.setwMax(max);
            course.setTeacher(lesson.getString("xm"));
            course.setJob(lesson.getString("zcmc"));
            course.setTest(lesson.getString("khfsmc"));
            courses.add(course);
        }
        return true;
    }

    //获取课表
    public List<Course> getCourses(){
        return courses;
    }


    //错误提示
    private boolean toEffexecute(String 错误提示) {
        eff = 错误提示;
        Log.e("JWXTEFF", 错误提示);
        return false;
    }
}

说明:
isCheckIMG()用于用Get方法获取验证码,并解析,之后可以调用getCheckIMG()获取到解析的图片,并显示在界面上
login(String id, String pass, String code)方法在其中判断返回的状态码是否为302判断登录是否成功并使用返回一个boolean,为此这个方法还可以进行一些扩展操作例如判断绑定学号
getURI()方法获取最终的Cookie和URI,并存储在内存中,同样也可以扩展,列如获取学生信息、获取成绩、抢课等操作
isCourse(String year,String semester)方法用于获取课程表,并提供JSON解析,结果用getCourses()方法返回一个课程数组,同样结合getURI(),按照本方法的范例可以实现获取学生信息、获取成绩、抢课、一键教学评价等更多操作;

写入课程到数据库的方法就不在这里赘述了


显示课程表

上文已经定义了XML布局,这里直接用代码来实现刚刚留下的空白

1、获取参数

首先获取屏幕宽度,并且平均为8,用于之后定位课程位置与大小

   int d = getWindowManager().getDefaultDisplay().getWidth() / 8;

2、获取当前周数

利用Calendar取得当前的日期和周数,显示到布局上的星期上,并且将当天的背景显示颜色突出
需要注意的是这里使用的SharedPreferences来判断和保存用户是否设置过周数

TextView[] weekTexts = {weekOne, weekTwo, weekSan, weekSi, weekWU, weekLiu, weekDAY};
        //获取当前周数
        int weeks = Calendar.getInstance().get(Calendar.WEEK_OF_YEAR);
        //打开SP
        SharedPreferences sp = getSharedPreferences(KCB, MODE_PRIVATE);
        //判断是否设置周数
        if (sp.getBoolean("flag", false)) {
            //读取设置时候的,如果没有就是weeks,默认为1
            int theWeek = sp.getInt("week", 1);
            //读取设置时候的日期
            int y = sp.getInt("year", 2020);
            int m = sp.getInt("math", 3);
            int day = sp.getInt("day", 3);
            //读取设置时候的日期
            Calendar calendar = new GregorianCalendar(y, m, day);//日期对象
            //获取设置那天的周数
            int theWeekofY = calendar.get(Calendar.WEEK_OF_YEAR);
            Log.d("TAGG", "iniView: " + theWeekofY);
            //得到当前周数
            weeks = (weeks - theWeekofY) + theWeek;
             }
        weekNums.setText(weeks + "周");
        //获取当前周几,转换为中国周(周一为第一天)
        int weekday = Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 1;
        //设置日期
        for (int i = 0; i < 7; i++)
        weekTexts[i].append("\n" + getDate(i - weekday));
         TextView textNow = new TextView(this);
        LinearLayout.LayoutParams paramNow = new LinearLayout.LayoutParams(d, dip2px(14 * 65));
        textNow.setBackgroundColor(0x40E91E63);
        paramNow.setMargins((weekday - 1) * d, 0, 0, 0);
        textNow.setLayoutParams(paramNow);
        PFF.addView(textNow);

3、读取数据库中的课程信息

这里用到了之前上文定义的getCourses(cursor) 方法,直接返回了数组,两行代码就完成了,是不是非常方便

 Cursor cursor = getContentResolver().query(CourseProvider.URI_COURSE, null, null, null, null);
        List<Course> courses = Course.getCourses(cursor);

4、判断是否非本周并给课程表上色

这里用到了一个变量i,由于每个课程最多会有4个相邻的课程,为了避免显示到一起颜色相同不便分辨我设置了5种不同的颜色,这样可以尽量避免相邻的两个课程颜色相同,并且比较本周与起止周数的大小判断本周是否有课,如果没课直接涂灰,并加上[非本周]提示,拼接课程信息用于显示;

int i=0
for (final Course course : courses) {
            //新建文本
            final TextView textView = new TextView(this);
            //设置颜色
            i++;
            switch (i % 5) {
                case 0:
                    textView.setBackgroundColor(0xffCDDC39);
                    break;
                case 1:
                    textView.setBackgroundColor(0xff22ccff);
                    break;
                case 2:
                    textView.setBackgroundColor(0xff22ffcc);
                    break;
                case 3:
                    textView.setBackgroundColor(0xff00BCD4);
                    break;
                case 4:
                    textView.setBackgroundColor(0xffF44336);
                    break;
            }
            //显示字符串
            String str = "";

            //获取当前课的周期,转换为纯数字和-,按-分隔
           int MIN=course.getwMin();
           int MAX=course.getwMax();
            if (MIN > weeks || weeks > MAX) {
                str += "[非本周]";
                //设置灰色
                textView.setBackgroundColor(0xcccccccc);
            }
            str += course.getName();
            str += "@" + course.getRoom();
            str += "#" + course.getTeacher() + course.getJob();
            str += "|" + course.getTest();
            textView.setText(str);

5、定位并显示课程

如上文所诉的原理,工具起止节和星期,分别设置高度,和左、上边距,从而达到定位课程的效果,最后添加到FrameLayout,显示课表;

 MIN=course.gettMin();
            MAX=course.gettMax();
            int height =MAX-MIN+1;
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(d, dip2px(height* 65));
            //设置位置
            int week = 6;
            switch (course.getWeek()) {
                case "星期一":
                    week = 0;
                    break;
                case "星期二":
                    week = 1;
                    break;
                case "星期三":
                    week = 2;
                    break;
                case "星期四":
                    week = 3;
                    break;
                case "星期五":
                    week = 4;
                    break;
                case "星期六":
                    week = 5;
                    break;
                case "星期七":
                    week = 6;
                    break;
                default:
                    break;
            }
            //左边-星期,  上边-节
            params.setMargins(week * d, dip2px((MIN - 1) * 65), 0, 0);
            textView.setLayoutParams(params);
            textView.setTextSize(12);
            textView.setTextColor(Color.WHITE);
            //结尾显示省略号
            textView.setEllipsize(TextUtils.TruncateAt.END);
            //添加到容器
            PFF.addView(textView);

6、内部方法:dp转px

手机屏幕和方法内设置的数值单位是px,XML中的布局单位是DP,为了保持一致,查阅资料后发现了一个方法,直接将dp 转换为px:

  public int dip2px(float dpValue) {
        Context context = this;
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

至此这个APP搭建完毕省略了登录页面,这个实现方法已经在上文JWhelper()中实现,具体界面可以自己实现,本文实在是太长了,不在赘述,康康效果吧!

效果演示

从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第8张图片
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第9张图片
从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第10张图片

从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程_第11张图片


后记

以上就是安卓APP抓取教务系统课表等信息并显示的主要部分分析和代码实现,本项目的源码我已经开源放到了GitHub上,大家可以在GitHub上下载源码进行扩展创作:

点击跳转GitHub Android-JAVA-ZFeducation-system

目前已经实现了如下功能:
1、通过教务系统判断学号密码
2、模拟登录抓取课程信息
3、根据课程信息显示课程表
4、动态判断是否本周
5、自定义添加编辑删除课程
6、自动判断课程导入
7、高亮显示当天
并且预留了以下实现接口:
(一个人一次性写完太难了/(ㄒoㄒ)/~~
1、通过教务系统读取学籍信息抓取照片
2、抓取成绩
3、空教室查询
3、POST选课
4、教学评价
5、新闻和通知获取
因此,欢迎大家拷贝下载,一起完善该项目!

写文章不易,有用的话记得点个赞哦
转载请注明出处

你可能感兴趣的:(从零开始开发一个自动抓取教务系统课表等信息并动态显示的安卓课程表APP,原理分析及功能实现完美教程)