[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,搜狗,谷歌),但是程序要想去访问网站,当然需要模拟出来一个浏览器(呃...其实浏览器本身也是程序写的),设计思路就是通过计算机程序来**模拟出一个用户和浏览器,去替代人工进行场地预定**。
话还没说完,出自于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>,我们使用的时候要以不影响别人正常使用为前提,比如把预定平台搞崩了,或者把场地都约完,不给别人留地方了,这些都是不好的行为,写这篇文章也希望大家学习为主,多了解些知识和技术,同时也锻炼下自己的语言组织和逻辑表达能力,嘻嘻...