模仿超级课程表——抓取学校课表数据

本文参考自:打造超级课程表

一、显示课表页面的制作

①、介绍

用过的超标的同学都知道,超标的课表页面是可以滑动的,并且背景为透明色,可以任意修改背景。
效果展示:

②、制作流程

那么如何制作出这样的表格呢?

(1)、首先我们得知道,Android提供了哪些可以用来制作表格的控件。

详情参照:

如何制作表格(1)——TableLayout

如何制作表格(2)——GridLayout

如何作表格(3)——GridView+RecyclerView

(2)、我们可以从效果图上看出,整个布局分为两个部分头部和可滑动的部分。

头部的制作

首先:选择什么样控件来制作头部的格子:

大家一看发现只有一行的表格,但是第一列的大小和其他列的大小是不一样的。大家肯定马上加想到了用TableLayout和GridLayout来制作头部的表格,当然如果不嫌麻烦可以直接用TextView堆加成一行。由于GridView生成的格子大小一致,所以不可以使用。(Recycler是可以制作的,但是总感觉有点大材小用了)。

个人选择GridLayout来制作该头部。(当然小伙伴也可以使用其他的控件来进行头部的制作,看看哪个更方便)

其次:设置每个格子的高度,和宽度。由于第一行比较特殊还需要分离出来。

本人是获取屏幕大小,然后分成15分,第一行占1份,其他行占2分来确定。

最后:分割线的确定

本人选择了,使用<View>的方式来添加了竖直分割线,并利用drawable中的<shape>来设置边框。(当然这不是最简便,和最有效的方法)

最后的效果:

添加头部:

    <GridLayout
        android:id="@+id/main_grid_title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/table_row_height"  //设置表格的高度为50
        android:background="@drawable/table_frame"       //背景
        android:rowCount="1"
        android:columnCount="15">
    </GridLayout>
columCount = "15" :其中有7列用来填充分割线的(这种做法,分割线和表格内容没有分开,容易混乱,不建议使用。本人懒癌发作了,请见谅。

背景:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/transparent"/>   //中间背景色为透明。
    <stroke
        android:color="@color/blue"
        android:width="1dp"/>
</shape>

利用代码填充表格内容:

//表格的内容
private static final String [] TITLE_DATA = {"9月","周一","周二","周三","周四","周五","周六","周日"};

private GridLayout mGlClsTitle;
//屏幕宽度的1/15
private int mTableDistance;
protected void onCreateView(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    mGlClsTitle = getViewById(R.id.main_grid_title);
    
		mTableDistance = getScreenPixelWidth()/15;
}

//设置表格的头部
private void setUpClsTitle(){
    for (int i=0; i<TITLE_DATA.length; ++i){
        String content = TITLE_DATA[i];
        //设置LayoutParams
        GridLayout.LayoutParams params = new GridLayout.LayoutParams();
        //第一列的时候
        if (i == 0){
            params.width = mTableDistance;//宽度为总宽度的1/15
        }
        else {
            //添加分割线
            View divider = getLayoutInflater().inflate(R.layout.grid_title_form,mGlClsTitle,false);
            mGlClsTitle.addView(divider);

            params.width = mTableDistance * 2;
        }
        params.height = GridLayout.LayoutParams.MATCH_PARENT;
        TextView textView = new TextView(this);
        textView.setTextColor(getResources().getColor(R.color.blue));
        textView.setText(content);
        textView.setGravity(Gravity.CENTER);
        mGlClsTitle.addView(textView,params);
    }
}

显示课程内容表格的制作:

首先:选择使用哪种控件来制作表格:

我们可以知道,有些课程需要合并多行、多列来显示课程。所以毫无疑问只能使用GridLayout来制作表格。

其次:设置每行的高度和宽度,并添加课程的节数,及其背景边框

然后:使用ScrollView添加滑动效果

效果:
 <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:scrollbars="none">
        <GridLayout
            android:id="@+id/main_grid_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:rowCount="13"
            android:columnCount="8">
        </GridLayout>
    </ScrollView>

     private static final int GRID_ROW_COUNT = 12;
     private static final int GRID_COL_COUNT = 8; <pre code_snippet_id="1909809" snippet_file_name="blog_20161001_5_6741136" name="code" class="java" style="font-size: 18px;">
    //初始化课表显示的格子
    private void setUpClsContent(){
        //设置每行第几节课的提示
        for(int i=0; i<GRID_ROW_COUNT+1; ++i){
            int row = i;
            int col = 0;
            GridLayout.LayoutParams params = new GridLayout.LayoutParams(
                    GridLayout.spec(row),GridLayout.spec(col)
            );
            params.width = mTableDistance;
            if (i == 0){
                params.height = 0;//第一行不显示
            }
            else {
                params.height = (int) getResources().getDimension(R.dimen.table_row_height);
            }
            TextView textView = new TextView(this);
            textView.setTextColor(getResources().getColor(R.color.blue));
            textView.setText(i+"");
            textView.setGravity(Gravity.CENTER);
            textView.setBackground(getResources().getDrawable(R.drawable.table_frame));
            mGlClsContent.addView(textView,params);
        }
 
  
 
  
 
  
 
  
<span style="font-weight: normal;">//初始化表格的距离
        for (int i=1; i<GRID_COL_COUNT; ++i){
            int row = 0;
            int col = i;
            GridLayout.LayoutParams params = new GridLayout.LayoutParams(
                    GridLayout.spec(row),GridLayout.spec(col)
            );
            params.width = mTableDistance*2;
            params.height = (int) getResources().getDimension(R.dimen.table_row_height);

            View view = new View(this);
            mGlClsContent.addView(view,params);
        }</span>
(3)、模拟显示数据
首先:创建Course类
其次:获取背景颜色素材并设置背景四角弯曲效果
           素材:
    <color name="blue">#378BE0</color>
    <color name="white">#fff</color>
    <color name="transparent">#00000000</color>
    
    <color name="light_blue">#9960BFE5</color>
    <color name="light_green">#9968CA5E</color>
    <color name="light_pink">#99F49C97</color>
    <color name="hole_blue">#9993AAE2</color>


然后:自定义Course的内容

最后:将Course添加到新建的表格中去

public class Course {
    //星期几:周一到周日
    private int day;
    //第几节课:总共12节
    private int clsNum;
    //每节课的长度
    private int clsCount;
    //随机的颜色
    private int color;
    //课程名
    private String clsName;

    public int getClsNum() {
        return clsNum;
    }

    public void setClsNum(int clsNum) {
        this.clsNum = clsNum;
    }

    public String getClsName() {
        return clsName;
    }

    public void setClsName(String clsName) {
        this.clsName = clsName;
    }

    public int getColor() {
        return color;
    }

    public void setColor(int color) {
        this.color = color;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }
    public int getClsCount() {
        return clsCount;
    }

    public void setClsCount(int clsCount) {
        this.clsCount = clsCount;
    }

    @Override
    public String toString() {
        return "StuClass{" +
                "clsCount=" + clsCount +
                ", day=" + day +
                ", clsNum=" + clsNum +
                ", color=" + color +
                ", clsName='" + clsName + '\'' +
                '}';
    }
}



其次:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners
        android:radius="8dp"/> //弯曲四个角
</shape>

    private void showCls(){
        for (int i = 0; i< mStuCourseList.size(); ++i){
            Course course = mStuCourseList.get(i);
            int row = course.getClsNum();
            int col = course.getDay();
            int size = course.getClsCount();
            //设定View在表格的哪行那列
            GridLayout.LayoutParams params = new GridLayout.LayoutParams(
                    GridLayout.spec(row,size),
                    GridLayout.spec(col)
            );
            //设置View的宽高
            params.width = mTableDistance*2;
            params.height = (int) getResources().getDimension(R.dimen.table_row_height) * size;
            params.setGravity(Gravity.FILL);
            //通过代码改变<Shape>的背景颜色
            GradientDrawable drawable = (GradientDrawable) getResources().getDrawable(R.drawable.cls_bg);
            drawable.setColor(getResources().getColor(course.getColor()));           
            //设置View
            TextView textView = new TextView(this);
            textView.setTextColor(getResources().getColor(R.color.white));
            textView.setText(course.getClsName());
            textView.setGravity(Gravity.CENTER);
            textView.setBackground(drawable);
            //添加到表格中
            mGlClsContent.addView(textView,params);
        }
    }

二、从正方教育系统提取课表数据

效果图:


①、登陆教育系统,观察需要提交的数据

第一点:正方使用的是Session,而不是Cookie。(关与Cookie与Session的差别请Google)说明当你登陆正方的任意一个网站的时候,网站就会返回一个Cookie,而不是像知乎一样当你登陆成功之后,返回一个Cookie给你。(这是个大坑,要注意)
(刚登陆的时候,就得到了一个Cookie。所以正方系统是无法实现持久化登陆的)

第二点:正方系统需要使用验证码登陆(网上说有绕过验证码的方法,但是没一个能成功),所以需要首先获取验证码,然后进行登陆,获取POST请求的地址。对于网络交互本文使用OkHttp请求

关于如何使用浏览器抓包,与OkHttp请求的使用请移步: OkHttp实现模拟登陆知乎

获取验证码的地址及上传参数:
模仿超级课程表——抓取学校课表数据_第1张图片
获取登陆请求的地址及上传参数:

模仿超级课程表——抓取学校课表数据_第2张图片 _VIEWSTATE:是默认的参数,直接复制下来就可以了,这是因为.Net编程的网站需要用这个参数进行验证。

RadioButtonList1:可能显示值为unable to decode value。该内容可以从网站的源码中找到(单击右键,查看网络源代码)

最后查询出来的结果是"学生"

②、模拟登陆学校官网

(1)、创建登陆页面

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">
    <EditText
        android:id="@+id/login_et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:hint="请输入账号"/>
    <EditText
        android:id="@+id/login_et_pwd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp"
        android:hint="请输入密码"
        android:inputType="textPassword"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <EditText
            android:id="@+id/login_et_codes"
            android:layout_width="200dp"
            android:layout_height="wrap_content"
            android:layout_marginRight="20dp"
            android:hint="请输入验证码"/>
        <ImageView
            android:id="@+id/login_iv_codes_img"
            android:layout_width="90dp"
            android:layout_height="40dp"
            android:scaleType="fitXY"/>
    </LinearLayout>
    <Button
        android:id="@+id/login_btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent"
        android:text="@string/login"
        android:textColor="@color/white"/>
</LinearLayout>

(2)、初始化封装一层OkHttp请求

public class HttpConnection {
    private static final long TIME_OUT = 10000;

    private OkHttpClient mClient;
    private static HttpConnection sConnection;
    private final Map<String,List<Cookie>> mCookieMap = new HashMap<>();
    private HttpConnection(){
        mClient = new OkHttpClient.Builder()
                .connectTimeout(TIME_OUT, TimeUnit.MILLISECONDS)
                .readTimeout(TIME_OUT,TimeUnit.MILLISECONDS)
                .cookieJar(new MyCookieJar())
                .build();
    }

    public static HttpConnection getInstance(){
        if (sConnection == null){
            sConnection = new HttpConnection();
        }
        return sConnection;
    }

    /*存储Cookie的类*/

    /**
     * 由于网站不能够持久化登陆,就直接将Cookie值放在HashMap中了
     */
    class MyCookieJar implements CookieJar{
        @Override
        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
            mCookieMap.put(url.host(),cookies);
        }

        @Override
        public List<Cookie> loadForRequest(HttpUrl url) {
            List<Cookie> cookieList = mCookieMap.get(url.host());
            return cookieList == null ? new ArrayList<Cookie>() : cookieList;
        }
    }

    public void saveCookie(HttpUrl url,List<Cookie> cookies){
        //如果url相同则替换
        mClient.cookieJar().saveFromResponse(url,cookies);
    }

    public List<Cookie> getCookies(HttpUrl url){
        //未判断Http是否合法
        return mClient.cookieJar().loadForRequest(url);
    }

    /*默认使用异步加载*/
    public void connectUrl(Request request, Callback callback){
        Call call = mClient.newCall(request);
        call.enqueue(callback);
    }

    public interface HttpCallBack <T>{
        void callback(T data);
    }
}
(3)、创建URLManager,管理网址
/**
 * Created by PC on 2016/9/25.
 * 存储需要用到的网址
 */
public class URLManager {
    //登陆的首页
    public static final String URL_BASE = "http://202.115.80.153";
    //登陆的验证码
    public static final String URL_CODES = "http://202.115.80.153/CheckCode.aspx?";
    //登陆的判定提交地址
    public static final String URL_LOGIN = "http://202.115.80.153/default2.aspx";
    //登陆之后的跳转页面
    public static final String URL_REFERER = "http://202.115.80.153/xs_main.aspx?xh=XH";
    //课程表
    public static final String URL_CLS = "http://202.115.80.153/xskbcx.aspx?xh=XH";
}

(4)、创建LoginService,封装登陆的服务,并创建回调接口,将回调设置在主线程中
逻辑:首先获取验证码,显示在ImageView上,然后在输入用户名密码进行登陆
/**
 * Created by PC on 2016/9/25.
 * 登陆到学校的教务网
 * 需要设置为单例模式
 */
public class LoginService {
    public static final String TAG = "LoginService";

    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private Context mContext;
    //设置登陆的Post参数
    private String rudioButton = "学生";
    private String state = "dDwyODE2NTM0OTg7Oz7QBx05W486R++11e1KrLTLz5ET2Q==";
    private String button1 = "";
    private String language = "";
    private String hidPdrs = "";
    private String hidsc = "";

    private HttpConnection mConnection = HttpConnection.getInstance();

    public LoginService(Context context){
        mContext = context;
    }

    //获取验证码

    /**
     * 获取验证码的时候,会返回给客户端一个Cookie
     * 作用:获取验证码
     * @param httpCallBack
     */
    public <T extends Bitmap> void getCodesImg(final HttpConnection.HttpCallBack<T> httpCallBack) throws IOException{
        URL codeUrl = new URL(URLManager.URL_CODES);
        final Request request = new Request.Builder()
                .url(codeUrl)
                .build();
        Callback callback = new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG,"出错");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()){
                    //用流优化当收到的数据比较大
                    byte [] bytes = response.body().bytes();
                    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
                    final Bitmap bitmap =  BitmapFactory.decodeStream(bais);
                    //切换线程环境
                    changeEnvironment(httpCallBack,bitmap);
                    response.close();
                }
            }
        };
        mConnection.connectUrl(request,callback);
    }

    public <T> void login(final String username, String pwd, final String codes, final HttpConnection.HttpCallBack<T> httpCallBack) throws IOException{
        URL url = new URL(URLManager.URL_LOGIN);
        FormBody formBody = new FormBody.Builder()
                .add("__VIEWSTATE",state)
                .add("txtUserName",username)
                .add("TextBox2",pwd)
                .add("txtSecretCode",codes)
                .add("RadioButtonList1",rudioButton)
                .add("Button1",button1)
                .add("lbLanguage",language)
                .add("hidPdrs",hidPdrs)
                .add("hidsc",hidsc)
                .build();
        final Request request = new Request.Builder()
                .url(url)
                .post(formBody)
                .build();
        Callback callback = new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                Log.d(TAG,"错误");
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()){
                    boolean isLogin = false;
                    //判断是否登陆成功   注释①
                    if(CourseParse.
                            parseIsLoginSucceed(response.body().string())){
                        isLogin = true;
                    }
                    else {
                        //显示登陆失败。
                        isLogin = false;
                    }
                    changeEnvironment(httpCallBack,isLogin);
                    response.close();
                }
            }
        };
        mConnection.connectUrl(request,callback);
    }

    private <T> void changeEnvironment(final HttpConnection.HttpCallBack httpCallBack, final T data){
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                httpCallBack.callback(data);
            }
        });
    }
}
注:如何判断是否登陆成功:
如果登陆失败,返回的Html文档,为登陆页面,
如果登陆成功后,返回的Html文档中,有某某同学,欢迎你,这段信息在id为xhxm的span里,成功后解析菜单。
(解析工具使用Jsoup)

(5)、创建CourseParase解析Html文本(是否登陆成功)

    public static boolean parseIsLoginSucceed(String data){
        Document doc = Jsoup.parse(data);
        //查看登陆文档。
        Element element = doc.getElementById("xhxm");
        if (element != null){
            return true;
        }
        return false;
    }

(6)、这个时候我们就相当于进入到了学生的信息页面( http://xxx.xxx.xx.xxx/xs_main.aspx?xh="你的学号")
之后,我们点击获取进入课表的链接地址(通过抓包查询):
发现是Get请求。然后创建CourseService类
public class CourseService {
    public static final String TAG = "CourseService";

    private final Handler mHandler = new Handler();
    private HttpConnection mConnection = HttpConnection.getInstance();

    public <T extends List<Course>> void getCourse(String username, final HttpConnection.HttpCallBack<T> callBack) throws IOException {
        //因为xh为学号,需要自己设置
        String urlCls = URLManager.URL_CLS.replace("XH",username);
        //重点:因为该网站实现的是动态跳转(有没有发现跳转到课表页面,但是网址没有变化)
        //必须添加Referer到Http头,Referer指的是当前页面的网址,否则会拒绝访问
        String referer = URLManager.URL_REFERER.replace("XH",username);
        Log.d(TAG,urlCls);
        URL url = new URL(urlCls);
        Request.Builder builder = new Request.Builder();
        builder.addHeader("Referer",referer);
        builder.url(url);
        final Request request = builder.build();

        Callback callback = new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                //网络较差环境下的问题
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                //登陆到课表,并解析课程表的资源
                if (response.isSuccessful()){
                    //其实这里可以使用InputStream进行优化的。以后再说吧
                    final String clsDocument = response.body().string();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            T stuClassList = (T) CourseParse.parsePersonal(clsDocument);
                            callBack.callback(stuClassList);
                        }
                    });
                    response.close();
                }
            }
        };
        mConnection.connectUrl(request,callback);
    }
}
(7)解析课表数据,并转化为Course(在CourseParase类中)
这里的课表数据解析情况,根据各位的课表情况而定= =,如果照搬可能出现问题
/*解析个人课表*/
    public static List<Course> parsePersonal(String data){
        List<Course> courses = new ArrayList<>();
        Document doc = Jsoup.parse(data);
        //首先获取Table
        Element table = doc.getElementById("Table1");
        //然后获取table中的td节点
        Elements trs = table.select("tr");
        //移除不需要的参数,这里表示移除前两个数值。
        trs.remove(0);
        trs.remove(0);
        //遍历td节点
        for (int i=0; i<trs.size(); ++i){
            Element tr = trs.get(i);
            //获取tr下的td节点,要求
            Elements tds = tr.select("td[align]");
            //遍历td节点
            for(int j=0; j<tds.size(); ++j){
                Element td = tds.get(j);
                String str = td.text();
                //如果数值为空则不计算。
                if (str.length() != 1){
                    //解析文本数据
                    str = parsePersonalCourse(str);
                    Course course = new Course();
                    course.setClsName(str);
                    course.setDay(j+1);
                    course.setClsCount(Integer.valueOf(td.attr("rowspan")));
                    course.setClsNum(i+1);
                    Random random = new Random();
                    int num = random.nextInt(COLOR.length);
                    course.setColor(COLOR[num]);
                    courses.add(course);
                }
            }
        }
        return courses;
    }

    private static String parsePersonalCourse(String text){
        //正则表达式获取课名,和教室
        Pattern courseNamePattern = Pattern.compile("^.+?(\\s{1})");
        Matcher courseNameMatcher = courseNamePattern.matcher(text);
        courseNameMatcher.find();
        String str = courseNameMatcher.group(0);

        Pattern courseLocPattern = Pattern.compile("\\s{1}(\\d+)");
        Matcher courseLocMatcher = courseLocPattern.matcher(text);
        courseLocMatcher.find();
        String data = courseLocMatcher.group(0);

        return str+"@"+data;
    }
好了,到这里,超级课程表就出炉了~~~~~吼吼

详细代码: CourseScheduleDemo







你可能感兴趣的:(模仿超级课程表——抓取学校课表数据)