【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩

今天这篇文章为大家带来的是模拟登录教务系统并抓取课表和成绩的详细实现过程。这个程序基于Android平台,大致的流程是首先使用OkHttp3网络请求框架来模拟登录教务系统,然后利用Jsoup库来解析获取到的html代码,最后只要处理下数据将其显示到界面上就可以了。运行程序后的效果如下图所示:

大家想看完整的源代码,可到文末的链接下载。之前也写过一个类似的程序,不过用的是HttpClient来进行网络请求,Android 5.0中已将其废弃,而Android 6.0也已移除了其相关的类,而且之前用的编辑器还是Eclipse,所以我重新编写了代码,使用Android Studio编辑程序,并换用了OkHttp库,我的上一篇文章:Android Studio从安装到配置,也刚好就能以这个项目作为实战程序,还能顺带熟悉下Android Studio。准备这篇文章我也挺用心的,有什么问题或者文章有什么错误都欢迎大家提出来,一起讨论交流,相互学习。


下面让我们先来捋一捋这篇文章所要分享大家哪几个方面的内容:

1. 如何使用HttpWatch工具,查看GET请求的地址,POST请求需要传递哪些参数,怎么设置header等等;

2. OkHttp3的基本使用方法,如何自动化管理Cookie等;

3. Jsoup的基本使用方法,用法很灵活,可通过DOM,CSS以及类似于jQuery的操作方法来操作数据;

4. 程序界面的设计。

接下来就正式开始进行详细说明了。



1. HttpWatch的使用方法

以前HttpWatch只能在IE浏览器中使用,现在也可以集成到火狐浏览器中使用。如果不想使用HttpWatch进行抓包,也可以使用Fiddler,这个工具也很强大。我们这里就还是介绍HttpWatch的用法,具体的打开方式是安装好HttpWatch Professional Edition后—>打开IE浏览器,点击“查看”这个菜单项—>选择“浏览器栏”—>最后选择“HttpWatch Professional”即可。成功打开后,现在就访问教务系统页面吧。点击Record按钮就可以开始抓包了。

P.S. 因为今年刚毕业,帐号已经被注销了,这里为了分享给大家这个程序的实现过程,找学弟借的帐号,下面相关的信息就打上马赛克了。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第1张图片


1.1 验证码

1.1.1 请求地址

以前正方教务系统有个bug,在用户名和密码都正确的情况下,登录时验证码直接传空值也能成功登录,这几天写代码的时候发现这个bug已经修复了,不正确填写验证码已经不能成功登录了。那我们先来看看验证码的请求地址吧,确保点击了Record按钮后,点击登录界面上的看不清换一张,从HttpWatch记录的信息可以知道发送了GET请求,这里我们最需要关心的是获取验证码的请求地址。

注:这里获取验证码和后面登录的操作,用的都是同一个Cookie。我后面会介绍怎么使用OkHttp自动化管理Cookie,所以这里的Cookie值是什么,不必关心。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第2张图片


1.2 登录系统

1.2.1 请求地址

填好用户名,密码及验证码后点击登录按钮,登录成功后,发现发送了POST请求,与GET请求不同的是我们不仅仅要关注请求的地址,还要关心请求Header以及Post的数据,而这三项可以分别在HttpWatch中的Overview, Header和POST Data这三个选项卡中查看。这里我们看到登录成功后,状态码是302,而不是200,说明这里进行了重定向,稍微关注一下,后面会提到我们的特殊处理。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第3张图片

1.2.2 请求头

同样的Cookie不用管,Headers我们就只用关心下面这三项。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第4张图片

注:现在我们来看看之前提到的状态码为302的问题,我们能发现302请求下面那个GET请求的URL和你浏览器地址栏的网址是一样的,也就是我们点击登录后,数据是发送给302那个地址的,最后才重定向到这个200的地址来。所以我们后面设置登录的Header时,Referer的值就直接设置这个状态码为200的地址就可以了,数据验证成功后直接跳转到这儿。

点击POST请求的Content标签页就能进一步验证我们的想法,这个是请求302那个地址成功后页面html代码,也就是说当数据验证成功后,会跳转到a标签的href属性里的地址,那个地址也就是200的GET请求URL,即系统主页,所以Referer直接设置它就行。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第5张图片

1.2.3 POST数据

POST的数据就每一条都要传了,没有值的传空字符串即可。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第6张图片

RadioButtonList1的值为乱码,我们可以去登录界面查看源代码就可以发现,RadioButtonList1是一个table的id,而我们登录时选择的是表格里的一个RadioButton,它的value是”学生”,所以我们后面传递参数的时候设成"学生"即可。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第7张图片

其实__VIEWSTATE也是通过取页面上控件的值所得,这个值据说是.net自动生成的,一般来说同一个页面,这个值不会变,不过我后面请求登录的时候还是每次都去取值,你也可以选择就用这个抓取到的数据,也是完全可以的。



1.3 查询课表

1.3.1 请求地址


1.3.2 请求头
还是这三个参数,只是跳转的Referer不同而已。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第8张图片

1.3.3 POST数据

__EVENTTARGET的值会根据你点击页面上的下拉框,最后改变的值学年还是学期来决定,如果最后点的是学年,值为xnd,点的是学期,值变为xqd。xnd是你所要查询的学年值,xqd是你要查询的学期值,而剩下的两个参数和之前一样,也是取得页面的固定的value值,后面请求就直接用抓取到的数据就行。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第9张图片


1.4 查询成绩

1.4.1 请求地址


1.4.2 请求头

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第10张图片

1.4.3 POST数据

ddlXN是你所要查询的学年值,ddlXQ是你要查询的学期值。那个乱码的值,是之前所点击按钮"按学期查询"的value值,和之前一样查看源代码就能看到value,剩下的还是页面上的固定值,请求时直接用就行。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第11张图片

注:查询课表和成绩时,我们用HttpWatch会发现,Referer的URL中包含中文字符,后面使用OkHttp设置Referer时,如果直接用这条包含中文的URL,会导致程序出错,所以需要对中文字符进行url编码:URLEncoder.encode(studentName, "gb2312");这点需要特别关注一下。



2. OkHttp3的基本用法

OkHttp是一款高效且强大的类库,目前最热门的网络请求框架之一。支持HTTP和HTTP/2协议,对相同地址的所有请求可共享同一Socket;使用连接池来减少请求延迟;支持GZIP缩小下载大小;以及响应缓存可以完全避免重复请求。用法也比较简单,同时支持同步和异步请求。最低支持在Android 2.3及Java 7.0的环境下使用。现在OkHttp在Github上已经更新到3.4.1了,如果需要在Android Studio上使用,需要项目的build.gradle中添加这条依赖:compile 'com.squareup.okhttp3:okhttp:3.4.1',以前说OkHttp内部依赖okio,不添加这个依赖请求会失败,现在只需要OkHttp这条依赖即可。因为是网络请求,还要记得去AndroidManifest.xml里添加网络权限:


2.1 OkHttp3和之前的版本的差异

① 创建OkHttpClient对象的方式不同:OkHttp3提供了Builder,调用其build()方法来创建;而之前的版本则是直接通过new OkHttpClient()来创建,没使用创建者设计模式。

② 设置Cookie的方法不同:OkHttp3使用Builder的CookieJar()方法来设置;而之前的版本则是使用OkHttpClient的setCookieHandler()方法。

③ 构建POST请求参数的方式不同:OKHttp3使用FormBody.Builder或MultipartBody.Builder来构建;而之前的版本使用的是FormEncodingBuilder,MultipartBuilder。

④ 异步请求的回调函数返回的参数不同:OkHttp3的请求成功和失败的callback分别是public void onFailure(Call call, IOException e){}public void onResponse(Call call, Response response){};以前的版本则是public void onFailure(Request request, IOException e){}和public void onResponse(final Response response){}

还有其他差异就不一一列举了。


2.2 自动化管理Cookie

自动化管理Cookie会给你带来极大地便利,你可以不用再去手动处理Cookie,你也不用关心Cookie的读取与保存。这里通过继承实现CookieJar这个类,然后使用OkHttpClient.Builder()的cookieJar()方法来设置,不过要注意的是你所有的请求都需要使用同一个OkHttpClient对象,不然使用的就不是同一个Cookie了。这个类的写法主要参考了stackoverflow中这个问题:Automatic cookie handling with OkHttp 3 里gncabrera的回答。下面贴出这个类的完整代码:

package com.example.webcrawler.utils;

import java.util.ArrayList;
import java.util.List;

import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;

public class MyCookieJar implements CookieJar {

    private static List cookies;

    @Override
    public void saveFromResponse(HttpUrl httpUrl, List cookies) {
        this.cookies =  cookies;
    }

    @Override
    public List loadForRequest(HttpUrl httpUrl) {
        if (null != cookies) {
            return cookies;
        } else {
            return new ArrayList();
        }
    }

    public static void resetCookies() {
        cookies = null;
    }
}
如何使用CookieJar()方法设置:

OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
mOkHttpClientBuilder.cookieJar(new MyCookieJar());
OkHttpClient mOkHttpClient = mOkHttpClientBuilder.build();


2.3 GET请求
2.3.1 同步的Http GET

OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
final OkHttpClient mOkHttpClient = mOkHttpClientBuilder.build();
new Thread(new Runnable() {

    @Override
    public void run() {
        Request request = new Request.Builder().url("https://www.google.com").build();
        Call call = mOkHttpClient.newCall(request);
        try {
            Response response = call.execute();
            // 或者直接Throw IOException
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
注:网络请求需放在子线程中执行

2.3.2 异步的Http GET
OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
OkHttpClient mOkHttpClient = mOkHttpClientBuilder.build();
Request request = new Request.Builder().url("https://www.google.com").build();
mOkHttpClient.newCall(request).enqueue(new Callback() {

    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});

注:回调函数是子线程,而非主线程

2.4 POST请求

2.4.1 同步的Http POST

OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
final OkHttpClient mOkHttpClient = mOkHttpClientBuilder.build();
new Thread(new Runnable() {

    @Override
    public void run() {
        Headers.Builder headersBuilder = new Headers.Builder()
                .add("headerKey", "headerValue");
        Headers requestHeaders = headersBuilder.build();

        FormBody.Builder formBodyBuilder = new FormBody.Builder()
                .add("paramKey", "ParamValue");
        RequestBody requestBody = formBodyBuilder.build();

        Request request = new Request.Builder()
                .url("https://www.google.com")
                .headers(requestHeaders)
                .post(requestBody)
                .build();
        try {
            Response response = mOkHttpClient.newCall(request).execute();
            // 或者直接Throw IOException
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

注:和同步的GET请求一样,要在子线程中执行
2.4.2 异步的Http POST

OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
final OkHttpClient mOkHttpClient = mOkHttpClientBuilder.build();
Headers.Builder headersBuilder = new Headers.Builder()
        .add("headerKey", "headerValue");
Headers requestHeaders = headersBuilder.build();

FormBody.Builder formBodyBuilder = new FormBody.Builder()
        .add("paramKey", "paramValue");
RequestBody requestBody = formBodyBuilder.build();

Request request = new Request.Builder()
        .url("https://www.google.com")
        .headers(requestHeaders)
        .post(requestBody)
        .build();
mOkHttpClient.newCall(request).enqueue(new Callback() {

    @Override
    public void onFailure(Call call, IOException e) {
        
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});
注:回调函数还是子线程

2.5 封装OkHttp3

这里将OkHttp3封装成了一个工具类OkHttpUtils以便调用,不然每次请求都要写很多相同的东西确实也是挺麻烦的。工具类的实现参考了鸿洋大神的Android OkHttp完全解析 是时候来了解OkHttp了,不过这篇博客用的是OkHttp2,而且很多东西我们用不到,所以我在理解的基础上,加入了自动化管理Cookie的代码,POST请求时添加了Header参数,方法也改为了OkHttp3的。我们的这个工具类用法也比较简单,比如要发送POST请求,可以将request header和body分别放到Map集合中,然后调用postAsync()方法将两个Map作为参数传进去即可。单例也符合我们之前提到的自动化管理Cookie的要求,使用同一个OkHttpClient对象来做请求操作。下面将工具类完整的代码贴出来:

package com.example.webcrawler.utils;

import android.os.Handler;
import android.os.Looper;

import java.io.IOException;
import java.util.Map;
import java.util.Set;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * http请求工具类, 封装了OkHttp3
 */
public class OkHttpUtil {
    private static OkHttpUtil mHttpUtil;
    private OkHttpClient.Builder mOkHttpClientBuilder;
    private OkHttpClient mOkHttpClient;
    private Handler mDelivery;

    private OkHttpUtil() {
        mOkHttpClientBuilder = new OkHttpClient.Builder();
        mOkHttpClientBuilder.cookieJar(new MyCookieJar());
        mOkHttpClient = mOkHttpClientBuilder.build();
        mDelivery = new Handler(Looper.getMainLooper());
    }

    private static OkHttpUtil getInstance() {
        if (mHttpUtil == null) {
            synchronized (OkHttpUtil.class) {
                if (mHttpUtil == null) {
                    mHttpUtil = new OkHttpUtil();
                }
            }
        }
        return mHttpUtil;
    }

    /**
     * 同步的Get请求, 返回Response对象
     */
    private Response _getSync(String url) throws IOException {
        final Request request = new Request.Builder().url(url).build();
        Call call = mOkHttpClient.newCall(request);
        Response response = call.execute();
        return response;
    }

    /**
     * 同步的Get请求, 返回string
     */
    private String _getSyncString(String url) throws IOException {
        Response response = _getSync(url);
        return response.body().string();
    }

    /**
     * 异步的Get请求
     */
    private void _getAsync(String url, final ResultCallback callback) {
        final Request request = new Request.Builder().url(url).build();
        deliveryResult(callback, request);
    }

    /**
     * 同步的Post请求, 返回Response对象
     */
    private Response _postSync(String url, RequestData[] params,
                               RequestData... headers) throws IOException {
        Request request = buildPostRequest(url, params, headers);
        Response response = mOkHttpClient.newCall(request).execute();
        return response;
    }

    /**
     * 同步的Post请求, 返回string
     */
    private String _postSyncString(String url, RequestData[] params,
                                   RequestData... headers) throws IOException {
        Response response = _postSync(url, params, headers);
        return response.body().string();
    }

    /**
     * 异步的post请求
     */
    private void _postAsync(String url, final ResultCallback callback,
                            RequestData[] params, RequestData... headers) {
        Request request = buildPostRequest(url, params, headers);
        deliveryResult(callback, request);
    }

    /**
     * 异步的post请求
     */
    private void _postAsync(String url, final ResultCallback callback,
                            Map params, Map headers) {
        RequestData[] paramsArr = mapToRequestDatas(params);
        RequestData[] headersArr = mapToRequestDatas(headers);
        Request request = buildPostRequest(url, paramsArr, headersArr);
        deliveryResult(callback, request);
    }

    /**
     * 将Map键值对数据转化为RequestData数组
     */
    private RequestData[] mapToRequestDatas(Map params) {
        int index = 0;

        if (params == null) {
            return new RequestData[0];
        }
        int size = params.size();

        RequestData[] res = new RequestData[size];
        Set> entries = params.entrySet();
        for (Map.Entry entry : entries) {
            res[index++] = new RequestData(entry.getKey(), entry.getValue());
        }
        return res;
    }

    /**
     * 构建post请求参数
     */
    private Request buildPostRequest(String url, RequestData[] params,
                                     RequestData... headers) {
        if (headers == null) {
            headers = new RequestData[0];
        }
        Headers.Builder headersBuilder = new Headers.Builder();
        for (RequestData header : headers) {
            headersBuilder.add(header.key, header.value);
        }
        Headers requestHeaders = headersBuilder.build();

        if (params == null) {
            params = new RequestData[0];
        }
        FormBody.Builder formBodyBuilder = new FormBody.Builder();
        for (RequestData param : params) {
            formBodyBuilder.add(param.key, param.value);
        }
        RequestBody requestBody = formBodyBuilder.build();
        return new Request.Builder()
                .url(url)
                .headers(requestHeaders)
                .post(requestBody)
                .build();
    }

    /**
     * 调用call.enqueue,将call加入调度队列,执行完成后在callback中得到结果
     */
    private void deliveryResult(final ResultCallback callback, Request request) {
        mOkHttpClient.newCall(request).enqueue(new Callback() {

            @Override
            public void onFailure(Call call, IOException e) {
                sendFailedStringCallback(call, e, callback);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                try {
                    switch (response.code()) {
                        case 200:
                            final byte[] bytes = response.body().bytes();
                            sendSuccessResultCallback(bytes, callback);
                            break;
                        case 500:
                            sendSuccessResultCallback(null, callback);
                            break;
                        default:
                            throw new IOException();
                    }

                } catch (IOException e) {
                    sendFailedStringCallback(call, e, callback);
                }
            }
        });
    }

    /**
     * 调用请求失败对应的回调方法,利用handler.post使得回调方法在UI线程中执行
     */
    private void sendFailedStringCallback(final Call call,
                                          final Exception e,
                                          final ResultCallback callback) {
        mDelivery.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null)
                    callback.onError(call, e);
            }
        });
    }

    /**
     * 调用请求成功对应的回调方法,利用handler.post使得回调方法在UI线程中执行
     */
    private void sendSuccessResultCallback(final byte[] bytes,
                                           final ResultCallback callback) {
        mDelivery.post(new Runnable() {
            @Override
            public void run() {
                if (callback != null) {
                    callback.onResponse(bytes);
                }
            }
        });
    }

    public static abstract class ResultCallback {
        public abstract void onError(Call call, Exception e);

        public abstract void onResponse(byte[] response);
    }

    /************************ 以下为外部可以调用的方法 ************************/

    public static class RequestData {
        String key;
        String value;

        public RequestData() {

        }

        public RequestData(String key, String value) {
            this.key = key;
            this.value = value;
        }
    }

    public static Response getSync(String url) throws IOException {
        return getInstance()._getSync(url);
    }

    public static String getSyncString(String url) throws IOException {
        return getInstance()._getSyncString(url);
    }

    public static void getAsync(String url, ResultCallback callback) {
        getInstance()._getAsync(url, callback);
    }

    public static Response postSync(String url, RequestData[] params,
                                    RequestData... headers) throws IOException {
        return getInstance()._postSync(url, params, headers);
    }

    public static String postSyncString(String url, RequestData[] params,
                                        RequestData... headers) throws IOException {
        return getInstance()._postSyncString(url, params, headers);
    }

    public static void postAsync(String url, final ResultCallback callback,
                                 RequestData[] params, RequestData... headers) {
        getInstance()._postAsync(url, callback, params, headers);
    }

    public static void postAsync(String url, final ResultCallback callback,
                                 Map params, 
                                 Map headers) {
        getInstance()._postAsync(url, callback, params, headers);
    }
}



3. Jsoup的基本用法

Jsoup是用来提取和操作存储在Html代码中的数据的Java开源库。用法简单,功能强大,提供了一套十分完备的API,让你能够轻松的解析Html代码。它可以从URL,字符串或文件中解析Html,Jsoup本身就可以发送GET,POST等一系列请求,并解析Html代码,但它也存在一些不足,如果页面代码是AJAX是动态生成的,那么可能会发生抓取不到数据的情况,可能要自己模拟AJAX请求才行。我们项目里也只是用Jsoup来解析OkHttp请求成功后返回的字符串数据。使用Android Studio编写代码,需要在项目的build.gradle中添加依赖:compile 'org.jsoup:jsoup:1.9.2'


3.1 创建Document对象

有四种方式可以创建,① 解析Html字符串:Document document = Jsoup.parse(html); ② 解析Html片段:Document document = Jsoup.parseBodyFragment(html); ③ 从URL加载Document:Document document = Jsoup.connect("http://www.google.com/").get();④ 从一个文件加载:File input = new File("/files/index.html"); Document document = Jsoup.parse(input, "UTF-8"); 只要拥有了一个Document,你就可以使用Document中合适的方法或它父类Element和Node中的方法来取得相关数据。


3.2 查找元素

3.2.1 使用类似DOM的方法查找

这里利用的是Elements对象提供的方法来查找元素:

① 通过id查找:getElementById(String id)

② 通过标签查找:getElementsByTag(String tag)

③ 通过类名称查找:getElementsByClass(String className)

④ 通过属性来查找:getElementsByAttribute(String key)

⑤ 通过同级元素查找:siblingElements(), firstElementSibling(), lastElementSibling(), nextElementSibling(), previousElementSibling()

⑥ 通过父节点,子节点查找:parent(), children(), child(int index)

3.2.2 使用select()方法查找

这是一种类似于CSS或jQuery选择器的语法的方法

① 选择器基本用法:

    Ⅰ #id: 通过ID查找元素
    Ⅱ .class: 通过class名称查找元素
    Ⅲ tagname: 通过标签查找元素
    Ⅳ [attribute]: 利用属性查找元素
    Ⅴ [attr=value]: 利用属性值来查找元素

② 选择器组合使用:

    Ⅰ ele#id: 元素+ID
    Ⅱ ele.class: 元素+class
    Ⅲ ele[attr]: 元素+class
    Ⅳ parent > child: 查找某个父元素下的直接子元素
    Ⅴ ancestor child: 查找某个元素包含的子元素

③ 伪选择器的用法

    Ⅰ :has(seletor): 查找匹配选择器包含元素的元素
    Ⅱ :contains(text): 查找包含给定文本的元素
    Ⅲ :matches(regex): 查找哪些元素的文本匹配指定的正则表达式


3.3 获取元素数据

① attr(String key):获取属性
② attributes():获取所有属性
③ id():获取id
④ className():获取class名
⑤ text():获取文本内容
⑥ html():获取元素内的HTML内容
⑦ outerHtml()获取元素外HTML内容
⑧ tagName():获取标签名


3.4 操作HTML和文本

① Element.html(String html):这个方法将先清除元素中的HTML内容,然后用传入的HTML代替。
② Element.prepend(String first) 和 Element.append(String last) 方法用于在分别在元素内部HTML的前面和后面添加HTML内容
③ Element.wrap(String around):对元素包裹一个外部HTML内容。

注:以上只是简单介绍了常用的方法,Jsoup还有很多其他的方法,你可以到Jsoup官网查看API,还可以通过Jsoup cookBook中文版学习基本用法。


3.5 运用Jsoup的实例

3.5.1 获取__VIEWSTATE的数值

部分HTML代码如下:

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第12张图片

我们会发现拥有name="__VIEWSTATE"这个属性的元素只有一个,所以我们就通过之前说过的选择器组合使用的方式,直接调用document.select("input[name=\"__VIEWSTATE\"]").first(); 来查找元素,再通过Elementattr()方法取得value属性。代码里还查找了__VIEWSTATEGENERATOR的value,是因为之前用HttpWatch记录请求信息的时候发现登录时会传递这个参数,所以你就根据需要传递的参数来解析取得相应的数据即可。

private Map _getViewStateValue(String html) {
    Map viewStateValue = new LinkedHashMap<>();
    if (null != html) {
        Document document = Jsoup.parse(html);
        Element viewstateElement = 
                document.select("input[name=\"__VIEWSTATE\"]").first();
        Element viewstateGeneratorElement = 
                document.select("input[name=\"__VIEWSTATEGENERATOR\"]").first();
        if (null != viewstateElement) {
            viewStateValue.put(Constants.LOGIN_BODY_NAME_VIEWSTATE, 
                    viewstateElement.attr("value"));
        }
        if (null != viewstateGeneratorElement) {
            viewStateValue.put(Constants.LOGIN_BODY_NAME_VIEWSTATEGENERATOR, 
                    viewstateGeneratorElement.attr("value"));
        }
    }
    return viewStateValue;
}

3.5.2 获取学生姓名或登录失败信息
登录成功时,部分HTML代码如下所示。学生姓名位于id为”xhxm”的span标签中,由于这个id是唯一的,所以直接通过Element nameElement = document.getElementById("xhxm"); 查找元素,在利用Elementhtml()方法取得内容,最后使用正则表达式取得学生姓名。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第13张图片

登录失败时,错误信息是通过alert()输出的,通过html代码就能发现,script标签有一个没有value的属性值"defer"是唯一的,所以使用selct()选择器的方式就能找到元素:Element infoElement = document.select("script[defer]").last(); 最后还是用正则表达式匹配中文字符。

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩_第14张图片    

private Map _getNameOrFailedInfo(String html) {
    Map returnInfo = new LinkedHashMap<>();
    if (null != html) {
        Document document = Jsoup.parse(html);
        Element nameElement = document.getElementById("xhxm");
        if (null != nameElement) {
            String studentName = nameElement.html();
            Pattern p = Pattern.compile("(.+)[^同学]");
            Matcher m = p.matcher(studentName);
            if(m.find()) {
                returnInfo.put(Constants.STUDENTNAME, m.group());
            }
        } else {
            // 找不到学生姓名,说明登录失败,跳转回了登录界面。这里取得登录失败的原因后返回
            Element infoElement = document.select("script[defer]").last();
            if (null != infoElement) {
                String login_failed_info = infoElement.html();
                Pattern p = Pattern.compile("([\\u4E00-\\u9FA5]+)");
                Matcher m = p.matcher(login_failed_info);
                if(m.find()) {
                    returnInfo.put(Constants.FAILEDINFO, m.group());
                }
            }
        }
    }
    return returnInfo;
}
Jsoup的基本用法就介绍到这里,项目完整的JsoupUtil代码就不贴出来了。说白了,利用Jsoup来抽取数据,重点是能找出所要查找元素的唯一特性,就能快速定位,最后利用抽取文本、属性等方法就能得到你想要的数据了。


4. 程序界面的实现

4.1 登录界面

登录界面还是比较常规的,主要使用线性布局,以及TextView,EditText,ImageView,Button等常见的控件构成。如果想要Activity以Dialog的形式出现,也很简单,只要在AndroidManifest.xml文件中指定该Activity对应的主题:android:theme="@style/LoginDialog",自定义的style文件如下所示:

直接继承appcompat-v7兼容包中的Dialog主题,通过android:windowBackground设置主题背景,windowNoTitle使title bar不出现。
登录界面的布局文件就如下所示:




    

        

        
    

    

    

    

        

        

        
    

    

        
    

    

        


4.2 设置验证码

当OkHttp成功获取验证码后,会返回的byte[]数据。我们可以通过BitmapFactory提供的decodeByteArray()方法将字节数组转化为Bitmap,但是如果直接将Bitmap设置为ImageView的background,验证码图片会特别小,所以再利用Matrix按比例缩放我们的Bitmap,最后就可以使用setBackground()方法设置ImageView的背景图片了。

/**
 * 设置验证码图片
 */
private void setVerificationCodeBg() {
    if (verificationCode != null && verificationCode.length > 0) {
        Bitmap bitmap = BitmapFactory.decodeByteArray(verificationCode, 0,
                verificationCode.length);
        Bitmap resizeBitmap = changeBitmapSize(bitmap, 140, 60);
        //iv_VerificationCode.setImageBitmap(resizeBitmap);
        iv_VerificationCode.setBackground(new BitmapDrawable(getResources(), 
                resizeBitmap));
        verificationCode = null;
    } else {
        iv_VerificationCode.setBackgroundResource(R.mipmap.loading_failed);
    }
}

/**
 * 用于改变验证码图片尺寸
 */
private Bitmap changeBitmapSize(Bitmap bitmap,float width,float height) {
    int w = bitmap.getWidth();
    int h = bitmap.getHeight();
    Matrix matrix = new Matrix();
    float scaleX = (float) width / w;
    float scaleY = (float) height / h;
    matrix.postScale(scaleX, scaleY);
    Bitmap resizeBitmap = Bitmap.createBitmap(bitmap, 0, 0, w, h, matrix, true);
    return resizeBitmap;
}

4.3 课程背景图
课程背景图通过自定义shape实现corners设置圆角弧度,padding设置了四个方向的间隔,gradient设置了45度的线性渐变。其中一个课程背景如下所示:




    
        
            

            

            
        
    


这篇文章到这儿就结束了。程序的大部分内容文中都有提及,大家感兴趣的话可以到下方链接下载完整的源代码,不过程序里Constants.java中的请求地址,请求参数等内容是不正确的,需自行修改,如果文中或程序有写得不好的地方欢迎大家提出来,有问题或者有更好的想法,也可以一起讨论。


程序源代码:

【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩(Android Studio)


【Android+OkHttp3+Jsoup】 模拟登录教务系统 抓取课表和成绩(Eclipse版)

你可能感兴趣的:(Android)