学校图书馆换了新的验证码预约系统,用户每天22:45准时完成滑块验证才能抢到次日座位,整个流程包括登录系统→选择区域→对准滑块缺口→卡点提交,任何一个环节延迟都可能前功尽弃,可谓手残党克星。当我第三次因为手抖把滑块滑偏,看着近10秒的验证耗时,意识到必须想办法自救。从灵光乍现到方案成型两杯咖啡的时间,以下是一点实战心得。

🥳 最终实现效果

Before: 手动模式
"bookDate": "2025-xx-xx 22:45:04",
"bookDate": "2025-xx-xx 22:45:07",
"bookDate": "2025-xx-xx 22:45:05"

After: Tampermonkey 脚本
"bookDate": "2025-xx-xx 22:45:00",

目前方案虽不算完美(仍需15秒内完成手动验证),但实测将操作误差从±7秒压缩到毫秒级,已经能保证稳定抢到目标座位。就像给老式汽车加装定速巡航——虽不是全自动驾驶,但至少不用再脚踩油门与时间赛跑~

The animation of the script in action
插件操作示意

1️⃣ 原始手动模式

滑动滑块到缺口位置,然后在22:45:00迅速松开。最简单直观的方法。但这个方法最大的敌人是薛定谔的验证时间:你以为4秒能搞定,实际可能花7秒。最崩溃的一次尝试:22:44:59松手,“未到预约开放时间”;22:45:00松手,“验证码被黑洞吸走了,请重试”。可能遇到的情况:22:45:03重试成功,心仪座位已被约。

虽然实际因为运气好,暂时还没遇到没抢到座位的情况,但通常目标座位区域在15秒内就已经全部被抢光,所以这个方法的容错率很低。在“可能抢不到”的焦虑催生下,有了后续的方案。

2️⃣ Proxyman 断点:延迟执行冻结请求

由于预约请求 /bookseat 是在验证码校验 /check 成功后立即POST,那么如果能控制/bookseat请求发送的精确时间,就能绕过不确定的人类操作延迟。选择Proxyman是因为它的 Breakpoint 功能可以冻结请求,就像给网络请求打了断点,可以手动延迟执行请求。

设置:在 Proxyman Tools > Breakpoints 中设置断点规则

Matching Rule (Request):   https://xxxx.xxx.xxx.edu.cn/*/bookseat/*

预约步骤:

  1. 确保其他 Proxy 工具已关闭(必须)。
  2. 打开已启用 SSL proxying 的浏览器(例如 Chrome),并选择座位。
  3. 点击“确认”按钮,调起滑块验证,并在 22:44:45 秒后通过验证,自动唤起Proxyman 断点窗口。
  4. 22:45:00 秒,在断点窗口 Excute Request。

这样,在 Proxyman 中设置 Breakpoint Rule 后,当请求匹配到指定的 URL时,会自动暂停请求并在断点窗口中显示。用户可以在断点窗口中手动延迟请求的执行时间,以便在指定的时间点(22:45:00 秒)执行 Request,完成抢座位的操作。

实测发现关键点在于:

  1. 系统的timeout设置约15秒,所以最多提前15秒完成滑块验证
  2. 本地时间应该要与服务器严格同步(这个误差目前可以忽略)
  3. Proxyman的Proxy和电脑其他Proxies不能兼容,setup起来比较麻烦

3️⃣ Proxyman 脚本:15秒容错窗口

但是,每次都盯着时钟点"Execute"请求还是有点麻烦。于是,我想到用 proxyman scripting 直接让程序自动处理。

设置:在 Proxyman > Scripting 中设置下面的脚本
// # Name:                      捕获并延迟到指定时间请求
// # URL (Request):             https://xxxx.xxx.xxx.edu.cn/*/bookseat/*
// 图书馆预约开放时间
const TARGET_HOUR = 22;    // 小时(24小时制)
const TARGET_MIN = 45;      // 分钟
const TARGET_SEC = 00;      // 秒

function onRequest(context, url, request) {

    // 计算目标时间
    const now = new Date();
    const targetTime = new Date(
        now.getFullYear(),
        now.getMonth(),
        now.getDate(),
        TARGET_HOUR,
        TARGET_MIN,
        TARGET_SEC
    );
    
    // 计算延迟时间(毫秒)
    const delay = targetTime - now;
    
    // 时间窗口检查(0-15秒)
    if (delay >= 0 && delay <= 15000) {
        console.log(`延迟请求: ${delay}ms`);
        sleep(delay);
        return request;
    } else {
        console.log("放弃请求:超出时间窗口");
        return request;
    }
}

预约步骤:

  1. 确保其他 Proxy 工具已关闭(必须)。
  2. 打开已启用 SSL proxying 的浏览器(例如 Chrome),并选择座位。
  3. 调起滑块验证,并在 22:44:45 秒后通过验证,自动唤起Proxyman 断点窗口。

这样,在 Proxyman 中设置 Script 后,当请求匹配到指定的 URL,脚本会延迟到22:45:00准时执行请求,从而提高抢座位的成功率。

4️⃣✅ 完全基于浏览器方案:Javascript Bookmarklet

那么,既然能直接拦截浏览器XHR请求,为什么不省去设置外部代理工具的环境,完全基于浏览器脚本实现?可以在 Chrome 的 Tampermonkey Extension (仅支持 PC 端,IOS 端的 Chrome 暂时不支持 Extension) ,或在 Safari 添加 Javascript Bookmarklet书签 (支持 Mac 和 IOS端) 运行下面的 script。

其中,Bookmarklet 将脚本压缩为单行 URI 的形式(以 javascript:(); 嵌套脚本内容),保存为 Safari 书签后,需要在目标页面加载后手动点击书签执行脚本。

Script
// ==UserScript==
// @name         学校图书馆预约抢座(22:44:45~45:00完成滑块验证)
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  捕获并延迟POST请求到指定时间
// @author       Tiana Lei
// @match        https://xxxx.xxx.xxx.edu.cn/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const TARGET_HOUR = 22;  // 小时(24小时制)
    const TARGET_MIN = 45;   // 分钟
    const TARGET_SEC = 0;    // 秒

    var originalOpen = XMLHttpRequest.prototype.open;
    var originalSend = XMLHttpRequest.prototype.send;

    // 正则表达式,用于匹配URL
    const targetPattern = /^https:\/\/xxxx\.xxx\.xxx\.edu\.cn\/.*bookseat\/.*/;

    XMLHttpRequest.prototype.open = function(method, url) {

        if (method === "POST" && targetPattern.test(url)) {
            var that = this;
            this.realSend = this.send; // 保存原始send方法

            this.send = function(data) {
                // 获取当前时间和目标时间
                const now = new Date();
                const targetTime = new Date(
                    now.getFullYear(),
                    now.getMonth(),
                    now.getDate(),
                    TARGET_HOUR,
                    TARGET_MIN,
                    TARGET_SEC
                );

                // 计算延迟时间(毫秒)
                const delay = targetTime - now;
                console.log(`当前时间:${now}, 目标时间:${targetTime}, 计算得到延迟:${delay}ms`);  // 输出延迟时间

                // 延迟发送请求
                if (delay > 0 && delay <= 15000) { // 如果在时间窗口内
                    console.log(`延迟发送:${delay}ms`);  // 输出延迟信息
                    setTimeout(function() {
                        console.log('延迟请求被发送');  // 延迟后执行的日志
                        that.realSend(data); // 延迟发送原始请求
                    }, delay);
                } else {
                    console.log('超出时间窗口,直接发送请求');  // 如果超出时间窗口,直接发送请求
                    that.realSend(data); // 超出时间窗口,直接发送
                }
            };
        } else {
            console.log(`跳过,非目标POST请求,URL: ${url}`);  // 如果不是目标请求,跳过
        }
        originalOpen.apply(this, arguments); // 调用原始open方法
    };

    XMLHttpRequest.prototype.send = function(data) {
        originalSend.apply(this, arguments);  // 调用原始send方法
    };
})()

现在的最佳操作流程变成:

  1. 22:44:30 优雅地打开页面
  2. 22:44:45~22:44:59 从容完成滑块验证(预留15秒容错)
  3. 浏览器脚本在00秒准时发送请求

至此,抢座焦虑已成过去式。Enjoy ~