本篇博客主要和大家分享編寫(xiě)一個(gè)學(xué)校教務(wù)系統(tǒng)的客戶(hù)端版本,主要是關(guān)于登錄以及數(shù)據(jù)獲取方面,結(jié)尾還會(huì)附上本人以前編寫(xiě)的客戶(hù)端源代碼,有興趣的可以自行下載玩耍~
閱讀本文大概需要5分鐘。
前言
好久沒(méi)有更新博客了,最近有點(diǎn)忙。今天對(duì)之前在學(xué)校做的一個(gè)項(xiàng)目開(kāi)源,并以正方教務(wù)系統(tǒng)為例,分享下如何抓取教務(wù)系統(tǒng)的數(shù)據(jù)~ 好了廢話(huà)不多說(shuō)直接開(kāi)始。
分析
搭建一個(gè)App,首先離不開(kāi)的肯定就是數(shù)據(jù),在通常情況下,App的數(shù)據(jù)都是由服務(wù)器提供的接口返回的,但是一般來(lái)說(shuō),學(xué)校都是不會(huì)把數(shù)據(jù)以及服務(wù)器提供給學(xué)生的,所以就要采取一些非正常手段。我們知道,網(wǎng)頁(yè)是由瀏覽器解析html代碼后展現(xiàn)出來(lái)的,那么只要我們拿到html代碼,自己抓取html里我們所需要的數(shù)據(jù),就能完成對(duì)數(shù)據(jù)的獲取了。
這里我使用的是一個(gè)能方便處理html文本的java庫(kù)Jsoup,對(duì)于它的具體用法可以參考我之前的文章《Android利用Jsoup抓取數(shù)據(jù),再也不怕寫(xiě)App沒(méi)有數(shù)據(jù)啦》,這里就不再贅述了。
登錄
Cookie保存
通常我們使用瀏覽器去訪(fǎng)問(wèn)我們的教務(wù)系統(tǒng)的時(shí)候,服務(wù)器都是通過(guò)cookie來(lái)對(duì)我們當(dāng)前的狀態(tài)進(jìn)行判斷以便獲取我們的登錄狀態(tài),那么為了能讓我們的登錄狀態(tài)得以持續(xù),以便我們后續(xù)對(duì)其他數(shù)據(jù)的抓取,我們?cè)诳蛻?hù)端中需要對(duì)cookie進(jìn)行一下存儲(chǔ)。
因?yàn)槲也捎玫氖荗kHttp來(lái)作為網(wǎng)絡(luò)請(qǐng)求,所以這里以O(shè)kHttp為例
OkHttpClient okHttpClient = new OkHttpClient.Builder().
connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS).
readTimeout(READ_TIMEOUT, TimeUnit.SECONDS).
writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS).
cookieJar(new OkHttpCookieJar()).
build();
public class OkHttpCookieJar implements CookieJar {
private Map<String, List<Cookie>> cookieStore = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookieStore.put(url.host(), cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
List<Cookie> cookies = cookieStore.get(url.host());
return cookies != null ? cookies : new ArrayList<Cookie>();
}
}
這里我只將其存入到了一個(gè)map中,并沒(méi)有對(duì)cookie進(jìn)行持久化存儲(chǔ)(比如通過(guò)SharedPreferences)等等,所以意味著每次重新打開(kāi)客戶(hù)端都需要登錄一遍,大家可以根據(jù)自己的需求進(jìn)行改造。
模擬登錄
首先我們需要先抓取到登錄的接口,以Chrome為例,按F12打開(kāi)開(kāi)發(fā)者工具,然后選擇Network,勾選Preserve log。
然后進(jìn)行一次正常的登錄,就可以抓取到登錄的url以及請(qǐng)求頭,表單數(shù)據(jù)等等(圖片對(duì)一些敏感數(shù)據(jù)做了處理)。
可以看到請(qǐng)求頭以及表單所需要的內(nèi)容,根據(jù)你所填的賬號(hào)密碼驗(yàn)證碼等等,很快就能判斷出對(duì)應(yīng)的key,以我之前學(xué)校為例的話(huà),TextBox1對(duì)應(yīng)賬號(hào),TextBox2對(duì)應(yīng)密碼,TextBox3對(duì)應(yīng)驗(yàn)證碼,RadioButtonList1就是身份了,然后你肯定發(fā)現(xiàn)了,_VIEWSTATE是什么鬼,因?yàn)檫@個(gè)正方教務(wù)系統(tǒng)是用Asp.net寫(xiě)的,那個(gè)_VIEWSTATE就是.net的,這里我們不探究它到底做啥用的,據(jù)我觀(guān)察,這個(gè)值并不是永遠(yuǎn)不變的,所以這里我們肯定是要在每次登錄的時(shí)候獲取它并把它放到表單里,那從哪里獲取它呢。還是一樣,F(xiàn)12然后查看登錄頁(yè)面的html源碼,
可以發(fā)現(xiàn)這個(gè)_VIEWSTATE的變量值就存在于form表單中,那么一切都很簡(jiǎn)單了,先獲取一次登錄頁(yè)面,拿到了_VIEWSTATE的值之后,在登錄的時(shí)候?qū)⑦@個(gè)值一起post上去就可以了。即為拿到登錄頁(yè)面的html源碼,使用Jsoup篩選出需要的值,然后登錄的時(shí)候一并post上去
String __VIEWSTATE = Jsoup.parse(html).select("input[name='__VIEWSTATE']").val();
這里不再贅述Jsoup的具體用法,可以參考我之前的文章。以O(shè)kHttp為例,附上簡(jiǎn)單的登錄代碼
RequestBody requestBody = new MultipartBody.Builder().
addFormDataPart("__VIEWSTATE", __VIEWSTATE ).
addFormDataPart("TextBox1", username).
addFormDataPart("TextBox2", password).
addFormDataPart("TextBox3", verificationCode).
addFormDataPart("Button1", "").
addFormDataPart("RadioButtonList1", "學(xué)生").build();
Request request = new Request.Builder().url(loginUrl).post(requestBody).build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
整個(gè)登錄流程如下
關(guān)于驗(yàn)證碼,這里要補(bǔ)充一點(diǎn),即請(qǐng)求驗(yàn)證碼圖片的cookie要和你登錄的時(shí)候一致,驗(yàn)證碼才能通過(guò),從代碼角度來(lái)說(shuō),以O(shè)kHttp為例,你需要用同一個(gè)OkHttp對(duì)象去完成請(qǐng)求驗(yàn)證碼以及登錄等等(就是不要new 兩個(gè)對(duì)象啦)~
抓取數(shù)據(jù)
登錄成功后,我們現(xiàn)在已經(jīng)能夠拿到各個(gè)模塊的數(shù)據(jù)了,那么一切都好辦了。具體怎么拿這里以獲取課表為例,同理其他的獲取成績(jī)等等均是這個(gè)思路
正方教務(wù)系統(tǒng)的首頁(yè)一般都是這個(gè)樣子的,我們老規(guī)矩,F(xiàn)12查看一下html源碼
可以看到,各個(gè)模塊的url均能拿到,老規(guī)矩,直接拿到源碼,Jsoup解析一下
Map<String, String> urlMap = new HashMap<>();
Document document = Jsoup.parse(html);
Elements elements = document.select("ul.nav li.top ul.sub li a");
for (Element element : elements) {
String value = "教務(wù)網(wǎng)的host" + "/" + element.attr("href").toString();
String key = element.text();
urlMap.put(key, value);
}
return urlMap;
這里我直接保存到map集合中,因?yàn)閔tml中的url是在同個(gè)域下的,所以抓取出來(lái)的url是不包含域名的,這里我們手動(dòng)把它拼上就可以了,現(xiàn)在我們拿到對(duì)應(yīng)模塊的url,還是老套路,按照所需要的參數(shù)進(jìn)行訪(fǎng)問(wèn),拿到html源碼
按照規(guī)則使用Jsoup進(jìn)行解析就行了,這里就不再贅述了,最后效果如下
總結(jié)
因?yàn)槠鶈?wèn)題,所以本文難以很細(xì)致的講清楚整個(gè)項(xiàng)目的每個(gè)細(xì)節(jié),只能大概的將整個(gè)思路分享出來(lái),如果有興趣的也可以自行clone源碼進(jìn)行查看,為了方便大家查看demo的效果,我在demo里已經(jīng)放入了一些html靜態(tài)頁(yè)面,不用賬號(hào)密碼即可直接登錄。
源碼地址:教務(wù)管理系統(tǒng)
關(guān)于快速替換為自己學(xué)校的教務(wù)系統(tǒng)
如果你學(xué)校的教務(wù)系統(tǒng)也是正方,那么這里提供一下比較快速的替換方法,但可能由于css樣式等差異,具體可能還是需要微調(diào),就需要你根據(jù)你學(xué)校教務(wù)系統(tǒng)的html源碼進(jìn)行調(diào)整了。
1.首先,CommonUtils.java中的isDemo改為false
public class CommonUtils {
public static boolean isDemo = true; // 改為false
....
}
2.將/app/src/main/res/values/api.xml下的url替換為你學(xué)校對(duì)應(yīng)的url
3.運(yùn)行App,看哪里解析有問(wèn)題,針對(duì)你學(xué)校教務(wù)系統(tǒng)的html代碼,根據(jù)css樣式等差異進(jìn)行微調(diào)。
如果覺(jué)得對(duì)你有所幫助,請(qǐng)點(diǎn)個(gè)贊,謝謝。你的鼓勵(lì)是我最大的動(dòng)力。
歡迎關(guān)注EoniJJ的簡(jiǎn)書(shū)
不定期與你分享關(guān)于Android開(kāi)發(fā)的點(diǎn)點(diǎn)滴滴。