編輯:關於Android編程
如果你是在校大學生,或許你用多了各種課程表,比如課程格子,超級課程表。它們都有一個共同點就是可以一鍵導入教務處的課程。那麼一直都是用戶的我們,沒有考慮過它是如何實現的。那麼現在就來模仿一款”超級課程表“。
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的處理機制本人不是很清楚。
說了這麼多理論知識,來點實際的把,先完成登錄界面的代碼
很簡單,就是賬號,密碼,以及驗證碼,這裡驗證碼被我隱藏了,因為我使用了bug2,不需要請求驗證碼,對應的界面隱藏掉,但是如果你把他顯示出來,獲取驗證碼讓用戶輸入也是可以的。
在登錄之前先初始化一下cookie,這一步必須在請求之前設置。
/**
* 初始化Cookie
*/
private void initCookie(Context context) {
//必須在請求前初始化
cookie = new PersistentCookieStore(context);
HttpUtil.getClient().setCookieStore(cookie);
}
package 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 = http://***.***.***.***/;
// 驗證碼地址
public static final String URL_CODE = http://***.***.***.***/CheckCode.aspx;
// 登陸地址
public static final String URL_LOGIN = http://***.***.***.***/default2.aspx;
// 登錄成功的首頁
public static String URL_MAIN = http://***.***.***.***/xs_main.aspx?xh=XH;
// 請求地址
public static String URL_QUERY = http://***.***.***.***/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();
}
});
}
}
地址信息被我處理掉了,替換成對應的地址即可,都是幾個簡單的函數,其中最後一個函數做了一個封裝,代碼自己讀吧,這裡就不講了。。。。。
現在查看登錄的代碼。
/**
* 登錄
*/
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();
}
});
}
通過抓取關鍵字,判斷是否登錄成功,登錄成功則解析菜單,對應的邏輯被我封裝在service層裡了
package 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 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 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;
}
}
判斷是否登錄成功的判斷依據是看頁面上是否有某某同學,歡迎你,這段信息在id為xhxm的span裡,成功後解析菜單,因為不一定只是抓課表,也可能抓成績,各種抓,所以這裡把鏈接都記錄下來,對應頁面的源代碼我會和代碼一同上傳。
如果你要使用驗證碼,則獲取驗證碼即可,對應代碼如下,就是獲得驗證碼後顯示在界面上
/**
* 獲得驗證碼
*/
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();
}
});
}
package 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=教學日歷查詢;
}
接下來是文章的重點,即如何解析課表。
課表是在一張table裡的,提取table裡的內容進行解析,解析方法不止一種,我在解析過程中也嘗試了多種方法,直接看代碼把
/**
* 根據網頁返回結果解析課程並保存
*
* @param content
* @return
*/
public String parseCourse(String content) {
StringBuilder result = new StringBuilder();
Document doc = Jsoup.parse(content);
Elements semesters = doc.select(option[selected=selected]);
String[] years=semesters.get(0).text().split(-);
int startYear=Integer.parseInt(years[0]);
int endYear=Integer.parseInt(years[1]);
int semester=Integer.parseInt(semesters.get(1).text());
Elements elements = doc.select(table#Table1);
Element element = elements.get(0).child(0);
//移除一些無用數據
element.child(0).remove();
element.child(0).remove();
element.child(0).child(0).remove();
element.child(4).child(0).remove();
element.child(8).child(0).remove();
int rowNum = element.childNodeSize();
int[][] map = new int[11][7];
for (int i = 0; i < rowNum - 1; i++) {
Element row = element.child(i);
int columnNum = row.childNodeSize() - 2;
for (int j = 1; j < columnNum; j++) {
Element column = row.child(j);
int week = fillMap(column, map, i);
//填充map,獲取周幾,第幾節至第幾節
//作用:彌補不能獲取這些數據的格式
if (column.hasAttr(rowspan)) {
try {
System.out.println(周+ week+ 第+ (i + 1)+ 節-第+ (i + Integer.parseInt(column.attr(rowspan))) + 節);
splitCourse(column.html(), startYear,endYear,semester,week, i + 1,i + Integer.parseInt(column.attr(rowspan)));
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
return result.toString();
}
/**
* 根據傳進來的課程格式轉換為對應的實體類並保存
* @param sub
* @param startYear
* @param endYear
* @param semester
* @param week
* @param startSection
* @param endSection
* @return
*/
private Course storeCourseByResult(String sub,int startYear,int endYear,int semester, int week,
int startSection, int endSection) {
// 周二第1,2節{第4-16周} 二,1,2,4,16,null
// {第2-10周|3節/周} null,null,null,2,10,3節/周
// 周二第1,2節{第4-16周|雙周} 二,1,2,4,16,雙周
// 周二第1節{第4-16周} 二,1,null,4,16,null
// 周二第1節{第4-16周|雙周} 二,1,null,4,16,雙周
// str格式如上,這裡只是簡單考慮每個課都只有兩節課,實際上有三節和四節,模式就要改動,其他匹配模式請自行修改
String reg = 周?(.)?第?(\d{1,2})?,?(\d{1,2})?節?\{第(\d{1,2})-(\d{1,2})周\|?((.*周))?\};
String splitPattern =
;
String[] temp = sub.split(splitPattern);
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(temp[1]);
matcher.matches();
Course course = new Course();
//課程開始學年
course.setStartYear(startYear);
//課程結束學年
course.setEndYear(endYear);
//課程學期
course.setSemester(semester);
//課程名
course.setCourseName(temp[0]);
//課程時間,冗余字段
course.setCourseTime(temp[1]);
//教師
course.setTeacher(temp[2]);
try {
// 數組可能越界,即沒有教室
course.setClasssroom(temp[3]);
} catch (ArrayIndexOutOfBoundsException e) {
course.setClasssroom(無教室);
}
//周幾,可能為空,此時使用傳進來的值
if (null != matcher.group(1)){
course.setDayOfWeek(getDayOfWeek(matcher.group(1)));
}else{
course.setDayOfWeek(getDayOfWeek(week+));
}
//課程開始節數,可能為空,此時使用傳進來的值
if (null != matcher.group(2)){
course.setStartSection(Integer.parseInt(matcher.group(2)));
}else{
course.setStartSection(startSection);
}
//課程結束時的節數,可能為空,此時使用傳進來的值
if (null != matcher.group(3)){
course.setEndSection(Integer.parseInt(matcher.group(3)));
}else{
course.setEndSection(endSection);
}
//起始周
course.setStartWeek(Integer.parseInt(matcher.group(4)));
//結束周
course.setEndWeek(Integer.parseInt(matcher.group(5)));
//單雙周
String t = matcher.group(6);
setEveryWeekByChinese(t, course);
save(course);
return course;
}
/**
* 提取課程格式,可能包含多節課
* @param str
* @param startYear
* @param endYear
* @param semester
* @param week
* @param startSection
* @param endSection
* @return
*/
private int splitCourse(String str, int startYear,int endYear,int semester,int week, int startSection,
int endSection) {
String pattern =
;
String[] split = str.split(pattern);
if (split.length > 1) {// 如果大於一節課
for (int i = 0; i < split.length; i++) {
if (!(split[i].startsWith(
) && split[i].endsWith(
))) {
storeCourseByResult(split[i], startYear,endYear,semester,week, startSection,
endSection);// 保存單節課
} else {
//
文化地理(網絡課程)
周日第10節{第17-17周}
李宏偉
// 以上格式的特殊處理,此種格式在沒有教師的情況下產生,即教室留空後
依舊存在
int brLength =
.length();
String substring = split[i].substring(brLength,
split[i].length() - brLength);
storeCourseByResult(substring, startYear,endYear,semester,week, startSection,
endSection);// 保存單節課
}
}
return split.length;
} else {
storeCourseByResult(str, startYear,endYear,semester,week, startSection, endSection);// 保存
return 1;
}
}
/**
* 填充map,獲取周幾,第幾節課至第幾節課
* @param childColumn
* @param map
* @param i
* @return 周幾
*/
public static int fillMap(Element childColumn, int map[][], int i) {
//這個函數的作用自行領悟,總之就是返回周幾,也是無意中發現的,於是就這樣獲取了,作用是雙重保障,因為有些課事無法根據正則匹配出周幾第幾節到第幾節
boolean hasAttr = childColumn.hasAttr(rowspan);
int week = 0;
if (hasAttr) {
for (int t = 0; t < map[0].length; t++) {
if (map[i][t] == 0) {
int r = Integer.parseInt(childColumn.attr(rowspan));
for (int l = 0; l < r; l++) {
map[i + l][t] = 1;
}
week = t + 1;
break;
}
}
} else {
if (childColumn.childNodes().size() > 1) {
childColumn.attr(rowspan, 1);
}
for (int t = 0; t < map[0].length; t++) {
if (map[i][t] == 0) {
map[i][t] = 1;
week = t + 1;
break;
}
}
}
return week;
}
/**
* 設置單雙周
* @param week
* @param course
*/
public void setEveryWeekByChinese(String week, Course course) {
// 1代表單周,2代表雙周
if (week != null) {
if (week.equals(單周))
course.setEveryWeek(1);
else if (week.equals(雙周))
course.setEveryWeek(2);
}
// 默認值為0,代表每周
}
/**根據中文數字一,二,三,四,五,六,日,轉換為對應的阿拉伯數字
* @param day
* @return int
*/
public int getDayOfWeek(String day) {
if (day.equals(一))
return 1;
else if (day.equals(二))
return 2;
else if (day.equals(三))
return 3;
else if (day.equals(四))
return 4;
else if (day.equals(五))
return 5;
else if (day.equals(六))
return 6;
else if (day.equals(日))
return 7;
else
return 0;
}
課程的實體類
package cn.lizhangqu.kb.model;
import org.litepal.crud.DataSupport;
/**
* 課程實體類
* @author lizhangqu
* @date 2015-2-1
*/
public class Course extends DataSupport{
private int id;//主鍵,自增
private int startYear;//學年開始年
private int endYear;//學年結束年
private int semester;//學期
private String courseName;//課程名
private String courseTime;//課程時間,冗余字段
private String classsroom;//教室
private String teacher;//老師
private int dayOfWeek;//星期幾
private int startSection;//第幾節課開始
private int endSection;//第幾節課結束
private int startWeek;//開始周
private int endWeek;//結束周
private int everyWeek;//標記是否是單雙周,0為每周,1單周,2雙周
public int getStartYear() {
return startYear;
}
public void setStartYear(int startYear) {
this.startYear = startYear;
}
public int getEndYear() {
return endYear;
}
public void setEndYear(int endYear) {
this.endYear = endYear;
}
public int getSemester() {
return semester;
}
public void setSemester(int semester) {
this.semester = semester;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getEveryWeek() {
return everyWeek;
}
public void setEveryWeek(int everyWeek) {
this.everyWeek = everyWeek;
}
public int getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(int dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public int getStartSection() {
return startSection;
}
public void setStartSection(int startSection) {
this.startSection = startSection;
}
public int getEndSection() {
return endSection;
}
public void setEndSection(int endSection) {
this.endSection = endSection;
}
public int getStartWeek() {
return startWeek;
}
public void setStartWeek(int startWeek) {
this.startWeek = startWeek;
}
public int getEndWeek() {
return endWeek;
}
public void setEndWeek(int endWeek) {
this.endWeek = endWeek;
}
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public String getCourseTime() {
return courseTime;
}
public void setCourseTime(String courseTime) {
this.courseTime = courseTime;
}
public String getClasssroom() {
return classsroom;
}
public void setClasssroom(String classsroom) {
this.classsroom = classsroom;
}
public String getTeacher() {
return teacher;
}
public void setTeacher(String teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return Course [id= + id + , startYear= + startYear + , endYear=
+ endYear + , semester= + semester + , courseName=
+ courseName + , courseTime= + courseTime + , classsroom=
+ classsroom + , teacher= + teacher + , dayOfWeek=
+ dayOfWeek + , startSection= + startSection
+ , endSection= + endSection + , startWeek= + startWeek
+ , endWeek= + endWeek + , everyWeek= + everyWeek + ];
}
}
以上代碼是提取課程的關鍵代碼,課程的格式是在一個table裡,tr裡有很多td,td裡就是課程,一個td裡可能不止一節課

td有rowspan屬性,代表占了幾行,2代表占了兩行,也就是兩節課,有些課不是兩節課的,rowspan的值也就對應改變,課程的節數有1,2,3,4節都有。我們可以根據課程的時間提取該課程是周幾上課,第幾節課到第幾節課,有了這些信息就可以在界面上顯示出來了,但是,有些格式,如第2-10周|3節/周是沒辦法提取時間的,這時候就用一定的技巧提取它,這裡使用了fillmap函數對一個7*12的數組進行填充,原理是掃描一行,如果具有rowspan值,則填充該行該列為1,如果rowspan大於等於2,則該列下面幾行對應的列也填充為1,等到掃描下一行的時候,該位置不會有課程,且不會有td,則如果是一個空td,則填充該行第一個為0的位置。技巧有點難以理解,具體細節稍微自己琢磨領悟下,這樣課程的信息就都提取出來了,當然提取方式不止一種。這種方法也不一定能提取所有格式的課程。
提取完畢後進行顯示,本來呢是使用LinearLayout簡單達到超級課程表的效果的,後來稍微暴力的使用了下自定義ViewGroup,注意了,這個自定義ViewGroup不具有現實使用意義,只是為了展示效果,裡面的代碼都太暴力了。。。所以看過一遍就無視吧,簡直不忍直視
首先是自定義屬性
然後是課程自定義View,繼承Button,增加一些課程信息而已。
package cn.lizhangqu.kb.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.widget.Button;
import cn.lizhangqu.kb.R;
public class CourseView extends Button {
private int courseId;
private int startSection;
private int endSection;
private int weekDay;
public CourseView(Context context) {
this(context,null);
}
public CourseView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CourseView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.CourseView);
courseId = array.getInt(R.styleable.CourseView_courseId, 0);
startSection=array.getInt(R.styleable.CourseView_startSection, 0);
endSection=array.getInt(R.styleable.CourseView_endSection, 0);
weekDay=array.getInt(R.styleable.CourseView_weekDay, 0);
array.recycle();
}
public int getCourseId() {
return courseId;
}
public void setCourseId(int courseId) {
this.courseId = courseId;
}
public int getStartSection() {
return startSection;
}
public void setStartSection(int startSection) {
this.startSection = startSection;
}
public int getEndSection() {
return endSection;
}
public void setEndSection(int endSection) {
this.endSection = endSection;
}
public int getWeek() {
return weekDay;
}
public void setWeek(int week) {
this.weekDay = week;
}
}
最後是自定義布局,簡單暴力,注意了,這個布局沒有處理重復時間的課程,也就是說沒有處理單雙周的情況,只是用來簡單顯示
package cn.lizhangqu.kb.view;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
public class CourseLayout extends ViewGroup {
private List courses = new ArrayList();
private int width;//布局寬度
private int height;//布局高度
private int sectionHeight;//每節課高度
private int sectionWidth;//每節課寬度
private int sectionNumber = 12;//一天的節數
private int dayNumber = 7;//一周的天數
private int divideWidth = 2;//分隔線寬度,dp
private int divideHeight = 2;//分隔線高度,dp
public CourseLayout(Context context) {
this(context, null);
}
public CourseLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CourseLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
width = getScreenWidth();//默認寬度全屏
height = dip2px(600);//默認高度600dp
divideWidth = dip2px(2);//默認分隔線寬度2dp
divideHeight = dip2px(2);//默認分隔線高度2dp
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
courses.clear();//清除
sectionHeight = (getMeasuredHeight() - divideWidth * sectionNumber)/ sectionNumber;//計算每節課高度
sectionWidth = (getMeasuredWidth() - divideWidth * dayNumber)/ dayNumber;//計算每節課寬度
int count = getChildCount();//獲得子控件個數
for (int i = 0; i < count; i++) {
CourseView child = (CourseView) getChildAt(i);
courses.add(child);//增加到list中
int week = child.getWeek();//獲得周幾
int startSection = child.getStartSection();//開始節數
int endSection = child.getEndSection();//結束節數
int left = sectionWidth * (week - 1) + (week) * divideWidth;//計算左邊的坐標
int right = left + sectionWidth;//計算右邊坐標
int top = sectionHeight * (startSection - 1) + (startSection) * divideHeight;//計算頂部坐標
int bottom = top + (endSection - startSection + 1) * sectionHeight+ (endSection - startSection) * divideHeight;//計算底部坐標
child.layout(left, top, right, bottom);
}
}
public int dip2px(float dip) {
float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dip * scale + 0.5f);
}
public int getScreenWidth() {
WindowManager manager = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(displayMetrics);
return displayMetrics.widthPixels;
}
}
package cn.lizhangqu.kb.activity;
import java.util.List;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
import cn.lizhangqu.kb.R;
import cn.lizhangqu.kb.model.Course;
import cn.lizhangqu.kb.service.CourseService;
import cn.lizhangqu.kb.util.CommonUtil;
import cn.lizhangqu.kb.view.CourseLayout;
import cn.lizhangqu.kb.view.CourseView;
/**
* @author lizhangqu
* @date 2015-2-1
*/
public class CourseActivity extends Activity {
//某節課的背景圖,用於隨機獲取
private int[] bg={R.drawable.kb1,R.drawable.kb2,R.drawable.kb3,R.drawable.kb4,R.drawable.kb5,R.drawable.kb6,R.drawable.kb7};
private CourseService courseService;
private CourseLayout layout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_course);
initValue();
initView();
}
/**
* 初始化變量
*/
private void initValue() {
courseService=CourseService.getCourseService();
}
/**
* 初始化視圖
*/
private void initView() {
//這裡有邏輯問題,只是簡單的顯示了下數據,數據並不一定是顯示在正確位置
//課程可能有重疊
//課程可能有1節課的,2節課的,3節課的,因此這裡應該改成在自定義View上顯示更合理
List courses=courseService.findAll();//獲得數據庫中的課程
layout=(CourseLayout) findViewById(R.id.courses);
Course course=null;
//循環遍歷
for (int i = 0; i < courses.size(); i++) {
course=courses.get(i);
CourseView view=new CourseView(getApplicationContext());
view.setCourseId(course.getId());
view.setStartSection(course.getStartSection());
view.setEndSection(course.getEndSection());
view.setWeek(course.getDayOfWeek());
int bgRes=bg[CommonUtil.getRandom(bg.length-1)];//隨機獲取背景色
view.setBackgroundResource(bgRes);
view.setText(course.getCourseName()+@+course.getClasssroom());
view.setTextColor(Color.WHITE);
view.setTextSize(12);
view.setGravity(Gravity.CENTER);
layout.addView(view);
}
}
}

整個過程可簡單概括為抓包分析,數據提取,數據顯示,其中關鍵的一步就是數據的提取。這個過程中有個注意點就是抓課程數據的時候header請求頭信息裡的referer信息請務必設置為登錄成功後的網址,即http://***.***.***.***/xs_main.aspx?xh=XH,否則抓數據的時候頁面會被循環重定向,將抓不到數據,程序也會報異常。
Android列表對話框用法實例分析
本文實例講述了Android列表對話框用法。分享給大家供大家參考。具體如下:main.xml布局文件:<?xml version=1.0 encoding=
【Android基礎知識】Drawable Animation和View Animation
Android中的動畫主要分為三類1.Drawable Animation2.View Animation3.Property Animation這裡介紹其中的兩類,Dr
Android實現桌面懸浮窗、蒙板效果實例代碼
現在很多安全類的軟件,比如360手機助手,百度手機助手等等,都有一個懸浮窗,可以飄浮在桌面上,方便用戶使用一些常用的操作。今天這篇文章,就是介紹如何實現桌面懸浮窗效果的。
詳解Android Scroller與computeScroll的調用機制關系
Android ViewGroup中的Scroller與computeScroll的有什麼關系?答:沒有直接的關系知道了答案,是不是意味著下文就沒必要看了,如果說對Vie