Android模拟登陆教务系统爬取课程表
之前为项目做了个模拟登陆教务系统的爬虫,由于你懂的拖延症一直没把博客写出来,终于这天还是来写了。希望为大环境做一点点贡献,把中间的过程尽可能详细简单地写成博客分享出来。
我使用到的:
- IDE:AndroidStudio(使用框架:Jsoup,OkHttpUtils,RxAndroid)
- 抓包工具:HttpWatch Professional8.5 + IE (或是直接打开Chrome的开发者模式选择NetWork
这里安利一下HttpWatch~ 链接: https://pan.baidu.com/s/147dLjKLcFWJob9IPasVojw 提取码: age9
项目定义是学生在何处通过任何网络方式都能访问到教务系统,那么就不能局限于校园网,所以我采用的是学校提供的vpn访问教务系统以此模拟登陆行为。但是没关系,模拟教务登陆行为和校园网登陆是一样的,在这里不讨论vpn登陆等相关问题。
废话不多说,下面开始整个模拟登陆的流程:
## 1. 获取Cookie
我们都知道,cookie是网站用来辨别用户身份的数据,我们学校教务系统使用的就是cookie来跟踪分辨用户【当然这是抓包后知道的】,那么我们就需要发起一次get请求以此获取Cookie,同时记录在客户端。
/**
* 请求新的Cookie
*/
private void GetCookies() {
cookie = "";
Observable.create((ObservableOnSubscribe<String>) emitter -> OkHttpUtils.get()
.url(NetConfig.BASE_EDU_PLUS)
.build()
.execute(new Callback() {
@Override
public Object parseNetworkResponse(Response response, int id) throws Exception {
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
if (headers.name(i).equals("Set-Cookie"))
if (headers.value(i).endsWith(" Path=/"))
cookie += headers.value(i).substring(0, headers.value(i).length() - 7);
else
cookie += headers.value(i);
}
emitter.onNext(response.body().string());
return null;
}
@Override
public void onError(Call call, Exception e, int id) {
emitter.onError(new Throwable("GetCookies onError" + call.toString()));
}
@Override
public void onResponse(Object response, int id) {
Log.e("print", "GetCookies onResponse");
}
}))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(String s) {
GetVIEWSTATE(s);
//刷新验证码
GetVerifation();
}
@Override
public void onError(Throwable e) {
printLog(e.getMessage());
}
@Override
public void onComplete() {}
});
}
返回请求后,只获取Cookie还不够,我们还需要获取_VIEWSTATE参数,这一参数是登陆请求中必须包含的一个参数,等下登陆抓包的时候我们可以看到。_VIEWSTATE在返回的内容页面里,也就是教务系统的HTML源代码里,实质_VIEWSTATE是以后每次抓取像教务网发起请求都要传递的一段字符串,它隐藏在每一个页面的一个表单里。
/**
* 获取__VIEWSTATE
* body参数为HTML代码
*/
private void GetVIEWSTATE(String body) {
String x = body.split("name=\"__VIEWSTATE\" value=\"")[1];
x = x.split("\" />")[0];
// 这里我把__VIEWSTATE存到了SharedPreferences,其实没必要,存内存就好了
App.set__VIEWSTATE(this, x);
}
这样前序工作基本做好了,我们来抓包整个登陆过程发送的请求:
## 2. 抓取登陆过程的请求信息并进行登陆
登陆页面
登陆后抓包抓到的请求
我们选择第一个Post请求,这就是我们登陆所发起的请求了,点击下方的POST DATA,可以看到该请求携带的参数
可以看到其中有个RadioButtonList1参数的值乱码了,通过翻阅源码我们可以用 %D1%A7%C9%FA 代替,我估计这个应该是学生、教师选项由于编码问题导致的乱码。
__VIEWSTATE就是之前我们在第一次发起请求的时候获取到的值,这里直接填充进去就好了。txtSecretCode就是登陆时输入的验证码,txtUserName和TextBox2分别是学号和密码。另外没有参数值为空的我们用”“提交就行了。
此外,还有个很重要的参数就是Cookie,我们还需要在请求头里加入之前获取的Cookie。
登陆代码:
private void doLogin(String eduid, String pwd, String checkid) {
Observable.create((ObservableOnSubscribe<String>) emitter -> OkHttpUtils.post()
.url(NetConfig.BASE_EDU_PLUS)
.addParams("__VIEWSTATE", App.get__VIEWSTATE(this))
.addParams("Button1", "")
.addParams("hidPdrs", "")
.addParams("hidsc", "")
.addParams("lbLanguage", "")
.addParams("RadioButtonList1", "%D1%A7%C9%FA")
.addParams("TextBox2", pwd)
.addParams("txtSecretCode", checkid)
.addParams("txtUserName", eduid)
.addHeader("Cookie", cookie)
.build()
.execute(new Callback() {
@Override
public Object parseNetworkResponse(Response response, int id) throws Exception {
String responseHTML = new String(response.body().bytes(), "GB2312");
user_eduid = eduid;
user_edupwd = pwd;
emitter.onNext(responseHTML);
return null;
}
@Override
public void onError(Call call, Exception e, int id) {
emitter.onError(new Throwable("doLogin() onError " + call.toString() + " " + e.getMessage()));
}
@Override
public void onResponse(Object response, int id) {
Log.e("print", "doLogin() onResponse");
}
}))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<String>() {
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(String s) {
// 检查登陆是否成功
checkLoginSuccess(s);
}
@Override
public void onError(Throwable e) {
printLog(e.getMessage());
}
@Override
public void onComplete() {}
});
}
## 3. 检查登陆是否成功
到这里已经可以进行正常的登录了,但是我们还需要做检查是否登陆成功的判断,毕竟还会有验证码错误,密码错误等等非正常登陆的原因。
/**
* 检查是否登录成功
* loginresult为登陆后返回的HTML源码
*/
private void checkLoginSuccess(String loginresult) {
Document doc = Jsoup.parse(loginresult);
Elements alert = doc.select("script[language]");
Elements success = doc.select("a[href]");
Elements err = doc.select("p[class]");
Elements names = doc.select("#xhxm");
for (Element link : success) {
// 获取所要查询的URL,这里相应地址button的名字叫成绩查询
if (link.text().equals("等级考试查询")) {
printLog("登陆成功");
// 登陆成功后获取学生姓名,此处获取到的学生姓名为xx同学
String stuName = "";
for (Element name : names)
stuName = name.text();
// 去掉尾缀
int index = stuName.lastIndexOf("同学");
if (index >= 0)
stuName = stuName.substring(0, stuName.length() - 2);
printLog("stuName " + stuName);
AfterSuccessLogin(stuName);
return;
}
}
for (Element link : alert) {
//获取错误信息
if (link.data().contains("验证码不正确")) {
etCheck.setText("");
ToastShort("验证码输入错误");
printLog("验证码输入错误");
//刷新验证码
GetVerifation();
return;
} else if (link.data().contains("username不能为空")) {
ToastShort("学号为空");
printLog("学号为空");
//刷新验证码
GetVerifation();
return;
} else if (link.data().contains("password错误")) {
ToastShort("学号或密码错误");
printLog("学号或密码错误");
//刷新验证码
GetVerifation();
return;
} else if (link.data().contains("password不能为空")) {
ToastShort("密码为空");
printLog("密码为空");
//刷新验证码
GetVerifation();
return;
}
}
for (Element link : err) {
if (link.text().equals("错误原因:系统正忙!")){
ToastShort("错误原因:系统正忙!");
printLog("错误原因:系统正忙!");
GetCookies();
}
}
}
4. 验证码显示*
到这里基本上模拟登陆已经结束了,但是我毕竟写的是Android模拟登陆,但是上面是不是感觉缺了点什么——-我们要怎么获取验证码?
于是我们刷新验证码进行抓包,发现了验证码的请求信息:
我们复制右边的请求URL在浏览器打开,可以看到这就是我们需要获取的验证码了
知道了这些的我们就可以动手写代码了,另外请求头中必须带上Cookie不然是没法判断用户的喔。
/**
* 获取验证码图片
*/
private void GetVerifation() {
Observable.create((ObservableOnSubscribe<Bitmap>) emitter -> OkHttpUtils.post()
.url(NetConfig.CHECKIMG_URL_RS)
.addHeader("Cookie", cookie)
.build()
.execute(new BitmapCallback() {
@Override
public void onError(Call call, Exception e, int id) {
emitter.onError(new Throwable("EduLoginActivity GetVerifation onError"));
}
@Override
public void onResponse(Bitmap response, int id) {
printLog("GetVerifation onResponse");
emitter.onNext(response);
}
}))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Bitmap>() {
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(Bitmap bitmap) {
ivCheck.setImageBitmap(bitmap);
etCheck.setText("");
}
@Override
public void onError(Throwable e) {
printLog(e.getMessage());
}
@Override
public void onComplete() {}
});
}
这样我们在获取验证码后就可以把验证码放在我们自己的登陆页面上让用户自己输入验证码,当然我们也可以利用深度学习等方法去优化这一过程,在获取到bitmap后用训练好的模型来完成自动辨别验证码的功能。
5. 项目模块展示:
最后,课程表的部分我开另一个博客来介绍。 项目地址:https://github.com/WithLei/plusClub