华科羽毛球自动抢订代码解析

本文最后更新于:几秒前

前言


之前学校光体维修,只剩下西体8个场地,以往的现场预定又改成了网上预订。早上八点开始可以预定后两天的场地……

羽毛球爱好者太多了,加上8点就起来抢,太困了……

所以写了一个python程序用来抢订场地。其实思路也很简单,使用windows自带的计划任务定时:如指定日期早上7点56开始运行。用selenium模拟浏览器进入系统执行场地的抢订……

后来,光体开了,网站也进行了升级,进入系统登录的时候多了个验证码,验证码是动的。就像下面的图一样:

code28e4a9aaa555b38e.gif

羽毛球场地虽然多了,但是紧俏的场地依然不好定,如晚上的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

01.png

逐帧另存之后可以发现是四张图片,那么我们需要将这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])

new.png

这样就把数字给实化了,全黑,有些干扰的圈圈也不是很要紧,接下来就是把这个图再度拆分,然后分别识别为对应数字。经过测试,该图片为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])

02.png

这样就可以分成四个子图分别对应预测了。由于策略关系,第一个子图比较靠左,第四个子图比较靠右,基本上有两种思路,再度调整或者分组进行匹配。这里我选择了第二种方案。对于每一个位置截取了一组0-9的数字,比如第一个子图对应的0-9和第四个子图的0-9分开存储,分别对比。

03.png04.png

可以发现形态还是有差异的~

接下来就是第一个子图如果生成了一个图,怎么计算它是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())

05.png

最后输出3515,成功了。

代码整合


然后就是代码整合一下,在登录页面整合右键下载图片,逐帧转换,锐化合成,拆分子图,相似度对比预测验证码。然后就可以填写进行登录以及后续操作了。

总结


这个例子比较简单的方面是图片的数字位置固定不变,不需要大量的机器学习去预测,只需要计算相似度就可以得到很好地结果,当然了,作为一个抢订羽毛球的小程序,就不扯那么高大上的东西了。

参考链接



本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!