最近做學校教務系統(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