华科羽毛球自动抢订代码解析
本文最后更新于:4 个月前
前言
之前学校光体维修,只剩下西体8个场地,以往的现场预定又改成了网上预订。早上八点开始可以预定后两天的场地……
羽毛球爱好者太多了,加上8点就起来抢,太困了……
所以写了一个python程序用来抢订场地。其实思路也很简单,使用windows自带的计划任务定时:如指定日期早上7点56开始运行。用selenium模拟浏览器进入系统执行场地的抢订……
后来,光体开了,网站也进行了升级,进入系统登录的时候多了个验证码,验证码是动的。就像下面的图一样:
羽毛球场地虽然多了,但是紧俏的场地依然不好定,如晚上的8-10点,但是课题组的人貌似下午也接受,下午的场地好像订的人不多。就没有再折腾了。
贴一下两年前写的代码,当时还不需要验证,所以一切都是那么简单:
# -*- coding: utf-8 -*-
# Author:tomorrow505
# 用于定时启动
import time
import datetime
from time import sleep
import threading
import sys
import json
from selenium import webdriver # 导入浏览器
from selenium.webdriver.support.ui import Select
from selenium.common.exceptions import NoSuchElementException
# 用于最后判定是否预约成功,如果不成功获取错误消息写入日志
import traceback
# 用于关闭谷歌浏览器,如果开着的话
import psutil
import os
# 用于干掉开着的火狐浏览器
def kill_chrome():
exe = 'firefox.exe'
pids = psutil.pids()
for pid in pids:
p = psutil.Process(pid)
# print('pid-%s,pname-%s' % (pid, p.name()))
if p.name() == exe:
# print('pid-%s,pname-%s' % (pid, p.name()))
cmd = 'taskkill /F /IM firefox.exe'
os.system(cmd)
break
def load_info()
login_info_ = {}
try:
with open('login_user.json', 'r') as login_file:
login_info_ = json.load(login_file)
except BaseException:
print("Json load failed")
finally:
return login_info_
def order_play():
login_info = load_info()
# 优先时间顺序——对应selector
time_order = login_info['order']
time_order = [item + 3 for item in time_order]
# 时间的字符串,用于发邮件
time_str = {3:"20:00-22:00", 1:"16:00-18:00", 2:"18:00-20:00"}
# 场地优先顺序
order = [7, 8, 9, 10, 12, 13, 14]
# 存储场地预定信息的xpath,用于寻找是否可以预定
changdi_xpath = []
# 添加第7个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[4]/td[3]/div/div/span')
# 添加第8个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[4]/td[4]/div/div/span')
# 添加第9个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[4]/td[5]/div/div/span')
# 添加第10个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[4]/td[6]/div/div/span')
# 添加第12个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[5]/td[3]/div/div/span')
# 添加第13个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[5]/td[4]/div/div/span')
# 添加第14个场地
changdi_xpath.append(
'/html/body/div[2]/div[2]/div[2]/form/div[1]/table/tbody/tr[5]/td[5]/div/div/span')
# 查看是否存在可以预定场地
flag = 0
driver_logo = 0
driver = get_driver(driver_logo)
with open("outputlog.txt", "w+") as f:
# sys.stdout = f # 输出指向txt文件
write_log('开始记录日志')
driver.get(
"https://pass.hust.edu.cn/cas/login?service=http%3A%2F%2Fpecg.hust.edu.cn%2Fcggl%2Findex1")
driver.implicitly_wait(5)
write_log('进入统一身份认证系统')
# 清空两个框里的东西
driver.find_element_by_id('un').clear()
driver.find_element_by_id('pd').clear()
# 填写账号密码:谷歌浏览器就不用
driver.find_element_by_id(
'un').send_keys(login_info['no'])
driver.find_element_by_id(
'pd').send_keys(login_info['pwd'])
# 点击登录统一认证
driver.find_element_by_xpath('//*[@id="index_login_btn"]').click()
write_log('成功进入预定界面')
# 进入场地预约界面
driver.find_element_by_xpath("//*[@id='main']/ul/li[2]/a").click()
write_log('进入场地预约界面')
# 规定8点开始预定
write_log("检查是否到达可预订时间……")
while True:
now = datetime.datetime.now()
if (now.hour >= 7 and now.minute >= 40 and now.second >= 0) or now.hour >= 8:
break
# 每隔1秒检查一次
write_log("等待系统开启")
time.sleep(1)
write_log("到达可预约时间点")
while True:
# 选择光体
driver.find_element_by_xpath(
"/html/body/div[2]/div/ul/li[1]/div[1]/div[2]/span/a").click()
try:
element = driver.find_element_by_xpath(
"/html/body/div[2]/div[2]/div[2]/div[1]/div[3]")
break
except NoSuchElementException:
write_log("预约系统显示:8:00到24:00才能开始预定,请等待")
driver.refresh()
write_log("刷新中……")
write_log("成功进入光体预约界面")
# 延迟两天,总是订后天的场地
element.click()
driver.find_element_by_xpath(
"/html/body/div[2]/div[2]/div[2]/div[1]/div[3]").click()
count = 5
while count >= 0:
count = count - 1
for ii in range(len(time_order)):
write_log("以下是%s的场地预定信息:" % time_str[time_order[ii]-3])
# 1.获取时间Select对象
selector = Select(driver.find_element_by_id("starttime"))
# 2.选择对应的时间
selector.select_by_index(time_order[ii])
partner = ''
t = threading.Thread(target=input_info, args=(partner, driver))
t.start()
for i in range(22):
info = ''
while True:
info = driver.find_element_by_xpath(changdi_xpath[i]).text
if not info == '':
break
write_log("第%d个场地%s------" % (order[i], info))
# 找到可以预约的场地了
if info == "可预约":
# 点击可以预约的场地,变换标志为1代表找到场地
t.join()
driver.find_element_by_xpath(changdi_xpath[i]).click()
flag = 1
write_log("找到可预约的场地")
break
if flag == 1:
break
# 有场地
if flag == 1:
# 5、点击预约按钮
driver.find_element_by_xpath(
"/html/body/div[2]/div[2]/div[2]/form/div[3]/input[3]").click()
write_log("正在进行预约……")
# 6、进入预约界面点击确定,有钱就扣钱成功了。
try:
# driver.find_element_by_xpath(
# "/html/body/div[2]/div[2]/form/div/div[3]/input[2]").click()
# write_log("正在扣费,等待系统响应……")
# 根据返还回来的消息判断是否成功预定。
sleep(2)
try:
order_info = driver.find_element_by_xpath(
"/html/body/div[2]/div[2]/form/div/div[1]/table/tbody/tr/td[2]").text
judge_str = "您已预约成功"
if order_info.find(judge_str) >= 0:
write_log("恭喜预约成功!")
break
else:
write_log("预约失败,账号里是不是没有钱?")
break
except Exception as e:
write_log("没找到预约成功消息。错误消息提示如下:")
exstr = traceback.format_exc()
write_log(exstr)
break
except:
continue
else:
write_log("没有找到可以预定的场地")
break
sleep(5) # 睡10秒,等待下载完成,这个可以改一下
driver.quit() # 关闭浏览器
def write_log(str):
now = datetime.datetime.now()
print('%s----%s' % (now, str))
def input_info(partner, driver):
driver.find_element_by_xpath('/html/body/div[2]/div[2]/div[2]/form/table/tbody/tr[4]/td/input').click()
driver.find_element_by_xpath('/html/body/div[3]/div[2]/div/div[2]/table/tbody/tr[2]/td[3]').click()
def get_driver(driver_num):
options = webdriver.FirefoxOptions()
options.add_argument('--disable-gpu') # 这里是禁用GPU加速
driver = webdriver.Firefox(options=options)
return driver
if __name__ == "__main__":
order_play()
command = input('输入任意键退出>>')
插曲
曾经给PT的小伙伴介绍过这个程序,结果他也是喜欢打羽毛球也需要给实验室定场地的,直接说了一声卧槽。。。。然后他也写了一个GUI版本的,哈哈~~
再次折腾
时至今日,好像羽毛球场地又不够用了,大概是因为军训篮球场地被征用,西体也不开。昨天定了一个下午场,课题组的人问能不能抢晚上的场地。所以抱着试试的心态就开始了。
首先需要了解流程,还是使用selenium,还是使用windows计划,但是验证码怎么破解呢?
查看源码,验证码对应的是一个链接,每刷新一次就会生成一个动态code:https://pass.hust.edu.cn/cas/code。
所以不能解析地址来下载图片,那么直接截图呢?也不太行,动图是动的,经常一帧下来有的图没有,这样需要截好几张图。那么怎么把动图下载呢?
暴力直接:右键另存为,下边是部分代码。
from selenium.webdriver.common.action_chains import ActionChains
import pyautogui
import pyperclip
# 用xpath定位到图片并且移动鼠标到图片上
pic = driver.find_element_by_xpath('//*[@id="codeImage"]')
action = ActionChains(driver).move_to_element(pic)
# 右键点击该元素,并敲击v进行保存
action.context_click(pic)
action.perform()
pyautogui.typewrite(['v']) # 敲击V进行保存
time.sleep(1)
# 为了保存到指定目录,我们先复制目录,然后粘贴到保存窗口
pyperclip.copy(code_path)
pyautogui.hotkey('ctrlleft', 'v') # 粘贴
# 有时候回车不太好使,就写一个循环不停回车~当图片下载下来之后,跳出
while True:
pyautogui.press('enter')
if os.path.exists(code_path):
break
sleep(1)
图形处理
下载之后,怎么解析呢?比如上面那张图,方法有很多,但是基本上都会是分而治之的方案。废话不多说,上代码:
def _get_img_from_gif(self):
im = Image.open(self.origin_path)
try:
while True:
current = im.tell()
im.save(self.tmp_img_paths[current])
#获取下一帧图片
im.seek(current+1)
except EOFError:
pass
逐帧另存之后可以发现是四张图片,那么我们需要将这4张图片再度合成一张图,观察到他们每个图片数字对应的位置不会变,只是颜色差异,那么就可以两两比较,选择颜色最深的作为合成图的对应点的颜色。
# 图片大小一致,且是单通道图,针对每一个像素,取像素值小的(颜色越深越小)
# 并且根据经验添加一个阈值,180默认为白的,小于180的就是黑的,用于锐化
def _merge_two_imgs_to_one(self, path1, path2):
img1 = Image.open(path1)
img2 = Image.open(path2)
# 单幅图像尺寸
width, height = img1.size
img_new = Image.new('L', (width, height))
for x in range(width):
for y in range(height):
px1 = img1.getpixel((x, y))
px2 = img2.getpixel((x, y))
r = min(px1, px2)
if r > 180:
r = 255
else:
r = 0
img_new.putpixel((x, y), r)
img_new.save(self.new_img_path)
# 多张合成的思路是两两比较,写一个循环
def _merge_multi_pictures(self):
for i in range(3):
if i == 0:
self._merge_two_imgs_to_one(self.tmp_img_paths[i], self.tmp_img_paths[i+1])
else:
self._merge_two_imgs_to_one(self.new_img_path, self.tmp_img_paths[i+1])
这样就把数字给实化了,全黑,有些干扰的圈圈也不是很要紧,接下来就是把这个图再度拆分,然后分别识别为对应数字。经过测试,该图片为90x58的图,可以拆成4个20x22的子图。
def _split_2_four_part(self):
img = Image.open(self.new_img_path)
for i in range(4):
new_img = Image.new('L', (20, 22))
for x in range(20):
for y in range(22):
new_img.putpixel((x,y), img.getpixel((x+20*i, 18+y)))
new_img.save(self.number_path[i])
这样就可以分成四个子图分别对应预测了。由于策略关系,第一个子图比较靠左,第四个子图比较靠右,基本上有两种思路,再度调整或者分组进行匹配。这里我选择了第二种方案。对于每一个位置截取了一组0-9的数字,比如第一个子图对应的0-9和第四个子图的0-9分开存储,分别对比。
可以发现形态还是有差异的~
接下来就是第一个子图如果生成了一个图,怎么计算它是0-9的具体哪一个数呢?
图形相似度
图形相似度算法很多,这里用的是最简单的dhash算法。
def _dHash(self, img_path):
hash_str = ''
img = Image.open(img_path)
width, height = img.size
for i in range(width-1):
for j in range(height):
if img.getpixel((i, j)) > img.getpixel((i+1, j)):
hash_str += '1'
else:
hash_str += '0'
return hash_str
def _cmpHash(self, hash1, hash2):
n = 0
if len(hash1) != len(hash2):
return -1
# 遍历判断
for i in range(len(hash1)):
# 不相等则n计数+1,n最终为相似度
if hash1[i] != hash2[i]:
n = n + 1
return n
这样遍历就可以预测出来了~~
def _predict_code(self):
predict_number = ''
for i in range(4):
predict_code = []
hash1 = self._dHash(self.number_path[i])
compare_path = os.path.join(self.img_dir, str(i+1))
files = os.listdir(compare_path)
for index, file in enumerate(files):
hash2 = self._dHash(os.path.join(compare_path, file))
cmp_hash = self._cmpHash(hash1, hash2)
predict_code.append(cmp_hash)
predict_number += str(predict_code.index(min(predict_code)))
return predict_number
def run(self):
self._get_img_from_gif()
self._merge_multi_pictures()
self._split_2_four_part()
print(self._predict_code())
最后输出3515,成功了。
代码整合
然后就是代码整合一下,在登录页面整合右键下载图片,逐帧转换,锐化合成,拆分子图,相似度对比预测验证码。然后就可以填写进行登录以及后续操作了。
注意,由于脚本使用到了鼠标操作,所以在运行的时候千万不要锁屏,不要锁屏!!!(错失好几次机会了)
总结
这个例子比较简单的方面是图片的数字位置固定不变,不需要大量的机器学习去预测,只需要计算相似度就可以得到很好地结果,当然了,作为一个抢订羽毛球的小程序,就不扯那么高大上的东西了。