天大北洋園羽毛球場地預約
- 2019 年 10 月 6 日
- 筆記
[toc]
## 寫作動機
近期作者疏於運動,加上吃得又多,打籃球天冷,所以想去室內打打羽毛球,鍛煉一下身體,生命在於運動嘛,畢竟學電腦的人一天到晚地待在實驗室對著電腦對身體不好。但是白天又沒時間,所以就想著約個晚上的羽毛球場地,本來想著場地很好約,就沒當回事,偶爾閑暇想起來的時候去預約網站上看一下,可沒成想,接連好幾天都顯示無剩餘場地可約…比較生氣,遂寫此文,**以抒心中不平**!
## 摘要
天津大學場館中心預定平台對於大家來講應該都不陌生,該平台對外提供羽毛球、排球、籃球等場館的在線預約服務,其目的是為了減少大家使用場地時的衝突,方便師生進行體育鍛煉。然而,**場地是有限的**,面對大家日益增長的物質和文化需求,現有的場地資源已經顯得力不從心,難以滿足大家的需求,**搶訂場地的現象也逐漸蔚然成風**。為了探究現有場館的使用情況,本篇文章利用爬蟲等技術進行了北洋園校區羽毛球館場地預定數據的統計和分析,並分析了預定平台現有的工作流程和不足之處,並基於此研究設計和開源了一套自動預約軟體,同時為預定平台中心建設提出了一些改進建議,希望減少腳本搶訂等不公平現象的發生。
—
## 一、羽毛球館場地預約現狀
### 1.1 預約時間段和場地熱度分析
羽毛球場館共計12個場地,其中1-8號場地位於場館主場,9-12號場地位於東側跑道附件的次場。當然了,大家都知道的嘛,主場的場地空間和相關照明設施要更好一點。每個場地都有使用時間段限制,通常以1小時為單位,從早晨8:00到晚上10:00,同時有些場地由於上課等其他事情的需要,開放時間段有固定限制。場地預定平台每天中午12:00開放後天的場地預定,比如周一中午12:00後可以預約周三的場地,因此類推。下圖展示了某日中午12:30的可預約場地截圖,紅圈標明的地方是晚上7:00-9:00的場地,已經顯示無場地可以用或者只剩下次場,而其他時間段的場地餘量卻比較充裕,我們可以猜測是晚間段的場地比較熱門導致了<font color=red>**時間不平衡性**</font>的出現。
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/E8867512FD2A4C55A3FF563847F4E414/6330″ height=”205″ width=”250″>
為了驗證猜測,我們採用爬蟲對該網頁的數據進行定期抓取,每天中午12:00統計昨日場地的剩餘情況,將得到的資訊繪製出來,如圖1、2所示。圖1展示了周一至周日羽毛球館14個時間段場地的預約率,x軸代表每周天數,y軸代表每天的14個時間段,以小時為單位,z軸為標準化後的場地預定率 (e.g. 11/12=0.916,8/8=1),預定率越高代表該時間段來打球的人越多。我們將三維圖投影到xy平面上,如圖3所示。可以很明顯得看出:
1. 白天和晚上的差距就更為明顯,也表明大部分同學也<font color=red>**更傾向於晚上去場館打球**</font>。
2. <font color=red>**晚上19:00-21:00的時段最為熱門**</font>,而同樣是晚間的21:00-22:00相對來講場地比較寬鬆一些,可能是由於場館22:00閉館的原因導致大家不願意玩到太晚。
3. <font color=red>**上午場的人數很少**</font>,可能是由於上午大家基本都有功課要上,沒有精力來體育館打球。周六也出現了這種現象,而出現這種情況的原因大概是由於<font color=red>**睡懶覺**</font>。
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/DCE57026388C4EF5A1967ABFE6B88686/7728″ height=”205″ width=”250″>
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/5C5B90643E324B0CBC8E8216BE05DC04/7720″ height=”205″ width=”250″>
### 1.2 熱門場地競爭性分析
基於上述的熱點時段研究,我們可以得出晚上19:00-21:00時間段比較熱門的結論,這也與我們的常識一致,但是這個時間段究竟有多熱門呢?亦或者說這些時間段內的場地多久會被大家搶光?
針對這個問題,我們又抓取了每日12:00開放預約後的網頁數據,每隔30秒鐘取樣,用下一個時間點採到的數據減去上一個時間點的數據就可以得出在這30秒的間隔內有多少場地被預約走,根據此方法,我們繪製了19:00-20:00的8個主場地的預約變化曲線,如圖3、4所示,x軸代表時間,y軸代表8個主場地剩餘量,藍色數據點為多次取樣的平均值,紅色虛線為擬合曲線,我們可以發現:
1. <font color=red>**場地剩餘曲線呈現先快後慢的變化**</font>,開放預定的前5分鐘,已經有一半的場地被預定走,說明搶訂的人還是蠻多的。
2. 開放預定的前10秒內,已經有場地被預約走,有<font color=red>**很大的可能性存在腳本預約**</font>的情況,這個結論圖上反映得不太直觀,因為取樣存在時間延遲,而且事實上預約開放並不是準時的12點0分0秒,這裡會在後面敘述。
3. 8個主場<font color=red>**基本在30分鐘內被訂光**</font>,意思就是如果你不盯著時間點去搶訂,能搶到漏網之魚的幾率很小。
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/74464435AC7A48C3BF9F9802C5319232/7719″ height=”205″ width=”250″>
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/5AA79032341F436A8EDCCBDE960025C2/7722″ height=”205″ width=”250″>
文章寫到這裡,我們可以看**到羽毛球黃金時段的主場地資源匱乏確實很嚴重**,加上有些腳本搶場的存在,想提前預定上期待的場地可謂難上加難,而且大家也都沒有那麼多的時間去盯著天天盯著場地。但是,著急上火也沒有用,畢竟你也管不了有些人(或者機器),不過也不是沒有辦法,對付這些搶場地的最有效手段就是跟他們對著搶,就看誰的程式好、網速快了。第二章我將詳細講述如何去分析場地預定工作流程並編寫程式幫我們去實現小目標。
## 二、預備知識
### 2.1 啥是爬蟲???
互聯網提供的服務大多數是以網站的形式提供的,而我們需要的數據一般都是從網頁中獲取的,如電商網站商品資訊、商品的評論、微博的資訊等。網站的網頁是由HTML程式碼組成的,我們平時通過瀏覽器訪問網頁,發出請求後瀏覽器接收到對應的網頁源程式碼,然後將其解析為頁面內容再呈現給我們。
通俗來講,**爬蟲的基本原理很簡單,就是利用程式去代替人去訪問網站,並將目標數據保存下來**。爬蟲當中涉及到的知識點大概如下,學電腦的人應該不會陌生,這個到後面會一一用到,我們繼續往下走。
– [x] web後端知識,GET/POST,session等
– [x] 前端HTML、javascript等
– [x] Java Httpclient使用
– [x] Jsoup頁面解析包
– [ ] 正則表達式
– [ ] Timer定時器
– [x] Java mail包使用
> 注意:未打鉤代表可選用
### 2.2 如何用程式去模擬瀏覽器訪問網站?
通常我們訪問網站需要一個瀏覽器(e.g. IE,搜狗,Google),但是程式要想去訪問網站,當然需要模擬出來一個瀏覽器(呃…其實瀏覽器本身也是程式寫的),設計思路就是通過電腦程式來**模擬出一個用戶和瀏覽器,去替代人工進行場地預定**。
話還沒說完,出自於Java的Httpclient包就迫不及待地跳了出來,Httpclient包就是一個專門用於模擬瀏覽器的java工具包,使用起來也很簡單,這裡我們用一下小例子來演示下它的使用過程。首先,假設我們需要爬取體育館預約平台首頁的源程式碼,則需要引入Java的HttpClient包,新建一個httpclient對象,這個對象就相當於一個虛擬瀏覽器,通過鏈接發送請求,從而獲取到數據,二話不說,上程式碼:
“`
import org.apache.http.impl.client.*;
/**
* 爬蟲之httpclient使用Demo
*/
public class CrawDemo {
public static void main(String[] args) {
CloseableHttpClient httpClient = HttpClients.createDefault();//建立一個新的請求客戶端
HttpGet httpGet = new HttpGet(“http://cgzx.tju.edu.cn:8080/index.php/Book/Login/index.html”); //使用HttpGet方式請求網址
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet); //獲取網址的返回結果
HttpEntity entity = response.getEntity(); //獲取返回結果中的實體
System.out.println(EntityUtils.toString(entity)); //將返回的實體輸出
EntityUtils.consume(entity); //關閉實體與連接
} catch (IOException e) {
e.printStackTrace();
}
}
}
“`
顯然,這就是我們需要的網址對應的頁面的源程式碼,於是我們的第一個爬蟲demo就演示完畢了。
“`
<html>
<head><meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ /><meta http-equiv=”X-UA-Compatible” content=”IE=7″><title>場館中心</title><link href=”/Public/Book/css/login.css” charset=”utf-8″ type=”text/css” rel=”stylesheet” /><script src=”/Public/Book/js/jquery-1.7.2.js” type=”text/javascript” ></script><script type=”text/javascript”>
$(document).ready(function(){
//登陸驗證
$(“#logBtn”).click(function(){
var data = new Object();
$.ajax({
url: “/index.php/Book/Login/authCheck.html”,
data: data,
type:”post”,
success: function (text) {
if(text==”SUCCESS”){
location.href=”/index.php/Book/Book/index.html”;
}else{
alert(‘帳號或密碼輸入錯誤’);
}
},
});
});
$(“#confirmBtn”).click(function(){
location.href = “https://sso.tju.edu.cn/cas/login?service=”+encodeURI(location);
});
});
</script></head><body class=”p-body”><div class=”m-logbg” id=”m-logbg”></div><div class=”m-loginTab”><ul><li style=”text-align:left; padding-left:15%;”><img src=”/Public/Book/images/tjuLogo.png” /></li><li><img src=”/Public/Book/images/loginLogo.png” height=”200″ border=”0″ /></li><li><div id=”loginTab”><input type=”text” name=”name” id=”name” /> </body>
</html>
“`
雖然我們已經使用HttpClient請求成功一個簡單的網頁。但是,在實際中有很多網頁的請求需要附帶許多參數設置。在HttpClient 4.3及以上的版本中,這個過程主要包含如下步驟:
– 使用List<NameValuePair>添加請求參數
– 使用URI對請求路徑及其參數進行組合
– 使用List<Header>設置請求的頭部,header相當於設置構造的虛擬瀏覽器的各項屬性(可以理解為其更像真的瀏覽器)
– 初始化自定義的HttpClient客戶端,並設置header
– 使用HttpUriRequest設置請求
– 使用HttpClient請求上述步驟中的HttpUriRequest對象
這些步驟會在後面進行詳細演示,<font color=red>**看不懂也沒關係**</font>。
### 2.3 程式是如何得到網頁數據的?
2.1節我們獲取了一個示例頁面的HTML源程式碼,但是這些源碼是提供給瀏覽器解析用的,我們只對頁面上標題、時間、數量有用的資訊感興趣,至於亂七八糟的標籤之類的東西我們不要。自己寫程式解析也不是不可以,不過就是太費勁,這裡推薦一款Java的HTML解析器Jsoup,其主要的功能包括解析HTML頁面,通過屬性名或者id查找、提取數據等。**輪子別人已經造好了**,我們直接拿著用就可以,下面通過一個示例來說明下它的用法,假設我們有個HTML頁面的內容如下:
“`
<html>
<div id=”list”>
<div class=”title”>
<a href=”url1″>第一篇文章</a>
</div>
<div class=”title”>
<a href=”url2″>第二篇文章</a>
</div>
<div class=”title”>
<a href=”url3″>第三篇文章</a>
</div>
</div>
</html>
“`
使用Jsoup編寫規則對每個標題的鏈接進行解析,整個HTML程式碼中有一個id=list的父div,其包含3個class=title的子div,每個子div里各包含一個鏈接和一個標題,解析程式碼如下:
“`
Document doc = Jsoup.parse(rawHTML); //將當前頁面轉換成Jsoup的Document對象
Elements List = doc.select(“div[id=list]”).select(“div[class=title]”); //獲取所有的父id=list且其class=title的div標籤
for( Element element : List ){ //針對每個符合條件的元素進行解析,並輸出
String url = element.select(“a”).attr(“href”);
System.out.println(“Url:t”+url);
}
“`
輸出結果如下,關於Jsoup的簡介就是這些,具體的使用可以單獨去學習JSoup的規則,==這裡(作)不(者)再(太)講(懶)解==…
“`
Url: url1
Url: url2
Url: url3
“`
### 2.4 Chrome網頁調試工具有啥用?
為什麼要講Chrome網頁調試工具呢,一般不做網站開發的人基本用不著它,但是要做爬蟲就必須…什麼?老夫2000000行編碼經驗,豈會用到這種東西。你確定不用?不用!**嗯,真香…**
Chrome的網頁調試工具可以追蹤請求的調用過程和附帶的參數等資訊,通俗來講,就是可以讓我們知道我們訪問網頁的時候瀏覽器幹了啥,發出去了什麼東西,又接收到了什麼東西。比如網站登錄的時候通常採用POST的形式向伺服器發送請求,並將用戶名和密碼等參數一同發送過去進行驗證。這些參數我們在瀏覽器的鏈接欄里是看不到的,因此需要抓包工具獲取並分析。這裡我舉出了一個體育館預定平台登錄的抓包分析過程,作為示例演示。
首先使用瀏覽器訪問:http://cgzx.tju.edu.cn:8080/index.php/Book/Book/index 頁面,滑鼠右鍵選擇”檢查”,瀏覽器會打開調試介面,如下圖所示,勾選”Preserve log”,這樣頁面一旦跳轉,可以保存請求的日誌,之後輸入帳號和密碼,點擊”登錄”按鈕,
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/8FA08B1E1DBE4C00B31BDE651DA377FB/7127″ >
登錄成功後我們可以進入到歡迎頁面,這時候調試工具窗口發生了如下變化,紅圈表示的是剛剛登錄請求的鏈接和相關的參數資訊,如下圖所示,這裡包含的資訊到下一章會具體用到,**這裡給你們講了也不明白,索性不講了…**
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/8735DE3AE7D340F892D4AE111D10A766/7129″ >
## 三、場地預定流程分析
俗話說,<font color=red>**知己知彼,百戰不殆**</font>,在我們用寫程式碼搶場地之前,肯定要弄清楚整個過程的工作原理。首先,我們用瀏覽器去正常的訪問預定平台,這個過程對於使用過的同學來講都不陌生,我們可以繪製一個流程圖來說明該過程以及每一步的作用。如下圖所示,箭頭表示流程走向。
“`
graph TB
A[登錄系統]–>|1.攜帶帳號密碼|B
B[選擇羽毛球預約]–>|攜帶球種類參數|C
C[選擇北洋園校區]–>|攜帶校區參數|D
D[查看可供預定的時段]–>E
E[選擇時間段和日期]–>|攜帶日期和時間段參數|F
F[查詢並選擇可用場地]–>|攜帶場地編號參數|G
G[確認預約]–>|攜帶預約表單資訊|H
H{處理並返回結果}–>|成功/失敗|D
“`
每個步驟訪問的鏈接如下表所示:
步驟 | 請求類型 | 訪問鏈接
—|—|—
登錄 | POST | /Book/Login/authCheck.html
選擇羽毛球預約 | GET |/Book/Book/index1.html?cg=01
選擇校區 | GET | /Book/Book/index2.html?cg=01&cp=02
選擇時間段和日期 | GET | /Book/Book/index3?day=2018-01-01&time=00001&cg=01&cp=02
選擇場地 | GET | /Book/Book/index3?day=2018-01-01&time=00001&cg=01&cp=02&cdinfoid=2201
提交預定資訊 | POST | /Book/Book/order.htm
註銷 | POST | /Book/Login/logout
上述鏈接中的參數解釋:
1. cg代表羽毛球種類編號,即01
2. cp代表北洋園校區編號,即02
3. day代表日期,time代表預定時段,00001代表8:00-9:00場,因此類推,每過1個小時加1
4. cdinfoid代表場地,2201代表1號場,因此類推,每個場次加1
> 注意:1-4條解釋的是GET請求,而提交預定資訊採用的POST請求,包含參數較多,在後面會詳細解釋。
> POST和GET有啥區別?簡單來講,它們是web請求的兩種形式,GET的鏈接在瀏覽器地址欄里可以看到,POST的不讓你看,為啥不讓看?安全唄。
## 四、程式設計與實現
<font color=red>**拿破崙爺爺說過,不會寫程式碼的學生不是好學生**</font>,好了,貼程式碼…
### 4.1 為什麼要模擬登錄?
就前面兩章寫的httpclient使用,Jsoup解析要是直接拿來去定場地,還不夠用。為什麼呢?你會發現不管是查詢時間,還是預定場地,訪問第三章表裡的哪個鏈接都出不來數據,學到的這點兒三腳貓功夫還真不行,原因就在於體育館的預定系統需要登錄後才能進行訪問操作,不過這也難不倒作者<font color=red>**憤怒的內心**</font>,既然需要登錄授權,那我們就利用httpclient進行模擬登錄,具體實施在4.2節會詳細敘述。
### 4.2 登錄實現與cookie保存
這裡要感謝當時製作平台的相關人員,沒有加入驗證碼,也就為我們免去了驗證碼識別的麻煩,任務難度就低了一個等級。這裡先觀察下3.1節里提到的登錄頁面的源程式碼的JavaScript程式碼,如下所示,
“`
//登陸驗證
$(“#logBtn”).click(function(){
// alert(111);
var data = new Object();
data.name = $(“#name”)[0][‘value’];
data.pwd = $(“#password”)[0][‘value’];
$.ajax({
url: “/index.php/Book/Login/authCheck.html”,
data: data,
type:”post”,
success: function (text) {
if(text==”SUCCESS”){
location.href=”/index.php/Book/Book/index.html”;
}else{
alert(‘帳號或密碼輸入錯誤’);
}
},
error: function (jqXHR, textStatus, errorThrown) {
alert(jqXHR.responseText);
}
});
});
“`
可以發現登錄採用的是ajax非同步請求的方式,請求的路徑是”/index.php/Book/Login/authCheck.html”,請求方式是POST請求,封裝了用戶輸入的用戶名和密碼,如果登錄成功,返回”SUCCESS”字元串,頁面跳轉到”/index.php/Book/Book/index.html”,也就是主頁,不成功就彈窗提升。
> 什麼是ajax?就是頁面數據非同步刷新技術,通俗講就是頁面不跳轉,就能實現跟後台的數據交換。
不過單獨靠這些資訊是無法保證成功登錄的,那怎麼辦?當然是要祭出我們的<font color=red>**大殺器Chrome調試工具**</font>了,我們展開3.3節里控制台輸出的資訊,如下圖所示,請注意藍色方框里的數據,分別是request header資訊和攜帶的form表單數據,帳號和密碼做了==馬賽克處理==,所以是不可能給你們看的。
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/DCCE396FF59B413993DA592547AA8639/7354″ >
接下來,我們來編碼實現模擬登錄,主要步驟是構造一個httpclient對象,然後按照request header的屬性給它賦值,從而把這個<font color=red>**假的瀏覽器包裝的和真的一模一樣**</font>(能騙過預定系統的後台伺服器就行),之後封裝好用戶名和密碼兩個參數,發送登錄請求,實現登錄。程式碼如下:
“`
CloseableHttpClient client = HttpClientBuilder.create().build();//創建CloseableHttpClient對象
HttpPost post = new HttpPost(Repository.loginUrl); //採用post方式的發送請求
//包裝模擬瀏覽器請求頭
post.setHeader(HttpHeaders.ACCEPT, “*/*”);
post.setHeader(HttpHeaders.ACCEPT_ENCODING, “gzip, deflate”);
post.setHeader(HttpHeaders.ACCEPT_LANGUAGE, “zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6”);
post.setHeader(HttpHeaders.CONNECTION, “keep-alive”);
post.setHeader(HttpHeaders.CONTENT_TYPE, “application/x-www-form-urlencoded; charset=UTF-8”);
post.setHeader(HttpHeaders.HOST, “cgzx.tju.edu.cn:8080”);
post.setHeader(“Origin”, “http://cgzx.tju.edu.cn:8080”);
post.setHeader(HttpHeaders.REFERER, “http://cgzx.tju.edu.cn:8080/index.php/Book/Login/index.html”);
post.setHeader(HttpHeaders.USER_AGENT, “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36”);
post.setHeader(“X-Requested-With”, “XMLHttpRequest”);
//封裝請求參數
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(“name”,bean.getUserName()));
formparams.add(new BasicNameValuePair(“pwd”,bean.getPassword()));
HttpEntity reqEntity = new UrlEncodedFormEntity(formparams, “utf-8”);
post.setEntity(reqEntity);
post.setConfig(RequestConfig.custom().setConnectTimeout(5000).build());
//發送請求
HttpClientContext httpClientContext = HttpClientContext.create();
HttpResponse response = client.execute(post,httpClientContext);
//接受響應數據
if(response.getStatusLine().getStatusCode()==200){
HttpEntity resEntity=response.getEntity();
String message = EntityUtils.toString(resEntity, “utf-8”);
System.out.println(“log in “+message);
}else{
System.out.println(“log in error”);
}
“`
上述程式碼就可以實現模擬登錄了,登錄成功之後收到的message變數值為”SUCCESS”,要是你的密碼寫錯了,會返回”FAILED”,到這裡還結束,提到登錄往往關聯著session會話保持,如果不想每次都登錄,可以將session保存下來,客戶端叫做cookie的東西可以保存這些資訊。
> 什麼是session?session記錄著用戶的登錄狀態,比如登錄一次就可以不用再登錄,一般具有一個有效期,過期失效。
考慮到session具有有效期,作者編寫程式碼的時候每次預定前先登錄,預定完成後註銷,這樣**最為簡單,也最為安全,同時減少麻煩**。註銷程式碼如下:
“`
{
List<Header> headerList=new ArrayList<Header>();
headerList.add(new BasicHeader(HttpHeaders.ACCEPT, “text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8”));
headerList.add(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, “gzip, deflate”));
headerList.add(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, “zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6”));
headerList.add(new BasicHeader(HttpHeaders.CONNECTION, “keep-alive”));
headerList.add(new BasicHeader(“Cookie”, “PHPSESSID=”+getSessionId())); //需要傳入cookie里記錄的sessionId
headerList.add(new BasicHeader(HttpHeaders.HOST, “cgzx.tju.edu.cn:8080”));
headerList.add(new BasicHeader(HttpHeaders.REFERER, “http://cgzx.tju.edu.cn:8080/index.php/Book/Login/index.html”));
headerList.add(new BasicHeader(“Upgrade-Insecure-Requests”,”1″));
headerList.add(new BasicHeader(HttpHeaders.USER_AGENT, “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36”));
//構造自定義的HttpClient對象
HttpClient httpClient = HttpClients.custom().setDefaultHeaders(headerList).build();
URI uri = new URIBuilder(Repository.logoutUrl).build(); //此處需要填入註銷的請求鏈接
HttpUriRequest httpUriRequest = RequestBuilder.get().setUri(uri).build();
HttpResponse response = httpClient.execute(httpUriRequest); //執行註銷
…
}
“`
> 這裡要注意登錄要採用POST,之前寫程式的時候錯用成了GET,發現可以成功登錄,但是最後預定的時候每次都失敗。
### 4.3 監聽場地預約開放
這個地方==是很重要的一個地方==,直接影響到了預約的效率,訂到早了,系統沒有開放許可權,導致定不了;訂的完了,被別人搶走了,之前作者不懂事,寫了個定時器每天12:00:00準時開搶,發現一次都沒有搶到,以後是程式不行,後來發現,媽蛋<font color=red>**預約平台12:02分才放出預定資訊**</font>,於是寫了個監聽函數,以便在第一時間預約,程式碼就兩句,如下:
“`
{
while (!visit(Repository.testUrl).contains(day+”&”)){
Thread.sleep(500); //不能定就睡眠等待
}
}
“`
> 不要小看這兩行程式碼,很NB的哦…
### 4.4 尋找預約場地參數
登錄之後,就可以進行相關的場地檢索和預約了,這裡沒有必要一步一步按照第三章里的步驟訪問網頁,獲取源程式碼,解析參數再拼接往後執行,清楚了各項參數的代表含義和取值範圍,預定的事情就變得簡單了。大家都知道,<font color=red>**好鋼要用在刀刃上**</font>,寫程式的目的當時是要搶最熱門最難搶的場地了,so,我們直接把參數定好,時間段定為19:00-21:00,場地定為1-8號場地,再拼接上要預定的日期,就可以進入確認預定頁面了,程式碼如下:
“`
private void driver() throws IOException{
long curDateMills=System.currentTimeMillis();//獲取當前時間
String bookDate=DateFormats.getInstance().LongToDate(curDateMills+48*3600*1000).substring(0,10);//往後加2天
String bookTime[]=Repository.bookTime;//要預定的時間段
int bookPlace=Repository.bookPlace;//要預定的場地編號
instance.startBooking(bookDate,bookTime,bookPlace);//調用預定函數
}
“`
> 因為每天可以預定兩天後的場地,所以這裡加2天
工作到目前為止完成了70%,接下來是==最重要的地方!!!==
進入了預定確認頁面後,伺服器返回一個數據結構,包含預定者的姓名、hash驗證值、隊列號等,我們需要把這些數據解析出來,然後作為參數作為執行的最後的預定請求,具體的獲取方式可以通過抓包工具獲得,這裡只貼上對應的解析程式碼。
首先構造一個實體類,作為數據結構來封裝傳遞數據:
“`
public class BookFormBean {
private String hash;
private String CELL_PHONE;
private String REAL_NAME ;
private String CGINFO_ID;
private String CDINFO_ID;
private String CAMPUS_ID;
private String SEQ_NO;
private String PRICE;
private String DISCOUNT;
private String PRICE_FINAL;
…
}
“`
之後請求預定確認頁面的鏈接,採用Jsoup解析網頁源碼獲取表單數據:
“`
/**
* 解析表單數據
* return 封裝好的實體類
*/
private BookFormBean parseBean(String formStr){
Document document = Jsoup.parse(formStr);
String hash = document.select(“input[name=__hash__]”).attr(“value”);
String CELL_PHONE = document.select(“input[name=CELL_PHONE]”).attr(“value”);
String REAL_NAME = document.select(“input[name=REAL_NAME]”).attr(“value”);
String CGINFO_ID = document.select(“input[name=CGINFO_ID]”).attr(“value”);
String CDINFO_ID = document.select(“input[name=CDINFO_ID]”).attr(“value”);
String CAMPUS_ID = document.select(“input[name=CAMPUS_ID]”).attr(“value”);
String SEQ_NO = document.select(“input[name=SEQ_NO]”).attr(“value”);
String PRICE = document.select(“input[name=PRICE]”).attr(“value”);
String DISCOUNT = document.select(“input[name=DISCOUNT]”).attr(“value”);
String PRICE_FINAL = document.select(“input[name=PRICE_FINAL]”).attr(“value”);
BookFormBean bean=new BookFormBean(hash, CELL_PHONE, REAL_NAME, CGINFO_ID, CDINFO_ID, CAMPUS_ID, SEQ_NO, PRICE, DISCOUNT, PRICE_FINAL);
return bean;
}
“`
封裝表單數據,執行預定請求:
“`
{
Boolean result=false;
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
// 所傳參數
formparams.add(new BasicNameValuePair(“__hash__”, bean.getHash()));
formparams.add(new BasicNameValuePair(“CELL_PHONE”, bean.getCELL_PHONE()));
formparams.add(new BasicNameValuePair(“REAL_NAME”, bean.getREAL_NAME()));
formparams.add(new BasicNameValuePair(“CGINFO_ID”, bean.getCGINFO_ID()));
formparams.add(new BasicNameValuePair(“CDINFO_ID”, bean.getCDINFO_ID()));
formparams.add(new BasicNameValuePair(“CAMPUS_ID”, bean.getCAMPUS_ID()));
formparams.add(new BasicNameValuePair(“SEQ_NO”, bean.getSEQ_NO()));
formparams.add(new BasicNameValuePair(“PRICE”, bean.getPRICE()));
formparams.add(new BasicNameValuePair(“DISCOUNT”, bean.getDISCOUNT()));
formparams.add(new BasicNameValuePair(“PRICE_FINAL”, bean.getPRICE_FINAL()));
HttpEntity reqEntity = new UrlEncodedFormEntity(formparams, “utf-8”);
…
HttpResponse response=client.execute(post);
if (response.getStatusLine().getStatusCode()==200){
//HttpEntity resEntity = response.getEntity();
//EntityUtils.consume(reqEntity);
if(bean.getHash()==null||bean.getHash().length()==0){
System.out.println(formats.getNowDate1()+” 場館已被他人預定 “+confirmUrl);
result=false;
}
}else{
if(bean.getHash()!=null&&bean.getHash().length()>5){
System.out.println(formats.getNowDate1()+” 預定成功 “+confirmUrl);
result=true;
}
}
}
“`
### 4.5 失敗重訂機制
但是我們也不能保證預定每次都能成功,萬一你的網路卡一下,或者其它什麼原因,導致預定失敗,預定程式需要<font color=red>**有一定的糾錯能力**</font>,為此,我們設計了一套失敗重頂機制,來保證訂的場地已被預定,就選定其它場地,反正換來換去總能訂到的嘛,程式碼如下:
“`
{
for(int i=0;i<time.length;i++){
tryNum=0;
while(tryNum<8){
tempConfirmUrl=Repository.confirmUrl;
tempConfirmUrl=tempConfirmUrl.replace(“D”,day);
tempConfirmUrl=tempConfirmUrl.replace(“T”,time[i]);
tempConfirmUrl=tempConfirmUrl.replace(“P”,Integer.toString(bookPlace+tryNum));//失敗則嘗試換個場地
boolean result=book(Repository.bookUrl,tempConfirmUrl,getSessionId(),parseBean(visit(tempConfirmUrl)));
if(result==false){
tryNum++; //預定失敗,更新嘗試次數
}else{
successResult++; //預定成功,繼續預定下一個時間段的場地
break;
}
}
}
if(successResult>0){
sendEmail(successResult);//發郵件提醒用戶
}else{
//do nothing
}
logOut();
}
“`
### 4.6 郵件提醒功能
對於預定成功的用戶來講,總不能沒事還要登錄系統去看一下是否成功了吧,於是就加了一個預定成功的郵件提醒功能,採用java mail包就能實現,程式碼也不多:
“`
#配置文件 sys.properties
loginUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Login/authCheck.html
logoutUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Login/logout
indexUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Book/index
searchBookedUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/QueryBill/
testUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Book/index2.html?cg=01&cp=02
confirmUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Book/index4?day=D&time=T&cg=01&cp=02&cdinfoid=P
bookUrl=http://cgzx.tju.edu.cn:8080/index.php/Book/Book/order.html
userName=A,B,C
password=a,b,c
bookTime=00012,00013
bookPlace=2201
mailList=xxxxx@qq.com,aaaaa@qq.com,aaafaa@qq.com
“`
> 為啥要用好幾個帳號呢?因為每個帳號只能預定兩個場地,未過期之前都不可以再預定新的,所以就沒法了,只能多開幾個號了
“`
public void sendEMail(String title,String body) throws InterruptedException{
for(String item:Repository.mailList){
this.sendMessage(item, title, body);
}
}
private void sendMessage(String to_address,String title,String body) {
String smtphost = “smtp.163.com”; // 發送郵件的伺服器
String user = “xxxxx@163.com”; // 郵件伺服器登錄用戶名
String password = “xxxxxxxxxxxx”; // 郵件伺服器登錄密碼
String from = “xxxxx@163.com”; // 發送人郵件地址
Send(smtphost,user,password,from,to_address,title,body);
}
“`
## 五、效果評估
根據在伺服器上打包運行的一段時間的評測,發現運行狀態還不錯,下面是實際表現效果:
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/178F166481F9499791A359EC0004745A/7582″ >
郵件通知:
<img src=”https://note.youdao.com/yws/public/resource/d7452ee9e67fb35cc63a452f1117cd99/xmlnote/A7067CB51B684AA2A282068026EAD286/7583″ >
程式碼地址:
https://github.com/yananYangYSU/book
## 六、個人建議
為了減少像作者這樣的人出現,預定平台可以考慮以下改進措施:
1. 增添登錄驗證碼機制,提高腳本預約難度。
2. 預定流程過於簡單,建議引入加密機制,提高程式程式碼預約的複雜度。
3. 增加反制措施,定時篩查伺服器日誌,對於頻繁出現的IP或者帳號進行封號等措施。
4. 掃描訂而不去的用戶,超過一定閾值可以凍結帳號一段時間。
> 當然了,維護系統是要花錢的,作者的建議只是個人看法,可以忽略,哈哈哈哈
## 七、總結
<font color=red>**科學技術是把雙刃劍**</font>,我們使用的時候要以不影響別人正常使用為前提,比如把預定平台搞崩了,或者把場地都約完,不給別人留地方了,這些都是不好的行為,寫這篇文章也希望大家學習為主,多了解些知識和技術,同時也鍛煉下自己的語言組織和邏輯表達能力,嘻嘻…