前言

我家大儿之前苦于在成都大学开设的免费健身房抢不到预约名额,于是给他写了一个抢课的爬虫。最先是使用Python写的一个简易的爬虫程序,后来准备学Java之后决定把这个项目给搬运过来,当一个Java项目练练手。

开源项目链接:https://github.com/Savlgoodman/CDUgym

已发布可执行Jar包:https://github.com/Savlgoodman/CDUgym/releases

一.项目分析

1.分析我的Python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import requests
import json
import time
import os
import re
from datetime import datetime
import keyboard

DIRT_PATH = os.path.dirname(os.path.abspath(__file__))
# LOGPATH = DIRT_PATH + r"\log.txt"
CURSEPAGEPATH = DIRT_PATH + r"\cursepage.txt"
BOOKPAGEPATH = DIRT_PATH + r"\bookpage.txt"
COOKIEPATH = DIRT_PATH + r"\cookie.txt"

def main(a, b, c, d, e):
print("程序开始运行,按 Esc 键退出循环")
total_times = 0
url_date_pre = "https://www.styd.cn/m/e74abd6e/default/search?date="
url_date_next = "&shop_id=612773420&type=1"

# 函数区 开始 function start
# 获取课程号
def find_curse_id(date):
url1 = url_date_pre + date + url_date_next
date_data1 = "date=" + date + "&shop_id=612773420&type=1"
########
#中间省略一万行
########
print("无课或满了 正在监听中")
# code area end
time.sleep(inval_time) # 设置循环间隔
total_times += 1
if total_times >= MAX_TIMES:
running = False
except KeyboardInterrupt:
print("程序被手动中断")

# 函数区 结束 function end

# 程序入口
search_loop(a, b, c, d, e) # 日期 课程关键词 课程时间关键词 最大询问次数 循环间隔
print("程序已退出")

可以看Python写得是相当的屎山,如果需要看一下Python的源代码可以移步到我的下载中心去下载查看学习。

我的Python项目虽然只有一个py文件,但是其内部主要含有一些基本函数:

1.从目录的cookie.txt中获取用户cookie。(是的,需要用户手动输入)

2.定义了一个Curse_Info类,用于存储需要抢课的课程的信息。

3.由用户输入的指定日期中,循环访问该日期下的网页。该网页存在三种情况:①当日课程未发布;②当日课程已发布,目标课程未满;③当日课程已发布,目标课程已满。当出现①和③的情况的时候,需要继续循环等待目标课程的出现或者有人退课。

1

如图在这一天内发布了四个课程,我们需要的是晚上的健身中心的课程。

4.从课程目录界面获取目标课程的信息:课程的编号

1

如图可以找到在response中的course_link前的一个课程链接,里面有我们需要的课程id。当然,如果课程已满,此处的链接是javascript:alert(‘预约已爆棚,下次请赶早~’)

5.通过课程id,前往课程详情的页面获取预约所需要的元素:

我使用了正则表达式来搜索所需要的用户身份信息和课程信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def find_card_id(text):
meber_card_id = "114514"
card_cat_id = "114514"
curse_id = "114514"
Return_List = [
"114514",
"114514",
"114514",
] # member_card_id card_cat_id class_id
pre_str = r'card_id="'
end_str = r'"'
pattern = re.compile(f"{re.escape(pre_str)}(.*?){end_str}")
match = pattern.search(text)
if match:
meber_card_id = match.group(1) # 获取member_card_id
pre_str = r'cat_id="'
pattern = re.compile(f"{re.escape(pre_str)}(.*?){end_str}")
match = pattern.search(text)
if match:
card_cat_id = match.group(1) # 获取card_cat_id
pre_str = r'course_id" value="'
pattern = re.compile(f"{re.escape(pre_str)}(.*?){end_str}")
match = pattern.search(text)
if match:
curse_id = match.group(1) # 获取curse_id
Return_List[0] = meber_card_id
Return_List[1] = card_cat_id
Return_List[2] = curse_id
return Return_List

6.下单预约课程

通过实践发现下单操作是通过访问https://www.styd.cn/m/e74abd6e/course/order_confirm,并传输一个订单信息,其中包括课程id,学校id,会员卡id等信息,所以我通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def book_curse(class_id):
# https://www.styd.cn/m/e74abd6e/course/order_confirm
order_url = r"https://www.styd.cn/m/e74abd6e/course/order_confirm"
book_url_raw = r"https://www.styd.cn/m/e74abd6e/course/order?id="
book_url = book_url_raw + class_id
book_page_res = requests.get(book_url, cookies=cookies, headers=header)
with open(BOOKPAGEPATH, "w", encoding="utf-8") as f:
f.write(book_page_res.text)
id_list = find_card_id(book_page_res.text)
# 以下的class_id为预约课程的id, curse_id为课程(比如游泳还是健身之类的区分)的id
order_data = f"member_card_id={id_list[0]}&card_cat_id={id_list[1]}&course_id={id_list[2]}&class_id={class_id}&note=&time_from_stamp=0&time_to_stamp=0&quantity=1&is_waiting="
# 获取下单前数据完成 ↑↑
order_res = requests.get(
order_url, data=order_data, cookies=cookies, headers=header
)
order_res_text_json = json.loads(order_res.text)
print(order_res_text_json["msg"])
print("order success")

这个函数来实现了下单请求

2.构建Java代码项目结构

我的项目结构如下:

1

由于苦于不想面对黑框框,这里简单的用JavaSwing写了一个简单的窗体。主要分为了两个窗体,一个是登录界面,登录之后就可以进入预约面板。整个项目实现了UI与功能分离,主要功能的实现通Main包内的MainAPI中的函数进行调用,不过这里只实现了单次访问网页,查找是否有剩余课程,如果有就预约课程。为了方便前后通讯,MainAPI内函数的返回值为预约情况,循环部分写到了BookFrame中,让其中的文本框更好的实现日志输出。

3.实现类与对象的思想

由于Java没有方便的requests库,我便随手写了一个我自己的”request”类:

在这个类中,需要先new一个request对象,然后对这个对象实现一些访问的方法,这个有三种构建方法:url,header,cookie,method方法;带数据的构造方法:url,header,cookie,method,data;单url构造方法:获取用户头像时使用。

这里举一个带data发包的方法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String requestWithData() throws IOException{
URL url1 = new URL(url);
HttpURLConnection conn = (HttpURLConnection) url1.openConnection();
conn.setRequestMethod(Method);
conn.setRequestProperty("User-Agent", UserAgent);
conn.setRequestProperty("Cookie",Cookie);
conn.setDoOutput(true);
conn.getOutputStream().write(data.getBytes());
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
}

如此便简单的获取了url 的response。

在获取课程信息的方法里面,同样使用了正则表达式来查找各个课程的状态和id,然后进入目标课程检测,当检测到目标课程有空位时,便构造BookCurse类,传入Curse_id来进行访问课程详情页获取下单所需数据。

二.项目简要

1.主界面截图

1

2.登录后截图

1

3.抢课界面

1

4.抢课测试

1

三.项目总结

随便水了一篇文章,具体没怎么细讲我的项目,但是代码很简单都看得懂,现在在攻克手机号登录获取cookie的问题,并添加更多的功能。

TODO:

1.添加用户选择界面,轻松切换不同的用户

2.添加手机号登录

3.试图将抢课程序挂载到服务器中,无需开启电脑,随时随地查看抢课状态