原創文章與源碼,若是轉載請註明來源。 javascript
開發環境:Myeclipse,依賴包:apache-httpclient 、 Jsoup、base64php
1、概述html
整個系統用Java開發。咱們如今要作的是相似於超級課程表、課程格子之類的功能:輸入一個學生的教務系統帳號、密碼,獲得Ta的課程表信息。點擊進入課表查詢,咱們發現了這樣的頁面:java
這就是咱們須要的結果。其實思路很簡單,用java訪問這個連接,拿到Html字符串,而後解析連接等須要的數據。web
這個頁面的URL是http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp算法
所以,咱們發送HTTP請求GET http://s.hub.hust.edu.cn/aam/report/scheduleQuery.jsp,這樣就能夠等到課表的內容了。可是,這個頁面必須是在登陸以後才能訪問的,若是直接發送GET請求的話,系統會認爲你沒有登陸,因此會拒絕你的請求(跳轉到登陸頁面),因此,在發送GET請求以前,必須實現模擬登陸。spring
2、JAVA中GET/POST請求的實現數據庫
在進行模擬登陸以前,咱們須要瞭解一些基本知識。apache
首先請看關於HTTP請求的基礎知識:http://www.cnblogs.com/yin-jingyu/archive/2011/08/01/2123548.html編程
在java中,實現執行http請求有多種方式,好比使用urlconnection等等,不過在這裏咱們使用apache-httpclient。HttpClient 是 Apache Jakarta Common 下的子項目,能夠用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,而且它支持 HTTP 協議最新的版本和建議。
1 //1. 首先建立一個CookieStore用於存儲Cookie數據 2 3 CookieStore cookieStore = new BasicCookieStore(); 4 5 //2.建立httpclient,並關聯CookieStore 6 7 DefaultHttpClient client = new DefaultHttpClient(); 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<NameValuePair> nvps = new ArrayList<NameValuePair>(); 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 = new StringBuilder(); 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) throws IOException{ 2 BufferedReader rd = new BufferedReader(new InputStreamReader(in,"utf-8")); 3 String line = null; 4 StringBuilder sb = new StringBuilder(); 5 while ((line=rd.readLine())!=null) { 6 sb.append(line).append("\r\n"); 7 } 8 return sb.toString(); 9 }
Jsoup解析
參考資料:http://www.open-open.com/jsoup/
以上幾段程序代碼就是咱們程序工做的核心了,在個人源碼中,對這些代碼進行了封裝,你能夠輕鬆找到它們(在spider包中)。
3、模擬登陸的實現
通常地,在java web中,登陸能夠由相似於以下的代碼實現:
前臺html的代碼以下:
1 <form action="/login.action" method="post" > 2 <label for="username">用戶名</label> 3 <input id="username" name=「username" type="text" /> 4 <label for="password">密碼</label> 5 <input id="password" name=「password" type="password" /> 6 <input type="submit" value="登陸" /> 7 8 9 </form>
後臺action以下(spring mvc):
1 @RequestMapping("/login.action") 2 public String 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和響應頭(以<Header,Value>表示的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<String, String > 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 <body> 2 <form action="" method="post" name="form1"> 3 <input type="hidden" id="usertype" name="usertype" value="xs"> 4 <input type="hidden" id="username" name="username" value="U2013"> 5 <input type="hidden" id="password" name="password" value="1061d0c (這裏爲了用戶隱私我沒有顯示) 6 269c"> 7 <input type="hidden" id="url" name="url" value="http://s.hub.hust.edu.cn/"> 8 <input type="hidden" name="key1" value="367265"/> 9 <input type="hidden" name="key2" value="a261ab7e0cecb430651868727cd3fb35"/> 10 <input type="hidden" name="F_App" value="From kslgin. App:app61.dc.hust.edu.cn 11 |app614|IP:10.10.10.247"/> 12 </form> 13 <script type="text/javascript"> 14 var url = document.getElementById("url").value; 15 document.form1.action=url+'hublogin.action'; 16 document.form1.submit(); 17 </script> 18 </body>
原來這就是華科中轉登陸的機制啊。仍是同樣的發送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也就是表示登陸已經成功,能夠跳轉到其餘頁面了。)
4、課表的獲取
在第三部登陸成功以後,咱們發現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月的課表。
至此,華科大教務系統課表爬取完成!
5、總結
個人代碼的編程思路:(用抽象語言表述)
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 implements SpiderTaskEvent{ 2 3 private static final String useragent = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko"; 4 List<StringHeader> headers; 5 6 private Cookies cookies; 7 public JwxtEventAdapter(Cookies cookies){ 8 HashMap<String, String> 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 public JwxtEventAdapter(){ 22 this(null); 23 } 24 25 @Override 26 public void beforeExecute(SpiderRequest request) throws IOException { 27 request.setTimeout(20000); 28 request.setHeaders(headers); 29 if(cookies!=null){ 30 request.setCookie(cookies); 31 } 32 } 33 34 @Override 35 public void afterExecute(SpiderRequest request, SpiderResponse response) 36 throws IOException { 37 38 39 } 40 41 }
1 public class JwxtRandomTask extends AbstractTask { 2 3 private String random; 4 5 private Image image; 6 7 public Image getImage(){ 8 return image; 9 } 10 11 /** 12 * @param client 13 */ 14 public JwxtRandomTask(HttpClient client) { 15 super(client); 16 17 } 18 19 public String getRandom() { 20 return random; 21 } 22 23 @Override 24 public Method getMethod() { 25 26 return Method.GET; 27 } 28 29 @Override 30 public String getURL() { 31 32 return "http://jwxt.hubu.edu.cn/verifycode.servlet"; 33 } 34 35 36 37 @Override 38 public SpiderTaskEvent getEvent() { 39 40 return new JwxtEventAdapter() { 41 42 @Override 43 public void afterExecute(SpiderRequest request, 44 SpiderResponse response) throws IOException { 45 image = ImageIO.read(response.getContentStream()); 46 47 } 48 }; 49 }
我在寫這個程序的時候,確實遇到了一些麻煩,就好比本文提到的404的問題;以及我多是有點急躁吧,一開始沒有注意到其實這個登陸action是有一次中轉的,致使後面的GET操做都被系統提示爲非法操做。
確實作這個讓本身感慨萬千,大學幾年來一直難以踏踏實實的作一些事情,太浮躁,C語言、算法、Java等等都是不精,只學了一點皮毛。一個大三學生班門弄斧,滿紙荒唐言,若有錯誤還請各位大神批評和指出,很是感謝!最後感謝一下提供帳號的同窗d=====( ̄▽ ̄*)b。
但願之後能越走越遠!import java.*;