第一次寫CSDN博客,想一想還是有點小緊張。嘿嘿,希望能幫到和我一樣剛入門的菜鳥coder解決特定問題。如果有幸被各路大牛看到此文,還望不吝賜教,指正文章中可能存在的錯誤。
安卓課程表的最主要目的是將教務網中的課程表信息(後面稱之爲“數據”)獲取到,再顯示於安卓應用界面上。需要解決的問題無非只有兩個:1. 獲得“數據”;2. 將“數據”顯示在設計好的界面上。本文主要說如何獲得“數據”。
在瀏覽器上看到自己課程表需要如下兩個過程:
1. 登錄(下圖是學校教務網登錄界面):
2.點擊“本學期選課”,獲得課程表信息
安卓課程表就是模擬瀏覽器這一行爲,從教務網服務器上獲取到“數據”。如下圖:
登錄界面:
獲取課程表信息界面:
最開始,我以爲要實現類似“超級課程表”這樣的應用需要像新浪微博API那樣進行Oauth2認證,才能獲得微博上的指定數據的JSON或XML格式,自己再去解析。這樣豈不是需要向教務網索要接口等等。。。後來參考這篇博文:http://blog.csdn.net/u010858238/article/details/9029653(建議可以先直接去看這篇文章,如果看完後,具體實現中遇到問題,再回頭來看我寫的,可能我們遇到的問題一模一樣。後文也全是在假設已經看過這篇文章的基礎上寫的),發現利用Httpclient,這個過程無非就是在安卓系統上仿瀏覽器獲得數據的原理來實現,而且這樣對於任何有網頁端而無客戶端的東西都可以用類似的方法去實現某一網站的客戶端程序。
我參考的那篇文章已經非常清楚地教給大家原理,而我之所以再寫一篇文章的原因是:在自己實現安卓課程表的過程中,會遇到非常多意想不到的問題,而且整個過程並不是參考的那一篇文章所能解決的,原理性的東西只是起參考作用,實踐過程中需要自己去解決很多問題,比如:爲什麼獲得了cookie但總是提示無權限訪問,請先登錄?爲什麼一登錄程序就停止運行?
我在實現應用的過程中遵循下面的思路:
步驟1. 利用Httpclient登錄教務網,獲得可以繼續訪問網站後續功能的可以作爲“憑證”的cookie;
步驟2. 帶着這個cookie繼續訪問獲得課程表信息(後面稱之爲“數據”);
步驟3. 會發現步驟2中獲得的“數據”其實是課程表頁面的HTML文件(利用谷歌瀏覽器,在課程表網頁點擊右鍵審查元素看到的代碼),利用JSoup解析HTML文件獲得需要的指定信息,將之存儲於SQlite數據庫中,前端界面讀取數據並顯示在listview中(根據實際顯示界面而定)。
在步驟1中,我最開始陷入了誤區,以爲只要獲得cookie就可以繼續訪問,但實際情況是:獲得cookie並開始執行步驟2時總是提示無權限訪問,請先登錄。對這個問題,谷歌了幾乎所有與Httpclient有關的博文,英文幫助文檔,看完了網上關於HTTP協議的各種講解後,發現坑不在這裏,而在學校教務網與自己的理解上。對於cookie,我最初以爲只有登錄成功後,服務器纔會給客戶端一個供後續訪問的憑證cookie,而實際情況是,即使登錄失敗,服務器也會給客戶端一個cookie,只是這個cookie是沒有權限的,無法繼續訪問後續只有登錄了才能訪問的界面。說白了,這個cookie對我們沒用。發現這個問題的過程是:最初我用程序獲得cookie,並帶着這個cookie訪問後續網頁,發現無權限。後來,我先在谷歌瀏覽器上登錄教務網,然後在登錄成功的頁面上點擊審查元素,找到左上角的Network並點擊,按F5刷新,重新載入頁面。點擊需要的元素,此處我需要index.jsp這個頁面。如下圖:
右邊Headers下Request Headers中的Cookie字段的JSESSIONID正是我們所需要的。我將這個值直接傳入程序,發現成功獲取到課程表信息的HTML文件,也就是說,我用程序獲得的cookie只是一個無權限的cookie,只要我獲得一個有權限的cookie就可以成功解決問題。
所以,當利用Httpclient登錄獲得cookie繼續訪問但網頁仍提示無權限請登錄時,應該首先考慮是否獲得了一個有權限的cookie?
之後,我再次從瀏覽器上登錄,這才發現了問題所在:原來學校教務網在登錄界面(http://***.***.***.***/service/login.jsp)上提交表單數據後會先將數據交給servlet處理,再轉發到一個新頁面。而我正是忽略了這個界面,如下圖:
困擾了這麼久,原來是urlLogin寫錯了,我錯誤地寫成了登錄界面的URL(http://***.***.***.***/service/login.jsp),正確的應該是http://***.***.***.***/servlet/UserLoginSQLAction,因爲我需要將表單數據提交給它。這一點覺得學校教務網在設計上有點奇怪,調試的過程中我發現,即使登錄失敗,返回的HttpStatusCode仍是200,最開始也誤導我,讓我一直以爲我登錄成功了,根本沒把問題歸結在我其實就沒登錄成功上。
在步驟3中,由於既有網絡操作,又有數據庫操作,所以需要將這些耗時的操作放在子線程中,而安卓系統不允許子線程更新UI線程,所以,像彈出Toast這樣的提示框需要用到Handler(網上一搜一大堆教程)。我的做法是:將步驟1登錄的過程放到一個線程當中,將獲取課程表信息並存放“數據”到數據庫的過程放到一個線程當中,利用join()方法控制線程執行的順序,在成功執行完後,向Handler發消息,讓Handler配合主線程更新UI,彈出Toast提示框。相關代碼如下:
定義必要的全局變量:
private static HttpClient httpclient = new DefaultHttpClient();// 創建一個客戶端實體
public static UserLoginInfo userLoginInfo = new UserLoginInfo();// 創建一個用戶實體
private static String urlLogin = "http://***.***.***.***/servlet/UserLoginSQLAction";// 登錄URL
private static String urlCourse = "http://***.***.***.***/student/course/MyCourseThisTerm.jsp";// 課程信息URL
private String CourseHTML = null;// 保存獲得的課程表網頁HTML文件的String類型
private static List<Cookie> cookies; // 保存登陸成功後的cookie
private static ArrayList<CourseInfo> allCourse;// 所有課程
登錄子線程:
class PostThread extends Thread {
String Tuser_id;
String Tpassword;
public PostThread(String user_id, String password) {
this.Tuser_id = user_id;
this.Tpassword = password;
}
public void run() {
HttpPost httpPost = new HttpPost(urlLogin);// Post方法
HttpResponse httpResponseForPost = null;// Post方法的響應信息
List<NameValuePair> params = new ArrayList<NameValuePair>();
userLoginInfo.setUser_id(Tuser_id);// 學號
userLoginInfo.setPassword(Tpassword);// 密碼
userLoginInfo.setUser_style("modern");// 瀏覽器上的界面風格,可要可不要
userLoginInfo.setUser_type("student");// 用戶類型
/* 利用Post方法登錄,目的是爲了獲取能繼續訪問網頁的“通行證”cookie(登陸成功後的cookie) */
/* Post方法參數設置 */
params.add(new BasicNameValuePair("Browser", ""));
params.add(new BasicNameValuePair("btn1", ""));
params.add(new BasicNameValuePair("OperatingSystem", ""));
params.add(new BasicNameValuePair("password", userLoginInfo
.getPassword()));
params.add(new BasicNameValuePair("url", "../usersys/index.jsp"));
params.add(new BasicNameValuePair("user_id", userLoginInfo
.getUser_id()));
params.add(new BasicNameValuePair("user_style", userLoginInfo
.getUser_style()));// 瀏覽器上的界面風格,可要可不要
params.add(new BasicNameValuePair("user_type", userLoginInfo
.getUser_type()));
/*
* 設置必要的request header:Host頭域指定請求資源的Intenet主機和端口號,
* 必須表示請求url的原始服務器或網關的位置。 HTTP/1.1請求必須包含主機頭域,否則系統會以400狀態碼返回。
*/
httpPost.setHeader("Host", "***.***.***.***");
/*
* 設置必要的request header:Referer 頭域允許客戶端指定請求uri的源資源地址,
* 這可以允許服務器生成回退鏈表,可用來登陸、優化cache等。 他也允許廢除的或錯誤的連接由於維護的目的被 追蹤。
* 如果請求的uri沒有自己的uri地址,Referer不能被髮送。 如果指定的是部分uri地址,則此地址應該是一個相對地址。
* 參考自http://blog.csdn.net/rainysia/article/details/8131174
*/
httpPost.setHeader("Referer",
"http://***.***.***.***/service/login.jsp?user_type=student");
try {
httpPost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
httpResponseForPost = httpclient.execute(httpPost);
if (httpResponseForPost.getStatusLine().getStatusCode() == 200) {
cookies = ((AbstractHttpClient) httpclient)
.getCookieStore().getCookies();
// 利用SharedPreferences存儲用戶登錄信息
SharedPreferences userSharedPreferences = getSharedPreferences(
"user", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = userSharedPreferences
.edit();
editor.putString("user_id", userLoginInfo.getUser_id());
editor.commit();
/*Message msg = new Message();
msg.what = 1;
handler.sendMessage(msg);*/
} else {
/*Message msg = new Message();
msg.what = 2;
handler.sendMessage(msg);*/
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
}
}
}
獲取課程表信息子線程:
class getThread extends Thread {
String GTuser_id;
public getThread(String user_id) {
this.GTuser_id = user_id;
}
public void run() {
HttpGet httpGet = new HttpGet(urlCourse);// Get方法
HttpResponse httpResponseForGet = null;// Get方法的響應信息
/* 利用Get方法,帶上成功登錄獲得的cookie,繼續訪問課程表頁面,並將頁面HTML格式全部放在String中 */
httpGet.setHeader("Cookie",
"JSESSIONID=" + cookies.get(0).getValue() + "; user_id="
+ userLoginInfo.getUser_id() + "; user_type="
+ userLoginInfo.getUser_type() + "; user_style="
+ userLoginInfo.getUser_style() + ";");
try {
httpResponseForGet = httpclient.execute(httpGet);
if (httpResponseForGet.getStatusLine().getStatusCode() == 200) {
StringBuffer stringBuffer = new StringBuffer();
HttpEntity entity = httpResponseForGet.getEntity();
InputStream inputStream = null;
inputStream = entity.getContent();
BufferedReader br = null;
br = new BufferedReader(new InputStreamReader(inputStream,
"UTF-8"));
String data = "";
while ((data = br.readLine()) != null) {
stringBuffer.append(data);
}
CourseHTML = stringBuffer.toString();
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// httpGet.releaseConnection();
}
// 利用Jsoup解析獲得的HTML文件,可以忽略,因具體實現而異
int flag = 1, idIndex = 4, nameIndex = 1, teacherIndex = 4, infoIndex = 1;
if ("".equals(CourseHTML) && null == CourseHTML) {
System.out.println("HTML null");
}
Document document = Jsoup.parse(CourseHTML);
Elements courseId = document.select("td[width=8%]");
Elements courseName = document.select("td[width=17%]");
Elements courseTeacher = document.select("td[width=6%]");
Elements courseInfo = document.select("td[width=20%]");
Elements userName = document.select("td[height=32]");
userLoginInfo.setUser_name(userName.text());
ArrayList<String> id = getElement(courseId.text());
ArrayList<String> name = getElement(courseName.text());
ArrayList<String> teacher = getElement(courseTeacher.text());
ArrayList<String> info = getElement(courseInfo.text());
int idSize = 0,nameSize = 0,teacherSize = 0,infoSize = 0;
idSize = id.size();
nameSize = name.size();
teacherSize = teacher.size();
infoSize = info.size();
while (flag != 0) {
CourseInfo course = new CourseInfo();
course.setCourseId(id.get(idIndex).toString());
course.setCourseName(name.get(nameIndex).toString());
course.setCourseTeacher(teacher.get(teacherIndex).toString());
course.setCourseInfo(info.get(infoIndex).toString()
+ info.get(infoIndex + 1).toString());
allCourse.add(course);
idIndex += 4;
nameIndex += 1;
teacherIndex += 3;
infoIndex += 2;
if (idIndex > idSize || nameIndex > nameSize || teacherIndex > teacherSize
|| infoIndex > infoSize) {
flag = 0;
}
}
for (int i = 0; i < allCourse.size(); i++) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(allCourse.get(i).getCourseTeacher() + "\n")
.append(allCourse.get(i).getCourseInfo());
courseDataBase.insertCourse(course_id, GTuser_id, allCourse
.get(i).getCourseName(), stringBuffer.toString());
Intent intent = new Intent();
intent.setAction("action.refreshFriend");
sendBroadcast(intent);
showCountId();
}
/*Message msg = new Message();
msg.what = 3;
handler.sendMessage(msg);*/
}
}
貼的代碼中包含的很多自定義的方法沒有列出具體代碼。