iOS模擬登錄方正教務系統(tǒng)獲取課表

最近做學校教務系統(tǒng)爬蟲甜橱,這里寫一下我遇到的一些問題和心得逊笆。

1.用到的工具

Chrome的開發(fā)者工具:分析網(wǎng)頁行為,查看每次HTTP請求命令與參數(shù)等岂傲。
TFhepple: HTML解析庫难裆。
demo中關于網(wǎng)絡請求部分是直接使用原生NSURLSession來完成的。

2.分析網(wǎng)頁行為

2.1打開教務系統(tǒng)網(wǎng)頁

  • 當我輸入教務系統(tǒng)網(wǎng)址镊掖,可以看到網(wǎng)頁行為如圖所示:


    一個個點開來看:這里訪問頁面全部都是通過GET方式乃戈。(其中那個blank的404暫時不知道有什么用,而且也不影響亩进,就忽略它吧)
    1.第一個200:沒有什么特別重要的信息症虑,忽略。

    2.接下來是連續(xù)三個重定向(response header里面的Location就是重定向的網(wǎng)址):




    這里我們可以看到归薛,在訪問http://jw2005.scuteo.com/ 時得到了一個cookie(這個cookie只有在第一次訪問時才會產(chǎn)生)谍憔。
    在重定向的最后,我們可以看到Request URL中附加了一個字段主籍,這個字段是隨機產(chǎn)生的习贫,而且后續(xù)的網(wǎng)頁訪問中這個隨機字段也會出現(xiàn)在url中,因此要把這個隨機字段保存起來(在第二張圖的Request URL中也有另一個隨機字段千元,但此時重定向并沒有完成苫昌,我們要保存的是最后的那個隨機字段)。

    另外還有一點幸海,在實際測試中發(fā)現(xiàn)祟身,我們學校的教務系統(tǒng)屋厘,上面重定向最后的Request URL中的host地址是會變化的,可能這次訪問的host地址是110.65.10.191下次訪問得到的host地址就是110.65.10.204了月而。所以在這里我們也要把host地址保存下來汗洒。

  • 關于驗證碼
    在網(wǎng)上看到很多文章都說可以繞過驗證碼,但現(xiàn)在方正教務系統(tǒng)好像已經(jīng)修復這個bug了父款。驗證碼識別有很多種方法溢谤,在項目中我選擇把驗證碼圖片獲取下來,然后讓用戶手動輸入憨攒。
    這里和驗證碼有關的是CheckCode.aspx(看到了吧世杀?那個隨機字段又出現(xiàn)了)。如果我們在瀏覽器上直接訪問圖中那個Request URL,的確是可以獲得驗證碼圖片肝集,但實際上它不是我們在教務系統(tǒng)上看到的那張瞻坝。實際上,獲取驗證碼是需要帶上之前獲取的那個cookie的杏瞻,這個cookie保證了我們的驗證碼所刀,是和賬號密碼在同一個網(wǎng)頁上的。


這里總結(jié)一下捞挥,在打開教務系統(tǒng)網(wǎng)頁時我們需要獲取什么:1.cookie浮创、2.重定向最后產(chǎn)生的隨機字段、3.重定向最后的Host地址

2.2登錄


登錄時是POST方式砌函,雖然被重定向斩披,但是這一次提交,完成了數(shù)據(jù)的驗證讹俊,驗證的字段如圖所示垦沉,第一個字段是登陸界面的一個隱藏字段,這個viewstate每次都得在登陸前獲取仍劈,還是通過上面GET請求得到頁面通過HTML分析工具得到對應的viwestate厕倍。txtUserName是用戶名(學號),TextBox2是密碼耳奕,txtSecretCode是驗證碼绑青,RadioButtonList1代表的是學生。


重定向訪問:(遮擋的部分是學號)


3.代碼實現(xiàn)模擬登錄

一些屬性的說明:

@property (nonatomic ,strong)NSURLSession *session;
@property (nonatomic ,strong)NSString *mainUrl;//教務系統(tǒng)網(wǎng)址@"http://jw2005.scuteo.com/"
@property (nonatomic ,copy)NSString *viewState;//viewstate隱藏字段
@property (nonatomic ,copy)NSString *randomStr;//隨機字段
@property (nonatomic ,copy)NSString *httpHost;//host地址
@property (nonatomic ,strong)NSMutableData *httpData;//html數(shù)據(jù)

@property (weak, nonatomic) IBOutlet UIImageView *img;//驗證碼圖片
@property (weak, nonatomic) IBOutlet UITextField *txf;//驗證碼輸入框
  • 獲得view state屋群、隨機字段和host:
 - (IBAction)viewStateAndRandomStrGetting:(id)sender {
    NSURL *url = [NSURL URLWithString:self.mainUrl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"GET";
    self.task = [self.session dataTaskWithRequest:request];
    self.task.taskDescription = @"getViewStateAndRandomStr";
    [self.task resume];
}

在這里重定向是交給NSURLSession代理方法去做的闸婴,每次重定向由completionHandler(request);來實現(xiàn),不需要人工手動重定向芍躏。

//重定向
 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler{
    completionHandler(request);
    NSLog(@"%s,",__func__);
}

重定向結(jié)束邪乍,就可以在響應頭(重定向最后200那一步的響應頭)獲得host和隨機字段(這里的做法不太美觀。。)

//獲取host和隨機串
 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{
    completionHandler(NSURLSessionResponseAllow);
    if ([dataTask.taskDescription isEqualToString:@"getViewStateAndRandomStr"]) {
        NSLog(@"getCookies---response:\n%@",response);        
        self.httpHost = response.URL.host;
        //這里要用正則表達式提取比較好
        self.randomStr = [response.URL.absoluteString substringWithRange:NSMakeRange(21, 26)];
        NSLog(@"%@",self.randomStr);
    }
}

獲取view state要從response Data中獲取庇楞,響應的數(shù)據(jù)不是一次性返回的沒所以要在- URLSession: dataTask: didReceiveData:方法中把數(shù)據(jù)拼接起來榜配。在網(wǎng)絡請求結(jié)束時再提取viewState。然后還有一點吕晌,viewState里面的特殊字符“+”和"="要做編碼處理蛋褥,+替換成%2B,=替換成%3D
然后關于編碼問題:正方教務管理系統(tǒng)IOS客戶端這篇文章里面說到:

正方教務系統(tǒng)用的編碼是GB2312 框架獲取下來的NSString雖然已經(jīng)自動解碼睛驳,但是很不穩(wěn)定烙心,有時候會得到空字符串,但是獲取下來的DATA就沒有這個問題乏沸,所以就要手動解碼將DATA轉(zhuǎn)為NSString淫茵。而且光轉(zhuǎn)碼也不行,在分析HTML的時候因為網(wǎng)頁頭部的編碼信息也有問題蹬跃,所以要做手動修改匙瘪,這樣才能被TFhepple正確解析。

//拼接數(shù)據(jù) 獲取viewState
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
   [data enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
       [self.httpData appendBytes:bytes length:byteRange.length];
   }];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
   if (error) {
       NSLog(@"error:%@",error);
       return;
   }
   if ([task.taskDescription isEqualToString:@"getViewStateAndRandomStr"]) {
       //轉(zhuǎn)碼
       NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding (kCFStringEncodingGB_18030_2000);
       NSString *transtr = [[NSString alloc]initWithData:self.httpData encoding:enc];
       //修改編碼
       NSString *htmlUTF8Str = [transtr stringByReplacingOccurrencesOfString:@"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=gb2312\">" withString:@"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"];
       NSData *htmlDataUTF8 = [htmlUTF8Str dataUsingEncoding:NSUTF8StringEncoding];
       TFHpple *xpathParser = [[TFHpple alloc]initWithHTMLData:htmlDataUTF8];
       NSArray *elements  = [xpathParser searchWithXPathQuery:@"http://input[@name='__VIEWSTATE']"];
       for (int i=0; i<[elements count]; i++) {
           TFHppleElement *element = [elements objectAtIndex:i];
           self.viewState=[element objectForKey:@"value"];
           NSLog(@"提取到得viewstate為%@",self.viewState);
           self.viewState = [self.viewState stringByReplacingOccurrencesOfString:@"+" withString:@"%2B"];
           self.viewState = [self.viewState stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"];
       }
       self.httpData = nil;
   }
}
  • 獲取驗證碼
 -(void)shuaXinYanZhengMa{
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/%@/CheckCode.aspx",self.httpHost,self.randomStr]];
    NSMutableURLRequest *UrlRequest = [NSMutableURLRequest requestWithURL:url];
//    UrlRequest.HTTPShouldHandleCookies = YES;
    NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSHTTPCookie *cookie = [[cookieJar cookiesForURL:[NSURL URLWithString:self.mainUrl]]firstObject];
    [UrlRequest setValue:[NSString stringWithFormat:@"%@=%@", [cookie name], [cookie value]] forHTTPHeaderField:@"Cookie"];
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:UrlRequest];
    task.taskDescription = @"getCheckCode";
    [task resume];
}
 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if ([task.taskDescription isEqualToString:@"getCheckCode"]){
        dispatch_async(dispatch_get_main_queue(), ^{
            self.img.image = [[UIImage alloc]initWithData:self.httpData];
            self.httpData = nil;
        });
    }
}

獲取cookie:

NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    NSHTTPCookie *cookie = [[cookieJar cookiesForURL:[NSURL URLWithString:self.mainUrl]]firstObject];
  • 登錄



    登錄這里按照格式構(gòu)造post參數(shù)即可蝶缀。中文編碼要注意一下丹喻。

 - (IBAction)login:(id)sender {
    NSString *paraStr = [NSString stringWithFormat:@"__VIEWSTATE=%@&txtUserName=%@&TextBox2=%@&txtSecretCode=%@&RadioButtonList1=學生&Button1=&lbLanguage=&hidPdrs=&hidsc=",self.viewState,xuehao,mima,self.txf.text];
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@/%@/default2.aspx",self.httpHost,self.randomStr]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";
    NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding (kCFStringEncodingGB_18030_2000);
    request.HTTPBody = [paraStr dataUsingEncoding:enc];
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
    task.taskDescription = @"login";
    [task resume];
}

登錄成功后,主要這里要獲得一個學生姓名的參數(shù)扼劈,這個姓名的值在后面獲取課表的時候要用到驻啤。在html中像是這樣的:



如果登錄失敗,就提取相應的錯誤信息對用戶進行提示荐吵。

 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    if ([task.taskDescription isEqualToString:@"login"]) {
        NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding (kCFStringEncodingGB_18030_2000);
        NSString *transtr = [[NSString alloc]initWithData:self.httpData encoding:enc];
        NSString *utf8HtmlStr = [transtr stringByReplacingOccurrencesOfString:@"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=gb2312\">" withString:@"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"];
        NSData *htmlDataUTF8 = [utf8HtmlStr dataUsingEncoding:NSUTF8StringEncoding];
        TFHpple *xpathParser = [[TFHpple alloc]initWithHTMLData:htmlDataUTF8];
        NSArray *elements  = [xpathParser searchWithXPathQuery:@"http://span[@id='xhxm']"];
        if (elements.count > 0) {
            for (int i=0; i<[elements count]; i++) {
                TFHppleElement *element = [elements objectAtIndex:i];
                NSString *content = [element text];
                self.name=[content substringToIndex:[content length]-2];
                NSLog(@"姓名為%@",self.name);
            }
        }
        else{
            NSArray *errElement = [xpathParser searchWithXPathQuery:@"http://script[@language='javascript']"];
            TFHppleElement *scriptNode = errElement.lastObject;//驗證碼不正確
            NSString *alertMessage = [[scriptNode.content componentsSeparatedByString:@";"]firstObject];
            alertMessage = [[alertMessage componentsSeparatedByString:@"("]lastObject];
            alertMessage = [[alertMessage componentsSeparatedByString:@")"]firstObject];
           ......略
        }
        self.httpData = nil;//清空數(shù)據(jù)
    }
}

4.獲取課表


獲取課表這里其實原理上也差不多的,按照截圖的格式去構(gòu)造URL就可以了赊瞬,訪問網(wǎng)頁用的還是GET方式先煎。(截這張圖的時候因為我太久沒操作教務系統(tǒng)了,所以系統(tǒng)給我自動退出了只好重新登錄巧涧,截圖里的隨機字段會和上面的不一樣薯蝎,但實際上代碼實現(xiàn)用的還是同一個隨機字段)。
關于URL的說明:xh后接的是學號谤绳,xm后的是姓名(就是登錄時候獲取的那個占锯,中文字符編碼要處理一下),gnmkdm=N121603這個固定就好(不清楚是啥)
帶有中文的url和NSString中文的轉(zhuǎn)換

- (IBAction)courseGetting:(id)sender {
    NSString *urlstr = [NSString stringWithFormat:@"http://%@/%@/xskbcx.aspx?xh=%@&xm=%@&gnmkdm=N121603",self.httpHost,self.randomStr,xuehao ,self.name];
    urlstr = [urlstr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
    NSURL *url = [NSURL URLWithString:urlstr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod= @"GET";
    [request addValue:[NSString stringWithFormat:@"http://%@/%@/xs_main.aspx?xh=%@",self.httpHost,self.randomStr,xuehao] forHTTPHeaderField:@"Referer"];//這句一定不能漏
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
    task.taskDescription = @"courseget";
    [task resume];
}

這里還有一個問題要注意一下的缩筛,[request addValue:[NSString stringWithFormat:@"http://%@/%@/xs_main.aspx?xh=%@",self.httpHost,self.randomStr,xuehao] forHTTPHeaderField:@"Referer"];這句一定不能漏消略,表明這個頁面時從哪里跳轉(zhuǎn)過來的(做模擬登錄時還不要求一定要提供Referer請求頭)。

請求成功后就可以從獲取到的html Data 中得到課程數(shù)據(jù)了瞎抛,具體要怎么解析艺演,根據(jù)實際獲得的html數(shù)據(jù)格式實際分析吧。

最后的一點感想:
不同學校的方正教務系統(tǒng)或多或少都會有些不同,但本質(zhì)上原理還是相同的胎撤。在做教務系統(tǒng)爬蟲的時候根據(jù)實際情況實際分析晓殊,多利用瀏覽器的開發(fā)者工具分析網(wǎng)頁行為。
demo在這里

正方教務管理系統(tǒng)IOS客戶端
使用 ASIHttpRequest 模擬登陸正方教務系統(tǒng)的幾點心得
畢業(yè)設計想把學校教務系統(tǒng)的功能模塊做成手機APP?
PHP模擬登陸正方系統(tǒng)獲取課表伤提、成績(一看就懂N装场!V啄小)
關于TFHpple第三方庫解析html的用法:
https://yq.aliyun.com/articles/30672
https://segmentfault.com/a/1190000003860297

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末介汹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子次伶,更是在濱河造成了極大的恐慌痴昧,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠王,死亡現(xiàn)場離奇詭異赶撰,居然都是意外死亡,警方通過查閱死者的電腦和手機柱彻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門豪娜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人哟楷,你說我怎么就攤上這事瘤载。” “怎么了卖擅?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵鸣奔,是天一觀的道長。 經(jīng)常有香客問我惩阶,道長挎狸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任断楷,我火速辦了婚禮锨匆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘冬筒。我一直安慰自己恐锣,他們只是感情好,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布舞痰。 她就那樣靜靜地躺著土榴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匀奏。 梳的紋絲不亂的頭發(fā)上鞭衩,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天学搜,我揣著相機與錄音,去河邊找鬼论衍。 笑死瑞佩,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的坯台。 我是一名探鬼主播炬丸,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蜒蕾!你這毒婦竟也來了稠炬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤咪啡,失蹤者是張志新(化名)和其女友劉穎首启,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體撤摸,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡毅桃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了准夷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钥飞。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖衫嵌,靈堂內(nèi)的尸體忽然破棺而出读宙,到底是詐尸還是另有隱情,我是刑警寧澤楔绞,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布结闸,位于F島的核電站,受9級特大地震影響酒朵,放射性物質(zhì)發(fā)生泄漏膀估。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一耻讽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧帕棉,春花似錦针肥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至即纲,卻和暖如春具帮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工蜂厅, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匪凡,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓掘猿,卻偏偏與公主長得像病游,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子稠通,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容