Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android 項目實戰——超級課程表課表一鍵提取功能

android 項目實戰——超級課程表課表一鍵提取功能

編輯:關於Android編程

 

如果你是在校大學生,或許你用多了各種課程表,比如課程格子,超級課程表。它們都有一個共同點就是可以一鍵導入教務處的課程。那麼一直都是用戶的我們,沒有考慮過它是如何實現的。那麼現在就來模仿一款”超級課程表“。

PS:由於超級課程表是商用軟件,原本提取了一些圖片,但是為了避免涉及侵權問題,所有圖片均已使用一張綠色圓圈代替,背景圖片也以顏色代替,缺乏美觀,如果你覺得太丑,可以自己尋找圖片代替。

那麼說了這麼久,先來看看這款高仿的軟件長什麼樣子。本文的代碼做過精簡,所以界面可能有出入。

\

好了,界面太丑,不忍直視,先暫時忽略,本文的重點不是UI,而是如何提取課程。

先做下准備工作。

  1. HttpWatch抓包分析工具。此工具的使用後文介紹

  2. Litepal數據持久化orm,郭大神的大作,挺好用的orm,用法詳見郭霖博客。

  3. Async-android-http 數據異步請求框架,這裡主要用到這個框架的異步請求以及session保持的功能,或許大多數人沒有使用過這個框架的會話保持功能,反正個人覺得就是一神器,操作十分簡單,就1句話,不然用HttpClient可能就沒那麼簡單了,要自己寫好多內容。具體用法參見github

  4. Jsoup網頁內容解析框架,可支持jquery選擇器。可以支持從本地加載html,遠程加載html,支持數據抽取,數據修改等功能,如果能靈活運用這個框架,那麼你想抓取什麼東西都不在話下。


    既然要導入課程表,那麼一定要登錄教務處,結論是需要教務處的賬號密碼,這個好辦,每個學生都有賬號密碼。那麼怎麼登錄呢,這個當然不是我們人工登錄了,只要提供賬號密碼,由程序來幫我們完成登錄過程以及課程的提取過程。如果登錄?首先打開教務處登錄界面,打開HttpWatch進行跟蹤。輸入賬號,密碼,驗證碼(驗證碼視具體學校不同,有些學校不含驗證碼,有些學校含驗證碼,驗證碼的處理後文進行說明),輸入完成後點擊登錄,再點擊查看課程的菜單,之後停止HttpWatch錄制,把文件保存一下進行分析。打開保存後的文件,查看登錄時提交的參數及一些信息,記錄下來,同時記錄查看課程頁提交的參數及信息。

    \

     

    先看登錄頁面提交的參數,參數均是POST提交,這可以通過HttpWatch看到提交方式

    __VIEWSTATE:有這個值頁面生成的,這裡我直接使用這個固定值而不去抓取,這個值是.net根據表單參數自動生成的。理論上同一個頁面是不會變動的。

    Button1:傳空值即可

    hidPdrs:傳空值即可

    lbLanguage:傳空值即可

    RadioButtonList1:圖上是亂碼,通過查看網頁源代碼可知該值是學生,因為我們是以學生的角色登錄的

    TextBox2:這個值是密碼,傳密碼即可

    txtSecrect:這個值是驗證碼,傳對應的驗證碼即可

    txtUserName:這個值是學號,傳學號即可

    你以為只要提交這些參數就好了嗎,那麼你就錯了,我們還有設置請求頭信息,如下圖\

     

    我們不必設置所有請求頭信息,只需要設置Host,Referer,User-Agent(可不設)。

     

    請求頭設置完畢了,那麼來說一個重大的問題,就是驗證碼的問題,這裡有三種方式供選擇。

    1. 在登錄之前抓取驗證碼,顯示出來,供用戶輸入。

    2. 使用正方的bug1,為什麼是bug1呢,因為後面一種方法利用了bug2,bug1,bug2不一定所有學校適用,正方的默認登錄頁面是default2.aspx,如果這個頁面有驗證碼,你可以試試default1.aspx-default6.aspx六個頁面,運氣好的話可能會有不需要驗證碼的頁面。這時候你使用該頁面進行登錄即可(提交參數會不同,具體自己抓包分析)

    3. 使用正方的bug2,不得不說這個bug2,大概是某個程序猿在某男某月某日無意間留下的把,那麼怎麼使用這個bug呢,很簡單,登錄的時候直接傳驗證碼為空值或者空字符串過去就好了,有人說,你他媽逗我,這都行,恩,真的行。為什麼行呢,原因可能是正方後台程序沒有判斷傳過來的值是不是空。我們模擬登錄的時候並沒有去請求驗證碼的頁面,所有不會產生驗證碼(此時為空字符串或者空值)和cookie,當我們提交空驗證碼時,後台接收到的值就是空字符串,兩個空字符串做比較當然相等了,以上只是猜測,畢竟正方是.net的,.net的處理機制本人不是很清楚。

       

      說了這麼多理論知識,來點實際的把,先完成登錄界面的代碼

       

      
      
          
          
          
      
          
      
          
      
              
      
              

       

      很簡單,就是賬號,密碼,以及驗證碼,這裡驗證碼被我隱藏了,因為我使用了bug2,不需要請求驗證碼,對應的界面隱藏掉,但是如果你把他顯示出來,獲取驗證碼讓用戶輸入也是可以的。

      在登錄之前先初始化一下cookie,這一步必須在請求之前設置。


      /**
      	 * 初始化Cookie
      	 */
      	private void initCookie(Context context) {
      		//必須在請求前初始化
      		cookie = new PersistentCookieStore(context);
      		HttpUtil.getClient().setCookieStore(cookie);
      	}

      那麼HttpUtil又是什麼呢,很簡單,就是一個請求用的工具類

       

      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();
       
                  }
              });
          }

      LinkUtil裡面是一些常量

       

      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);
      			}
      	}
      }
      

      背景圖使用的是shape,這裡貼出其中一個,其余的就只是顏色不同

       

       

      
      
          
          
          
       
      

      至此,超級課程表的一鍵提取課表功能就完成了。顯示最終效果見下方

       

       

      \

      整個過程可簡單概括為抓包分析,數據提取,數據顯示,其中關鍵的一步就是數據的提取。這個過程中有個注意點就是抓課程數據的時候header請求頭信息裡的referer信息請務必設置為登錄成功後的網址,即http://***.***.***.***/xs_main.aspx?xh=XH,否則抓數據的時候頁面會被循環重定向,將抓不到數據,程序也會報異常。

       

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved