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