Java爬取frame的课程表_从爬取湖北某高校hub教务系统课表浅谈Java信息抓取的实现 —— import java.*;...
原創文章與源碼,如果轉載請注明來源。
一、概述
整個系統用Java開發。我們現在要做的是類似于超級課程表、課程格子之類的功能:輸入一個學生的教務系統賬號、密碼,得到Ta的課程表信息。點擊進入課表查詢,我們發現了這樣的頁面:
這就是我們需要的結果。其實思路很簡單,用java訪問這個鏈接,拿到Html字符串,然后解析鏈接等需要的數據。
因此,我們發送HTTP請求GET?http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp,這樣就可以等到課表的內容了。但是,這個頁面必須是在登錄之后才能訪問的,如果直接發送GET請求的話,系統會認為你沒有登錄,所以會拒絕你的請求(跳轉到登錄頁面),所以,在發送GET請求之前,必須實現模擬登錄。
二、JAVA中GET/POST請求的實現
在進行模擬登錄之前,我們需要了解一些基本知識。
在java中,實現執行http請求有多種方式,比如使用urlconnection等等,不過在這里我們使用apache-httpclient。HttpClient 是 Apache Jakarta Common 下的子項目,可以用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,并且它支持 HTTP 協議最新的版本和建議。
1 //1. 首先創建一個CookieStore用于存儲Cookie數據
2
3 CookieStore cookieStore = newBasicCookieStore();4
5 //2.創建httpclient,并關聯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")); //該代碼用于設置cookie中的expires時間日期格式。添加該代碼是因為華科網站使用的cookie日期格式不是標準格式。
創建GET請求:
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();
創建POST請求:
HttpPost post = new HttpPost("http://xxxx");
post.setHeader("xxx","xxxx");
post.setHeader("xxxx","xxxx");
post.setHeader("Cookie","cookie");//對post請求發送參數
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對象中獲取執行的結果(輸入流)
1 InputStream inputStream =response.getEntity().getContent();2 //獲取結果的輸入流
從輸入流中獲取字符串,可以用如下的函數(注意編碼問題)
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解析
以上幾段程序代碼就是我們程序工作的核心了,在我的源碼中,對這些代碼進行了封裝,你可以輕松找到它們(在spider包中)。
三、模擬登錄的實現
一般地,在java web中,登錄可以由類似于如下的代碼實現:
前臺html的代碼如下:
1 2 用戶名
3
4 密碼
5
6
7
8
9
后臺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 }
其實登錄也就是發送POST請求,服務器接收到POST請求(Request)后,對其處理(查詢數據庫等),返回Response。
其中最關鍵的與身份驗證有關的操作就是request.getSession().setAttribute("loginUser",user) 了。將登錄后的用戶保存到session中,這樣,在訪問其他需要身份驗證的頁面時,服務器只需要判斷session中是否有該用戶,如果有就表示身份驗證通過,如果沒有則表示身份驗證失敗。而java中對于session的實現是依賴于cookie中的jsessionid屬性的(參考文檔),如果模擬出登錄請求后(也就是模擬一個POST請求),得到cookie(也就是得到jsessionid),下次請求時將cookie發送給服務器以表明身份,不就可以訪問帶有權限的URL了么?
首先我們需要下載webscrab,這個軟件有多強大這里就不細說了,大家可以自行百度下載地址。下載后是.jar格式,怎么運行不用我多說了吧。關于webscrab的使用見webscrab.pdf
(webscrab的核心設置)
1.攔截登錄時的POST請求:(如果不會請參考使用說明或者百度webscrab的使用)
這里我們需要這兩種信息:Parsed和URLEncoded,其中,Parsed是POST請求的URL和Header,而URLEncoded則是該請求發送的參數。
我們先看Parsed部分,Parsed部分是由Method、URL和響應頭(以表示的Map型結構)組成。Method表示該請求是POST請求還是GET請求;響應頭對應了HttpGet/HttpPost類中的setHeader方法,大多數Header不是必須的,但是在請求時,最好加上相同的Header,以免出現一些問題。例如:如果沒有Host(該值表示域名,例如url是http://www.abc.com/login.action,則該值就是www.abc.com)或者Referer頭(表示發起請求時的頁面,告訴服務器我是從哪里過來的,比如是http://www.abc.com/login.html),在某些情況下可能會出現404錯誤?!具@可能是由于服務器設置了防盜鏈機制】
因此,最好的處理是將攔截到的Header,都添加到HttpGet/HttpPost中。
或者以一個HashMap的方式存儲:(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");
遍歷它們,調用setHeader方法。
下面我們再來看URLEncoded部分,該部分表示POST請求發送給服務器的數據。我們發現,其中有三項數據username、password、ln。
我們發現,這里的password值并不是我們剛剛輸入的密碼,而似乎是一種加密之后的結果,查看http://hub.hust.edu.cn/index.jsp的源代碼,發現如下代碼:(第210行)
var password = $("input[name='password']").val();if(password==""){
alert("請輸入用戶密碼(Password)");
$("input[name='password']").focus();return false;
}
$("input[name='password']").val($.base64.encode(password));//我們要找的東西在這里!!!
很明顯,$.base64,這是base64加密,所以在我們發送POST請求之前,應該對密碼進行一次base64加密后再發送。(可以根據密碼長度判斷是什麼加密類型,一般都是base64加密,32位一般是MD5加密,再長一些則可能是AES加密,如果結果非常長則很可能是RSA加密。)
而ln值,你可以嘗試反復刷新頁面,反復提交、攔截,會發現每次ln值都會改變,對于這樣每次會改變的值,我們采取這樣的方式:
GET /index.jsp -> cookie、ln??- >POST /login.action
首先對首頁執行GET方法,獲取首頁的HTML內容,并保存cookie。、
接下來用Jsoup解析首頁的html內容,得到ln值。
最后將ln值與cookie,加上用戶輸入的用戶名、密碼一起POST到/login.action 。
3.中轉登錄
在發送POST請求后,使用(二)中提供的in2Str方法,得到返回結果,居然發現結果如下:
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
原來這就是華科中轉登錄的機制啊。還是一樣的發送POST請求
POST?http://s.hub.hust.edu.cn/hublogin.action
usertype,username,password,url,key1,key2,F_App。
注意:此時的域名已經改為http://s.hub.hust.edu.cn/了,那么Header中的Host和Refer值最好也改為http://s.hub.hust.edu.cn/。
4.返回
使用下面代碼獲取POST執行后的整型返回值:
int code = response.getStatusLine().getStatusCode();
如果code=302則登錄成功,否則登錄失敗。(302也就是表示登錄已經成功,可以跳轉到其他頁面了。)
四、課表的獲取
在第三部登錄成功之后,我們發現GET ?http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp 似乎不包含我想要的課表信息,于是繼續使用webscrab。
點擊“課表查詢”,繼續攔截請求,通過幾次攔截,發現有一個請求應該包含我需要的課表信息。
因此,還是使用跟之前類似的方法,發送POST請求
POST http://s.hub.hust.edu.cn:80/aam/score/CourseInquiry_ido.action
start = 2016-02-29
end = 2016-04-11
別忘記帶上第三步(登錄后)的Cookie!
最后得到的結果如下:
當當~~當————
點擊下一月,URLEncoded變成了:
這樣的日期似乎比較亂啊,
如果將start設置為2016-03-01,end設置為2016-03-31,獲取的就是3月的課表。
至此,華科大教務系統課表爬取完成!
五、總結
我的代碼的編程思路:(用抽象語言表述)
1 //整體代碼用抽象Java語言表示,這些代碼只是表示設計思路。不能運行
2
3 Header header1 = {"refer","http://hub.hust.edu.cn/index.jsp","host":"hub.hust.edu.cn"}; //響應頭header1
4 Header header2 = {"refer","http://s.hub.hust.edu.cn/index.jsp","host","s.hub.hust.edu.cn"}; //響應頭header2
5
6 Get get = new Get ("http://hub.hust.edu.cn/index.jsp").header(header1); //進入首頁
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); //準備模擬登錄的,POST提交12
13 //添加post數據
14 post.add("username","123456789");15 post.add("password",base64encode("mypassword"));16 post.add("ln",ln)17
18 Response res2 = post.execute(); //執行post請求
19
20 Post post2 = new Post("http://s.hub.hust.edu.cn/hublogin.action").header(header2); //中轉登錄,注意header的變化
21 Document dform = Jsoup.parse(res2.getContent()); //得到返回的動態表單內容
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請求
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("服務器異常!");48 }
擴展:
你可以直接在我的基礎上擴展,適用于其他學校的“課程格子”。
你可以選擇繼承AbstractTask類來表示一項POST/GET請求任務,用getEvent方法來表示該任務的具體內容,最好是對SpiderTaskEvent使用適配器模式。
示例代碼如下:(這是基于另一個學校的教務系統實現)
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 }
我在寫這個程序的時候,確實遇到了一些麻煩,就比如本文提到的404的問題;以及我可能是有點急躁吧,一開始沒有注意到其實這個登錄action是有一次中轉的,導致后面的GET操作都被系統提示為非法操作。
確實做這個讓自己感慨萬千,大學幾年來一直難以踏踏實實的做一些事情,太浮躁,C語言、算法、Java等等都是不精,只學了一點皮毛。一個大三學生班門弄斧,滿紙荒唐言,如有錯誤還請各位大神批評和指出,非常感謝!最后感謝一下提供賬號的同學d=====( ̄▽ ̄*)b。
希望以后能越走越遠!import java.*;
ps.我的源碼下載地址:下載1下載2
總結
以上是生活随笔為你收集整理的Java爬取frame的课程表_从爬取湖北某高校hub教务系统课表浅谈Java信息抓取的实现 —— import java.*;...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大耳牛是如何获得当代年轻人的热捧?
- 下一篇: java io流大全_Java IO流系