最近做學(xué)校教務(wù)系統(tǒng)爬蟲,這里寫一下我遇到的一些問題和心得。
1.用到的工具
Chrome的開發(fā)者工具:分析網(wǎng)頁行為,查看每次HTTP請求命令與參數(shù)等。
TFhepple: HTML解析庫。
demo中關(guān)于網(wǎng)絡(luò)請求部分是直接使用原生NSURLSession來完成的。
2.分析網(wǎng)頁行為
2.1打開教務(wù)系統(tǒng)網(wǎng)頁
-
當(dāng)我輸入教務(wù)系統(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),我們學(xué)校的教務(wù)系統(tǒng),上面重定向最后的Request URL中的host地址是會變化的,可能這次訪問的host地址是110.65.10.191下次訪問得到的host地址就是110.65.10.204了。所以在這里我們也要把host地址保存下來。 -
關(guān)于驗證碼
在網(wǎng)上看到很多文章都說可以繞過驗證碼,但現(xiàn)在方正教務(wù)系統(tǒng)好像已經(jīng)修復(fù)這個bug了。驗證碼識別有很多種方法,在項目中我選擇把驗證碼圖片獲取下來,然后讓用戶手動輸入。
這里和驗證碼有關(guān)的是CheckCode.aspx(看到了吧?那個隨機字段又出現(xiàn)了)。如果我們在瀏覽器上直接訪問圖中那個Request URL,的確是可以獲得驗證碼圖片,但實際上它不是我們在教務(wù)系統(tǒng)上看到的那張。實際上,獲取驗證碼是需要帶上之前獲取的那個cookie的,這個cookie保證了我們的驗證碼,是和賬號密碼在同一個網(wǎng)頁上的。
這里總結(jié)一下,在打開教務(wù)系統(tǒng)網(wǎng)頁時我們需要獲取什么:1.cookie、2.重定向最后產(chǎn)生的隨機字段、3.重定向最后的Host地址
2.2登錄

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

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

3.代碼實現(xiàn)模擬登錄
一些屬性的說明:
@property (nonatomic ,strong)NSURLSession *session;
@property (nonatomic ,strong)NSString *mainUrl;//教務(wù)系統(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é)束,就可以在響應(yīng)頭(重定向最后200那一步的響應(yīng)頭)獲得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中獲取,響應(yīng)的數(shù)據(jù)不是一次性返回的沒所以要在- URLSession: dataTask: didReceiveData:方法中把數(shù)據(jù)拼接起來。在網(wǎng)絡(luò)請求結(jié)束時再提取viewState。然后還有一點,viewState里面的特殊字符“+”和"="要做編碼處理,+替換成%2B,=替換成%3D
然后關(guān)于編碼問題:正方教務(wù)管理系統(tǒng)IOS客戶端這篇文章里面說到:
正方教務(wù)系統(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=學(xué)生&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];
}
登錄成功后,主要這里要獲得一個學(xué)生姓名的參數(shù),這個姓名的值在后面獲取課表的時候要用到。在html中像是這樣的:

如果登錄失敗,就提取相應(yīng)的錯誤信息對用戶進行提示。
- (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方式。(截這張圖的時候因為我太久沒操作教務(wù)系統(tǒng)了,所以系統(tǒng)給我自動退出了只好重新登錄,截圖里的隨機字段會和上面的不一樣,但實際上代碼實現(xiàn)用的還是同一個隨機字段)。
關(guān)于URL的說明:xh后接的是學(xué)號,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ù)格式實際分析吧。
最后的一點感想:
不同學(xué)校的方正教務(wù)系統(tǒng)或多或少都會有些不同,但本質(zhì)上原理還是相同的。在做教務(wù)系統(tǒng)爬蟲的時候根據(jù)實際情況實際分析,多利用瀏覽器的開發(fā)者工具分析網(wǎng)頁行為。
demo在這里
正方教務(wù)管理系統(tǒng)IOS客戶端
使用 ASIHttpRequest 模擬登陸正方教務(wù)系統(tǒng)的幾點心得
畢業(yè)設(shè)計想把學(xué)校教務(wù)系統(tǒng)的功能模塊做成手機APP?
PHP模擬登陸正方系統(tǒng)獲取課表、成績(一看就懂?。。。?/a>
關(guān)于TFHpple第三方庫解析html的用法:
https://yq.aliyun.com/articles/30672
https://segmentfault.com/a/1190000003860297








