如果你是在校大学生,或许你用多了各种课程表,比如课程格子,超级课程表。它们都有一个共同点就是可以一键导入教务处的课程。那么一直都是用户的我们,没有考虑过它是如何实现的。那么现在就来模仿一款”超级课程表“。
PS:由于超级课程表是商用软件,原本提取了一些图片,但是为了避免涉及侵权问题,所有图片均已使用一张绿色圆圈代替,背景图片也以颜色代替,缺乏美观,如果你觉得太丑,可以自己寻找图片代替。
那么说了这么久,先来看看这款高仿的软件长什么样子。本文的代码做过精简,所以界面可能有出入。
好了,界面太丑,不忍直视,先暂时忽略,本文的重点不是UI,而是如何提取课程。
先做下准备工作。
-
HttpWatch抓包分析工具。此工具的使用后文介绍
-
Litepal数据持久化orm,郭大神的大作,挺好用的orm,用法详见郭霖博客。
-
Async-android-http 数据异步请求框架,这里主要用到这个框架的异步请求以及session保持的功能,或许大多数人没有使用过这个框架的会话保持功能,反正个人觉得就是一神器,操作十分简单,就1句话,不然用HttpClient可能就没那么简单了,要自己写好多内容。具体用法参见github
-
Jsoup网页内容解析框架,可支持jquery选择器。可以支持从本地加载html,远程加载html,支持数据抽取,数据修改等功能,如果能灵活运用这个框架,那么你想抓取什么东西都不在话下。
既然要导入课程表,那么一定要登录教务处,结论是需要教务处的账号密码,这个好办,每个学生都有账号密码。那么怎么登录呢,这个当然不是我们人工登录了,只要提供账号密码,由程序来帮我们完成登录过程以及课程的提取过程。如果登录?首先打开教务处登录界面,打开HttpWatch进行跟踪。输入账号,密码,验证码(验证码视具体学校不同,有些学校不含验证码,有些学校含验证码,验证码的处理后文进行说明),输入完成后点击登录,再点击查看课程的菜单,之后停止HttpWatch录制,把文件保存一下进行分析。打开保存后的文件,查看登录时提交的参数及一些信息,记录下来,同时记录查看课程页提交的参数及信息。
先看登录页面提交的参数,参数均是POST提交,这可以通过HttpWatch看到提交方式
__VIEWSTATE:有这个值页面生成的,这里我直接使用这个固定值而不去抓取,这个值是.net根据表单参数自动生成的。理论上同一个页面是不会变动的。
Button1:传空值即可
hidPdrs:传空值即可
lbLanguage:传空值即可
RadioButtonList1:图上是乱码,通过查看网页源代码可知该值是学生,因为我们是以学生的角色登录的
TextBox2:这个值是密码,传密码即可
txtSecrect:这个值是验证码,传对应的验证码即可
txtUserName:这个值是学号,传学号即可
你以为只要提交这些参数就好了吗,那么你就错了,我们还有设置请求头信息,如下图
我们不必设置所有请求头信息,只需要设置Host,Referer,User-Agent(可不设)。
请求头设置完毕了,那么来说一个重大的问题,就是验证码的问题,这里有三种方式供选择。
-
在登录之前抓取验证码,显示出来,供用户输入。
-
使用正方的bug1,为什么是bug1呢,因为后面一种方法利用了bug2,bug1,bug2不一定所有学校适用,正方的默认登录页面是default2.aspx,如果这个页面有验证码,你可以试试default1.aspx-default6.aspx六个页面,运气好的话可能会有不需要验证码的页面。这时候你使用该页面进行登录即可(提交参数会不同,具体自己抓包分析)
-
使用正方的bug2,不得不说这个bug2,大概是某个程序猿在某男某月某日无意间留下的把,那么怎么使用这个bug呢,很简单,登录的时候直接传验证码为空值或者空字符串过去就好了,有人说,你他妈逗我,这都行,恩,真的行。为什么行呢,原因可能是正方后台程序没有判断传过来的值是不是空。我们模拟登录的时候并没有去请求验证码的页面,所有不会产生验证码(此时为空字符串或者空值)和cookie,当我们提交空验证码时,后台接收到的值就是空字符串,两个空字符串做比较当然相等了,以上只是猜测,毕竟正方是.net的,.net的处理机制本人不是很清楚。
说了这么多理论知识,来点实际的把,先完成登录界面的代码
12345678910111213<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,这一步必须在请求之前设置。
12345678/**
* 初始化Cookie
*/
private
void
initCookie(Context context) {
//必须在请求前初始化
cookie =
new
PersistentCookieStore(context);
HttpUtil.getClient().setCookieStore(cookie);
}
那么HttpUtil又是什么呢,很简单,就是一个请求用的工具类
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206package
cn.lizhangqu.kb.util;
import
org.apache.http.Header;
import
android.app.ProgressDialog;
import
android.content.Context;
import
android.widget.Toast;
import
cn.lizhangqu.kb.service.LinkService;
import
com.loopj.android.http.AsyncHttpClient;
import
com.loopj.android.http.AsyncHttpResponseHandler;
import
com.loopj.android.http.BinaryHttpResponseHandler;
import
com.loopj.android.http.RequestParams;
/**
* Http请求工具类
* @author lizhangqu
* @date 2015-2-1
*/
/**
* @author Administrator
*
*/
public
class
HttpUtil {
private
static
AsyncHttpClient client =
new
AsyncHttpClient();
// 实例话对象
// Host地址
public
static
final
String HOST = ***.***.***.***;
// 基础地址
public
static
final
String URL_BASE = https:
//***.***.***.***/;
// 验证码地址
public
static
final
String URL_CODE = https:
//***.***.***.***/CheckCode.aspx;
// 登陆地址
public
static
final
String URL_LOGIN = https:
//***.***.***.***/default2.aspx;
// 登录成功的首页
public
static
String URL_MAIN = https:
//***.***.***.***/xs_main.aspx?xh=XH;
// 请求地址
public
static
String URL_QUERY = https:
//***.***.***.***/QUERY;
/**
* 请求参数
*/
public
static
String Button1 = ;
public
static
String hidPdrs = ;
public
static
String hidsc = ;
public
static
String lbLanguage = ;
public
static
String RadioButtonList1 = 学生;
public
static
String __VIEWSTATE = dDwyODE2NTM0OTg7Oz7YiHv1mHkLj1OkgkF90IvNTvBrLQ==;
public
static
String TextBox2 =
null
;
public
static
String txtSecretCode =
null
;
public
static
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
*/
public
static
void
get(String urlString, AsyncHttpResponseHandler res) {
client.get(urlString, res);
}
/**
* get,url里面带参数
*
* @param urlString
* @param params
* @param res
*/
public
static
void
get(String urlString, RequestParams params,
AsyncHttpResponseHandler res) {
client.get(urlString, params, res);
}
/**
* get,下载数据使用,会返回byte数据
*
* @param uString
* @param bHandler
*/
public
static
void
get(String uString, BinaryHttpResponseHandler bHandler) {
client.get(uString, bHandler);
}
/**
* post,不带参数
*
* @param urlString
* @param res
*/
public
static
void
post(String urlString, AsyncHttpResponseHandler res) {
client.post(urlString, res);
}
/**
* post,带参数
*
* @param urlString
* @param params
* @param res
*/
public
static
void
post(String urlString, RequestParams params,
AsyncHttpResponseHandler res) {
client.post(urlString, params, res);
}
/**
* post,返回二进制数据时使用,会返回byte数据
*
* @param uString
* @param bHandler
*/
public
static
void
post(String uString, BinaryHttpResponseHandler bHandler) {
client.post(uString, bHandler);
}
/**
* 返回请求客户端
*
* @return
*/
public
static
AsyncHttpClient getClient() {
return
client;
}
/**
* 获得登录时所需的请求参数
*
* @return
*/
public
static
RequestParams getLoginRequestParams() {
// 设置请求参数
RequestParams params =
new
RequestParams();
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);
return
params;
}
/**
* 接口回调
* @author lizhangqu
*
* 2015-2-22
*/
public
interface
QueryCallback {
public
String handleResult(
byte
[] result);
}
/**
* 登录后查询信息封装好的函数
* @param context
* @param linkService
* @param urlName
* @param callback
*/
public
static
void
getQuery(
final
Context context, LinkService linkService,
final
String urlName,
final
QueryCallback callback) {
final
ProgressDialog 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,
new
AsyncHttpResponseHandler() {
@Override
public
void
onSuccess(
int
arg0, Header[] arg1,
byte
[] arg2) {
if
(callback !=
null
) {
callback.handleResult(arg2);
}
Toast.makeText(context, urlName + 获取成功!!!, Toast.LENGTH_LONG)
.show();
dialog.dismiss();
}
@Override
public
void
onFailure(
int
arg0, Header[] arg1,
byte
[] arg2,
Throwable arg3) {
dialog.dismiss();
Toast.makeText(context, urlName + 获取失败!!!, Toast.LENGTH_SHORT)
.show();
}
});
}
}
地址信息被我处理掉了,替换成对应的地址即可,都是几个简单的函数,其中最后一个函数做了一个封装,代码自己读吧,这里就不讲了。。。。。
现在查看登录的代码。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253/**
* 登录
*/
private
void
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
;
}
final
ProgressDialog 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,
new
AsyncHttpResponseHandler() {
@Override
public
void
onSuccess(
int
arg0, Header[] arg1,
byte
[] arg2) {
try
{
String resultContent =
new
String(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
public
void
onFailure(
int
arg0, Header[] arg1,
byte
[] arg2,
Throwable arg3) {
Toast.makeText(getApplicationContext(), 登录失败!!!!,
Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
});
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081package
cn.lizhangqu.kb.service;
import
java.util.List;
import
org.jsoup.Jsoup;
import
org.jsoup.nodes.Document;
import
org.jsoup.nodes.Element;
import
org.jsoup.select.Elements;
import
org.litepal.crud.DataSupport;
import
cn.lizhangqu.kb.model.Course;
import
cn.lizhangqu.kb.model.LinkNode;
/**
* LinNode表的业务逻辑处理
* @author lizhangqu
* @date 2015-2-1
*/
public
class
LinkService {
private
static
volatile
LinkService linkService;
private
LinkService(){}
public
static
LinkService getLinkService() {
if
(linkService==
null
){
synchronized
(LinkService.
class
) {
if
(linkService==
null
)
linkService=
new
LinkService();
}
}
return
linkService;
}
public
String getLinkByName(String name){
List<linknode> find = DataSupport.where(title=?,name).limit(
1
).find(LinkNode.
class
);
if
(find.size()!=
0
){
return
find.get(
0
).getLink();
}
else
{
return
null
;
}
}
public
boolean
save(LinkNode linknode){
return
linknode.save();
}
/**
* 查询所有链接
*
* @return
*/
public
List<linknode> findAll() {
return
DataSupport.findAll(LinkNode.
class
);
}
public
String parseMenu(String content) {
LinkNode linkNode =
null
;
StringBuilder result =
new
StringBuilder();
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=
new
LinkNode();
linkNode.setTitle(element.text());
linkNode.setLink(element.attr(href));
save(linkNode);
}
return
result.toString();
}
public
String isLogin(String content){
Document doc = Jsoup.parse(content, UTF-
8
);
Elements elements = doc.select(span#xhxm);
try
{
Element element=elements.get(
0
);
return
element.text();
}
catch
(IndexOutOfBoundsException e){
//e.printStackTrace();
}
return
null
;
}
}</linknode></linknode>
判断是否登录成功的判断依据是看页面上是否有某某同学,欢迎你,这段信息在id为xhxm的span里,成功后解析菜单,因为不一定只是抓课表,也可能抓成绩,各种抓,所以这里把链接都记录下来,对应页面的源代码我会和代码一同上传。
如果你要使用验证码,则获取验证码即可,对应代码如下,就是获得验证码后显示在界面上
12345678910111213141516171819202122232425262728/**
* 获得验证码
*/
private
void
getCode() {
final
ProgressDialog dialog =CommonUtil.getProcessDialog(LoginActivity.
this
,正在获取验证码);
dialog.show();
HttpUtil.get(HttpUtil.URL_CODE,
new
AsyncHttpResponseHandler() {
@Override
public
void
onSuccess(
int
arg0, Header[] arg1,
byte
[] arg2) {
InputStream is =
new
ByteArrayInputStream(arg2);
Bitmap decodeStream = BitmapFactory.decodeStream(is);
code.setImageBitmap(decodeStream);
Toast.makeText(getApplicationContext(), 验证码获取成功!!!,Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
@Override
public
void
onFailure(
int
arg0, Header[] arg1,
byte
[] arg2,
Throwable arg3) {
Toast.makeText(getApplicationContext(), 验证码获取失败!!!,
Toast.LENGTH_SHORT).show();
dialog.dismiss();
}
});
}
LinkUtil里面是一些常量
1234567891011121314151617181920212223242526272829package
cn.lizhangqu.kb.util;
/**
* 首页菜单接口
* 用于定义linknode表中的标题
* @author lizhangqu
* @date 2015-2-1
*/
public
interface
LinkUtil {
public
static
final
String ZYXXK=专业选修课;
public
static
final
String QXXGXK=全校性公选课(通识限选);
public
static
final
String SYXK=实验选课;
public
static
final
String DJKSBM=等级考试报名;
public
static
final
String GRXX=个人信息;
public
static
final
String MMXG=密码修改;
public
static
final
String XSGRKB=学生个人课表;
public
static
final
String XSKSCX=学生考试查询;
public
static
final
String CJCX=成绩查询;
public
static
final
String DJKSCX=等级考试查询;
public
static
final
String JCSYXX=教材使用信息;
public
static
final
String XSXKQKCX=学生选课情况查询;
public
static
final
String XSBKKSCX=学生补考考试查询;
public
static
final
String XSXXYPJ=学生信息员评价;
public
static
final
String FKJGCX=反馈结果查询;
public
static
final
String JWGG=教务公告;
public
static
final
String BMJSKBCX=部门教师课表查询;
public
static
final
String QXKBCX=全校课表查询;
public
static
final
String JXRLCX=教学日历查询;
}
接下来是文章的重点,即如何解析课表。
-