Java爬取frame的课程表_从爬取湖北某高校hub教务系统课表浅谈Java信息抓取的实现 —— import java.*;...
原創(chuàng)文章與源碼,如果轉(zhuǎn)載請(qǐng)注明來(lái)源。
一、概述
整個(gè)系統(tǒng)用Java開發(fā)。我們現(xiàn)在要做的是類似于超級(jí)課程表、課程格子之類的功能:輸入一個(gè)學(xué)生的教務(wù)系統(tǒng)賬號(hào)、密碼,得到Ta的課程表信息。點(diǎn)擊進(jìn)入課表查詢,我們發(fā)現(xiàn)了這樣的頁(yè)面:
這就是我們需要的結(jié)果。其實(shí)思路很簡(jiǎn)單,用java訪問這個(gè)鏈接,拿到Html字符串,然后解析鏈接等需要的數(shù)據(jù)。
因此,我們發(fā)送HTTP請(qǐng)求GET?http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp,這樣就可以等到課表的內(nèi)容了。但是,這個(gè)頁(yè)面必須是在登錄之后才能訪問的,如果直接發(fā)送GET請(qǐng)求的話,系統(tǒng)會(huì)認(rèn)為你沒有登錄,所以會(huì)拒絕你的請(qǐng)求(跳轉(zhuǎn)到登錄頁(yè)面),所以,在發(fā)送GET請(qǐng)求之前,必須實(shí)現(xiàn)模擬登錄。
二、JAVA中GET/POST請(qǐng)求的實(shí)現(xiàn)
在進(jìn)行模擬登錄之前,我們需要了解一些基本知識(shí)。
在java中,實(shí)現(xiàn)執(zhí)行http請(qǐng)求有多種方式,比如使用urlconnection等等,不過在這里我們使用apache-httpclient。HttpClient 是 Apache Jakarta Common 下的子項(xiàng)目,可以用來(lái)提供高效的、最新的、功能豐富的支持 HTTP 協(xié)議的客戶端編程工具包,并且它支持 HTTP 協(xié)議最新的版本和建議。
1 //1. 首先創(chuàng)建一個(gè)CookieStore用于存儲(chǔ)Cookie數(shù)據(jù)
2
3 CookieStore cookieStore = newBasicCookieStore();4
5 //2.創(chuàng)建httpclient,并關(guān)聯(lián)CookieStore
6
7 DefaultHttpClient client = newDefaultHttpClient();8 client.setCookieStore(cookieStore);9
10 client.getParams().setParameter(CookieSpecPNames.DATE_PATTERNS, Arrays.asList("EEE, dd MMM yyyy HH:mm:ss z")); //該代碼用于設(shè)置cookie中的expires時(shí)間日期格式。添加該代碼是因?yàn)槿A科網(wǎng)站使用的cookie日期格式不是標(biāo)準(zhǔn)格式。
創(chuàng)建GET請(qǐng)求:
1 HttpGet get = new HttpGet("http://xxxx");2 get.setHeader("xxx","xxx");3 get.setHeader("xxxx","xxxx");4 get.setHeader("Cookie","cookie");5 HttpResponse response =client.execute(get);6 get.releaseConnection();
創(chuàng)建POST請(qǐng)求:
HttpPost post = new HttpPost("http://xxxx");
post.setHeader("xxx","xxxx");
post.setHeader("xxxx","xxxx");
post.setHeader("Cookie","cookie");//對(duì)post請(qǐng)求發(fā)送參數(shù)
List nvps = new ArrayList();
nvps.add(new BasicNameValuePair("username", "111"));
nvps.add(new BasicNameValuePair("password", "xxx"));
post.setEntity(new UrlEncodedFormEntity(nvps,"utf-8"));
HttpResponse response = client.execute(post);
從CookieStore得到Cookie字符串
1 StringBuilder stringBuilder = newStringBuilder();2 for(Cookie cookie:cookieStore.getCookies()){3 String key =cookie.getName();4 String value =cookie.getValue();5 stringBuilder.append(key).append("=").append(value).append(";");6 }7
8 return stringBuilder.toString();
從HttpResponse對(duì)象中獲取執(zhí)行的結(jié)果(輸入流)
1 InputStream inputStream =response.getEntity().getContent();2 //獲取結(jié)果的輸入流
從輸入流中獲取字符串,可以用如下的函數(shù)(注意編碼問題)
1 public static String in2Str(InputStream in) throwsIOException{2 BufferedReader rd = new BufferedReader(new InputStreamReader(in,"utf-8"));3 String line = null;4 StringBuilder sb = newStringBuilder();5 while ((line=rd.readLine())!=null) {6 sb.append(line).append("\r\n");7 }8 returnsb.toString();9 }
Jsoup解析
以上幾段程序代碼就是我們程序工作的核心了,在我的源碼中,對(duì)這些代碼進(jìn)行了封裝,你可以輕松找到它們(在spider包中)。
三、模擬登錄的實(shí)現(xiàn)
一般地,在java web中,登錄可以由類似于如下的代碼實(shí)現(xiàn):
前臺(tái)html的代碼如下:
1 2 用戶名
3
4 密碼
5
6
7
8
9
后臺(tái)action如下(spring mvc):
1 @RequestMapping("/login.action")2 publicString loginSubmit(HttpServletRequest request,HttpServletResponse response,3 @RequestParam("username") String username,@RequestParam("password") String password) {4 5
6 if(username==null||password==null){7 request.setAttribute("msg", "您的輸入有誤!");8 return "/login";9 }10 if(username.equals("")||password.equals("")){11 request.setAttribute("msg", "您的輸入有誤!");12 return "/login";13 }14 User user =userDao.getUser(username, password);15 if(user==null){16 //TODO 登錄失敗
17 return "xxx";18 }else{19 request.getSession().setAttribute("loginUser",user); //保存登錄后的用戶到session
20 //TODO 登錄成功
21 return "xxx";22 }23
24 }
其實(shí)登錄也就是發(fā)送POST請(qǐng)求,服務(wù)器接收到POST請(qǐng)求(Request)后,對(duì)其處理(查詢數(shù)據(jù)庫(kù)等),返回Response。
其中最關(guān)鍵的與身份驗(yàn)證有關(guān)的操作就是request.getSession().setAttribute("loginUser",user) 了。將登錄后的用戶保存到session中,這樣,在訪問其他需要身份驗(yàn)證的頁(yè)面時(shí),服務(wù)器只需要判斷session中是否有該用戶,如果有就表示身份驗(yàn)證通過,如果沒有則表示身份驗(yàn)證失敗。而java中對(duì)于session的實(shí)現(xiàn)是依賴于cookie中的jsessionid屬性的(參考文檔),如果模擬出登錄請(qǐng)求后(也就是模擬一個(gè)POST請(qǐng)求),得到cookie(也就是得到j(luò)sessionid),下次請(qǐng)求時(shí)將cookie發(fā)送給服務(wù)器以表明身份,不就可以訪問帶有權(quán)限的URL了么?
首先我們需要下載webscrab,這個(gè)軟件有多強(qiáng)大這里就不細(xì)說了,大家可以自行百度下載地址。下載后是.jar格式,怎么運(yùn)行不用我多說了吧。關(guān)于webscrab的使用見webscrab.pdf
(webscrab的核心設(shè)置)
1.攔截登錄時(shí)的POST請(qǐng)求:(如果不會(huì)請(qǐng)參考使用說明或者百度webscrab的使用)
這里我們需要這兩種信息:Parsed和URLEncoded,其中,Parsed是POST請(qǐng)求的URL和Header,而URLEncoded則是該請(qǐng)求發(fā)送的參數(shù)。
我們先看Parsed部分,Parsed部分是由Method、URL和響應(yīng)頭(以表示的Map型結(jié)構(gòu))組成。Method表示該請(qǐng)求是POST請(qǐng)求還是GET請(qǐng)求;響應(yīng)頭對(duì)應(yīng)了HttpGet/HttpPost類中的setHeader方法,大多數(shù)Header不是必須的,但是在請(qǐng)求時(shí),最好加上相同的Header,以免出現(xiàn)一些問題。例如:如果沒有Host(該值表示域名,例如url是http://www.abc.com/login.action,則該值就是www.abc.com)或者Referer頭(表示發(fā)起請(qǐng)求時(shí)的頁(yè)面,告訴服務(wù)器我是從哪里過來(lái)的,比如是http://www.abc.com/login.html),在某些情況下可能會(huì)出現(xiàn)404錯(cuò)誤。【這可能是由于服務(wù)器設(shè)置了防盜鏈機(jī)制】
因此,最好的處理是將攔截到的Header,都添加到HttpGet/HttpPost中。
或者以一個(gè)HashMap的方式存儲(chǔ):(spider.tools.hub.HubEventAdapter和SHubEventAdapter)
1 HashMap map = new HashMap<>();2 map.put("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");3 map.put("Referer", "http://hub.hust.edu.cn/index.jsp");4 map.put("Accept-Language","zh-CN,zh;q=0.8");5 map.put("User-Agent", useragent);6 map.put("Accept-Encoding", "gzip, deflate");7 map.put("Host", "hub.hust.edu.cn");8 map.put("Proxy-Connection", "Keep-Alive");9 map.put("Pragma", "no-cache");
遍歷它們,調(diào)用setHeader方法。
下面我們?cè)賮?lái)看URLEncoded部分,該部分表示POST請(qǐng)求發(fā)送給服務(wù)器的數(shù)據(jù)。我們發(fā)現(xiàn),其中有三項(xiàng)數(shù)據(jù)username、password、ln。
我們發(fā)現(xiàn),這里的password值并不是我們剛剛輸入的密碼,而似乎是一種加密之后的結(jié)果,查看http://hub.hust.edu.cn/index.jsp的源代碼,發(fā)現(xiàn)如下代碼:(第210行)
var password = $("input[name='password']").val();if(password==""){
alert("請(qǐng)輸入用戶密碼(Password)");
$("input[name='password']").focus();return false;
}
$("input[name='password']").val($.base64.encode(password));//我們要找的東西在這里!!!
很明顯,$.base64,這是base64加密,所以在我們發(fā)送POST請(qǐng)求之前,應(yīng)該對(duì)密碼進(jìn)行一次base64加密后再發(fā)送。(可以根據(jù)密碼長(zhǎng)度判斷是什麼加密類型,一般都是base64加密,32位一般是MD5加密,再長(zhǎng)一些則可能是AES加密,如果結(jié)果非常長(zhǎng)則很可能是RSA加密。)
而ln值,你可以嘗試反復(fù)刷新頁(yè)面,反復(fù)提交、攔截,會(huì)發(fā)現(xiàn)每次ln值都會(huì)改變,對(duì)于這樣每次會(huì)改變的值,我們采取這樣的方式:
GET /index.jsp -> cookie、ln??- >POST /login.action
首先對(duì)首頁(yè)執(zhí)行GET方法,獲取首頁(yè)的HTML內(nèi)容,并保存cookie。、
接下來(lái)用Jsoup解析首頁(yè)的html內(nèi)容,得到ln值。
最后將ln值與cookie,加上用戶輸入的用戶名、密碼一起POST到/login.action 。
3.中轉(zhuǎn)登錄
在發(fā)送POST請(qǐng)求后,使用(二)中提供的in2Str方法,得到返回結(jié)果,居然發(fā)現(xiàn)結(jié)果如下:
1
2
3
4
5
7
8
9
10
12
13
14 varurl=document.getElementById("url").value;15 document.form1.action=url+'hublogin.action';16 document.form1.submit();17
18
原來(lái)這就是華科中轉(zhuǎn)登錄的機(jī)制啊。還是一樣的發(fā)送POST請(qǐng)求
POST?http://s.hub.hust.edu.cn/hublogin.action
usertype,username,password,url,key1,key2,F_App。
注意:此時(shí)的域名已經(jīng)改為http://s.hub.hust.edu.cn/了,那么Header中的Host和Refer值最好也改為http://s.hub.hust.edu.cn/。
4.返回
使用下面代碼獲取POST執(zhí)行后的整型返回值:
int code = response.getStatusLine().getStatusCode();
如果code=302則登錄成功,否則登錄失敗。(302也就是表示登錄已經(jīng)成功,可以跳轉(zhuǎn)到其他頁(yè)面了。)
四、課表的獲取
在第三部登錄成功之后,我們發(fā)現(xiàn)GET ?http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp 似乎不包含我想要的課表信息,于是繼續(xù)使用webscrab。
點(diǎn)擊“課表查詢”,繼續(xù)攔截請(qǐng)求,通過幾次攔截,發(fā)現(xiàn)有一個(gè)請(qǐng)求應(yīng)該包含我需要的課表信息。
因此,還是使用跟之前類似的方法,發(fā)送POST請(qǐng)求
POST http://s.hub.hust.edu.cn:80/aam/score/CourseInquiry_ido.action
start = 2016-02-29
end = 2016-04-11
別忘記帶上第三步(登錄后)的Cookie!
最后得到的結(jié)果如下:
當(dāng)當(dāng)~~當(dāng)————
點(diǎn)擊下一月,URLEncoded變成了:
這樣的日期似乎比較亂啊,
如果將start設(shè)置為2016-03-01,end設(shè)置為2016-03-31,獲取的就是3月的課表。
至此,華科大教務(wù)系統(tǒng)課表爬取完成!
五、總結(jié)
我的代碼的編程思路:(用抽象語(yǔ)言表述)
1 //整體代碼用抽象Java語(yǔ)言表示,這些代碼只是表示設(shè)計(jì)思路。不能運(yùn)行
2
3 Header header1 = {"refer","http://hub.hust.edu.cn/index.jsp","host":"hub.hust.edu.cn"}; //響應(yīng)頭header1
4 Header header2 = {"refer","http://s.hub.hust.edu.cn/index.jsp","host","s.hub.hust.edu.cn"}; //響應(yīng)頭header2
5
6 Get get = new Get ("http://hub.hust.edu.cn/index.jsp").header(header1); //進(jìn)入首頁(yè)
7 Response res1 =get.execute();8 String content1 = res1.getContent(); //獲取index.jsp的html代碼
9 String ln = getln(Jsoup.parse(content1)); //使用jsoup解析index.jsp的html代碼,從中獲取出ln(input hidden name='ln'的value)
10
11 Post post = new Post("http://hub.hust.edu.cn/hubulogin.action").header(header1); //準(zhǔn)備模擬登錄的,POST提交12
13 //添加post數(shù)據(jù)
14 post.add("username","123456789");15 post.add("password",base64encode("mypassword"));16 post.add("ln",ln)17
18 Response res2 = post.execute(); //執(zhí)行post請(qǐng)求
19
20 Post post2 = new Post("http://s.hub.hust.edu.cn/hublogin.action").header(header2); //中轉(zhuǎn)登錄,注意header的變化
21 Document dform = Jsoup.parse(res2.getContent()); //得到返回的動(dòng)態(tài)表單內(nèi)容
22 post.add("usertype",getUserType(d));23 post.add("username",getUserName(d));24 post.add("password",getPassword(d));25 post.add("url",getURL(d));26 post.add("key1",getKey1(d));27 post.add("key2",getKey2(d));28 post.add("F_App",getFApp(d));29
30 Response res3 =post2.execute();31
32 if(res3.getStatusLine().getStatusCode()==302){33 syso("登錄成功");34 }else{35 syso("登錄失敗");36 return;37 }38
39
40 Post kbPost = new Post("http://s.hub.hust.edu.cn:80/aam/score/CourseInquiry_ido.action").header(header2); //獲取課表的post請(qǐng)求
41 kbPost.add("start","2016-03-01");42 kbPost.add("end","2016-03-30");43 Response res4 =kbPost.execute();44 if(res4.getStatusLine().getStatusCode()==200){45 syso(res4.getContent());46 }else{47 syso("服務(wù)器異常!");48 }
擴(kuò)展:
你可以直接在我的基礎(chǔ)上擴(kuò)展,適用于其他學(xué)校的“課程格子”。
你可以選擇繼承AbstractTask類來(lái)表示一項(xiàng)POST/GET請(qǐng)求任務(wù),用getEvent方法來(lái)表示該任務(wù)的具體內(nèi)容,最好是對(duì)SpiderTaskEvent使用適配器模式。
示例代碼如下:(這是基于另一個(gè)學(xué)校的教務(wù)系統(tǒng)實(shí)現(xiàn))
1 public abstract class JwxtEventAdapter implementsSpiderTaskEvent{2
3 private static final String useragent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko";4 Listheaders;5
6 privateCookies cookies;7 publicJwxtEventAdapter(Cookies cookies){8 HashMap map = new HashMap<>();9 map.put("Accept","text/html, application/xhtml+xml, image/jxr, */*");10 map.put("Referer", "http://jwxt.hubu.edu.cn/");11 map.put("Accept-Language","zh-Hans-CN,zh-Hans;q=0.8,en-GB;q=0.5,en;q=0.3");12 map.put("User-Agent", useragent);13 map.put("Accept-Encoding", "gzip, deflate");14 map.put("Host", "jwxt.hubu.edu.cn");15 map.put("Proxy-Connection", "Keep-Alive");16 map.put("Pragma", "no-cache");17
18 headers =StringHeader.build(map);19 this.cookies =cookies;20 }21 publicJwxtEventAdapter(){22 this(null);23 }24
25 @Override26 public void beforeExecute(SpiderRequest request) throwsIOException {27 request.setTimeout(20000);28 request.setHeaders(headers);29 if(cookies!=null){30 request.setCookie(cookies);31 }32 }33
34 @Override35 public voidafterExecute(SpiderRequest request, SpiderResponse response)36 throwsIOException {37
38
39 }40
41 }
1 public class JwxtRandomTask extendsAbstractTask {2
3 privateString random;4
5 privateImage image;6
7 publicImage getImage(){8 returnimage;9 }10
11 /**
12 *@paramclient13 */
14 publicJwxtRandomTask(HttpClient client) {15 super(client);16
17 }18
19 publicString getRandom() {20 returnrandom;21 }22
23 @Override24 publicMethod getMethod() {25
26 returnMethod.GET;27 }28
29 @Override30 publicString getURL() {31
32 return "http://jwxt.hubu.edu.cn/verifycode.servlet";33 }34
35
36
37 @Override38 publicSpiderTaskEvent getEvent() {39
40 return newJwxtEventAdapter() {41
42 @Override43 public voidafterExecute(SpiderRequest request,44 SpiderResponse response) throwsIOException {45 image =ImageIO.read(response.getContentStream());46
47 }48 };49 }
我在寫這個(gè)程序的時(shí)候,確實(shí)遇到了一些麻煩,就比如本文提到的404的問題;以及我可能是有點(diǎn)急躁吧,一開始沒有注意到其實(shí)這個(gè)登錄action是有一次中轉(zhuǎn)的,導(dǎo)致后面的GET操作都被系統(tǒng)提示為非法操作。
確實(shí)做這個(gè)讓自己感慨萬(wàn)千,大學(xué)幾年來(lái)一直難以踏踏實(shí)實(shí)的做一些事情,太浮躁,C語(yǔ)言、算法、Java等等都是不精,只學(xué)了一點(diǎn)皮毛。一個(gè)大三學(xué)生班門弄斧,滿紙荒唐言,如有錯(cuò)誤還請(qǐng)各位大神批評(píng)和指出,非常感謝!最后感謝一下提供賬號(hào)的同學(xué)d=====( ̄▽ ̄*)b。
希望以后能越走越遠(yuǎn)!import java.*;
ps.我的源碼下載地址:下載1下載2
總結(jié)
以上是生活随笔為你收集整理的Java爬取frame的课程表_从爬取湖北某高校hub教务系统课表浅谈Java信息抓取的实现 —— import java.*;...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大耳牛是如何获得当代年轻人的热捧?
- 下一篇: win10怎么检查更新 win10如何查