拆解)
簡介面向北京郵電大學(xué)本科生的自動搶課工具基于JavaScript實現(xiàn)針對教務(wù)系統(tǒng)登錄后的選課流程設(shè)計無需進(jìn)入選課界面即可按課程名自動匹配并搶課覆蓋必修、選修與公選課。壓縮包共2個文件包含1個JavaScript腳本和1個Markdown說明文檔整體僅2KB輕量簡潔。腳本支持通過courses變量自定義目標(biāo)課程通過interval調(diào)整輪詢間隔搶課模式可設(shè)50ms撿漏模式建議300ms兼顧不同場景下的成功率與服務(wù)器壓力。已有2579人學(xué)習(xí)下載適合熟悉基本瀏覽器調(diào)試操作、希望提升選課效率的在校學(xué)生。除核心腳本外說明文檔還梳理了啟動時機(jī)、異常停止、與手動搶課配合等實用經(jīng)驗同時提醒勿設(shè)置過低間隔、勿惡意搶課引導(dǎo)合理使用。 每年選課季北郵的本科生群都會哀嚎一片。熱門通識課、體育課開選即滿刷新、提交、失敗、再刷新這套循環(huán)能磨掉人半條命。我寫了個叫 BUPTtakeCourse 的小工具輸入課程名它自動完成登錄、檢索、提交選課請求到點自動開搶。這篇博文把我做這個搶課腳本的完整思路、技術(shù)拆解和踩坑記錄整理出來希望能幫到同樣有選課需求的學(xué)弟學(xué)妹也適合想了解校園自動化腳本怎么寫的人參考。1. 項目背景與核心需求拆解1.1 選課季到底難在哪北郵的選課系統(tǒng)在高峰期頁面卡頓、接口超時是常態(tài)。名額有限的課程比如熱門公選課、體育專項課往往開放選課的第一分鐘就被秒空。手動操作的流程看起來簡單登錄、搜索課程、點選課、確認(rèn)提交。但真正經(jīng)歷過的人都知道問題出在高頻和快速響應(yīng)的矛盾上——你還在等頁面刷新別人已經(jīng)提交完了。搶課的本質(zhì)其實是把“查詢余量”和“提交選課”這兩個動作反復(fù)執(zhí)行直到成功。這個場景天然適合腳本化流程固定、頻次高、對響應(yīng)速度要求苛刻。人工操作有兩大痛點一是手速跟不上節(jié)奏二是老得盯屏幕一盯就是半小時起步。腳本的價值不是作弊而是把這些重復(fù)勞動交給程序讓它在后臺持續(xù)監(jiān)控一旦發(fā)現(xiàn)余量就第一時間提交。1.2 “按課程名自動選課”的真實需求這個項目最關(guān)鍵的設(shè)計點名字里已經(jīng)寫清楚了根據(jù)課程名自動選課。很多人會問為什么不直接用課程代碼因為對普通學(xué)生來說課程代碼記不住但“電路分析基礎(chǔ)”“羽毛球初級”這類課程名是一眼就能認(rèn)出來的。用課程名做主輸入需要額外解決兩個問題。第一是課程名可能重復(fù)不同老師開課、不同時段開課課程名一樣第二是系統(tǒng)里的課程名和選課界面顯示的名稱可能不完全一致比如多了“A班”這種后綴。所以腳本的核心邏輯不是簡單地拿字符串去比較而是要有一套匹配策略把課程名、上課時間、任課教師等信息組合起來先識別、再確認(rèn)最后才發(fā)起選課。1.3 腳本的能力邊界需要先把定位說清楚BUPTtakeCourse 不是漏洞利用工具也不是繞過認(rèn)證的違規(guī)程序。它的前提是學(xué)校選課系統(tǒng)本身允許你選這門課你有選這門課的身份資格只是名額緊張、需要拼手速。腳本做的事情和人工搶課時一直在點“提交”按鈕沒有本質(zhì)區(qū)別只是把它變成了自動化的高頻檢測一旦余量出現(xiàn)就立刻提交。這個邊界很重要。我在設(shè)計腳本時就定了幾個原則不碰驗證碼破解、不嘗試?yán)@過登錄認(rèn)證、不濫用高頻請求輪詢頻率控制在合理范圍內(nèi)避免對教務(wù)系統(tǒng)造成額外壓力。腳本只是把你的個人操作變快而不是破壞規(guī)則。2. 整體方案設(shè)計與技術(shù)選型2.1 自動化方案的選型邏輯做這類校園自動化腳本有兩套主流方案模擬 HTTP 請求和瀏覽器自動化。模擬請求就是用 requests 庫直接構(gòu)造登錄、查詢、選課的請求優(yōu)點是輕量、快、資源占用小能在極小延遲內(nèi)完成整套操作缺點是必須先分析出系統(tǒng)的接口規(guī)律還要處理登錄態(tài)、參數(shù)加密、可能的驗證碼。瀏覽器自動化比如 Selenium 或 Playwright則是啟動一個真實瀏覽器模擬人的點擊行為。它的優(yōu)點是幾乎不用逆向接口頁面怎么操作代碼就怎么寫缺點是速度慢每次啟動瀏覽器都要幾秒鐘內(nèi)存開銷也大在搶課這種拼毫秒的場景下反而吃虧。BUPTtakeCourse 選用的是以請求模擬為主、瀏覽器自動化兜底的混合策略。主流程全部走 HTTP 請求這樣速度快遇到個別必須要驗證碼或復(fù)雜前端邏輯的場景再臨時喚起瀏覽器人工介入處理一次拿到登錄態(tài)后繼續(xù)走請求流程。2.2 技術(shù)棧與依賴選擇整個項目基于 Python 3依賴庫非常克制核心就幾個requests負(fù)責(zé)所有 HTTP 請求網(wǎng)絡(luò)交互的基礎(chǔ)PyYAML讀取配置文件把課程名、輪詢間隔、賬號信息等參數(shù)外置BeautifulSoup4 lxml解析選課頁面返回的 HTML 表格logging本地日志記錄方便排查問題smtplib / requests結(jié)果通知郵件或微信推送渠道二選一選這些庫的考慮是穩(wěn)定、成熟、沒有太深的坑。requests 是 Python 社區(qū)的基礎(chǔ)庫各種加密參數(shù)雖然要手動處理但網(wǎng)上案例多BeautifulSoup 做 HTML 表格解析足夠用不需要引重型爬蟲框架。后續(xù)如果接口返回的不是 HTML 而是 JSON解析模塊可以直接替換成 json.loads改動成本很小。2.3 目錄結(jié)構(gòu)與模塊劃分項目拆成幾個職責(zé)單一的模塊后期維護(hù)起來非常舒服BUPTtakeCourse/ ├── main.py # 入口調(diào)度邏輯 ├── config.yaml # 配置文件賬號與選課目標(biāo) ├── login.py # 統(tǒng)一身份認(rèn)證登錄 ├── course.py # 課程查詢與目標(biāo)匹配 ├── selector.py # 選課請求提交 ├── monitor.py # 輪詢監(jiān)控與狀態(tài)機(jī) ├── notifier.py # 結(jié)果通知 └── logger.py # 日志初始化模塊劃分的原則是登錄只管登錄查詢只管查詢選課只管選課。尤其是登錄態(tài)單獨放一個模塊通過全局 session 對象共享其他模塊不關(guān)心 cookie 怎么來的只負(fù)責(zé)在請求時帶上它。這樣有個好處后面如果要給腳本加多賬號支持只需要改動 login.py 的調(diào)用方式其他模塊不用動。3. 核心模塊實現(xiàn)與細(xì)節(jié)解析3.1 統(tǒng)一身份認(rèn)證登錄模塊登錄是整套腳本的地基。北郵的賬號體系走的是統(tǒng)一身份認(rèn)證登錄狀態(tài)拿到手之后后續(xù)的課程查詢、選課提交都依賴這個 session 里的 cookie。我實現(xiàn)登錄模塊時遇到了最常見的三個坑。第一個是登錄請求中往往帶有動態(tài) token 或隱藏字段需要先從登錄頁抓取再拼進(jìn) POST 表單第二個是密碼傳輸經(jīng)常不是明文有的系統(tǒng)用 RSA 公鑰加密公鑰要從某個接口動態(tài)獲取第三個是部分環(huán)境會彈驗證碼這種情況自動識別成本太高我選擇了折衷方案——檢測到驗證碼時腳本暫停并向終端輸出提示人工輸入一次驗證碼后繼續(xù)。登錄成功后把 session 的 cookie 序列化保存到本地文件。這樣腳本重啟之后如果 cookie 還在有效期內(nèi)可以直接復(fù)用不需要每次都走登錄流程。選課系統(tǒng)卡頓的時候重復(fù)登錄本身也是一種負(fù)擔(dān)。3.2 課程查詢與目標(biāo)匹配課程查詢接口通常會返回當(dāng)前學(xué)期所有課程列表或者按關(guān)鍵字匹配的課程子集。頁面一般是一張表格里面包含課程名、課程號、任課教師、上課時間、剩余名額這些信息。course.py 負(fù)責(zé)把這張表解析成結(jié)構(gòu)化數(shù)據(jù)然后與 config.yaml 里配置的目標(biāo)課程做匹配。匹配邏輯我做了三層校驗避免誤選同名課程。第一層是課程名包含匹配比如配置的是“羽毛球”那“羽毛球初級班”“羽毛球提高班”都能命中第二層是校區(qū)或時段校驗如果你只想選周六上午的課腳本會過濾掉其他時段的選項第三層是人工確認(rèn)機(jī)制如果匹配到的課程多于一條腳本會先把結(jié)果打印出來讓用戶選一條避免腳本自作主張。這一層最關(guān)鍵的是要拿到穩(wěn)定字段。有些系統(tǒng)的課程名單里課程名和選課提交時的參數(shù)不是一回事課程名只是給人看的真正提交時用的是課程號或開課編號。所以 course.py 在匹配到課程后必須把該課程對應(yīng)的提交參數(shù)也一起提取出來存成一條“選課目標(biāo)”。3.3 自動選課與重試策略selector.py 負(fù)責(zé)真正提交選課請求。提交接口接收課程標(biāo)識、學(xué)期等參數(shù)一般返回一個狀態(tài)碼顯示選課成功、課容量已滿、時間沖突或者未到選課時間。這些狀態(tài)碼必須逐一映射成可讀的中文提示方便判斷失敗原因。重試策略是個值得細(xì)講的地方。這里不能直接用 while True 死循環(huán)高頻率提交而是要做狀態(tài)機(jī)狀態(tài)一等待開始選課時間未到低頻輪詢每個 30 秒查一次狀態(tài)二監(jiān)控余量時間已到每 3 到 5 秒查一次余量狀態(tài)三提交選課余量大于 0立即提交狀態(tài)四成功結(jié)束收到成功返回后停止該課程的監(jiān)控這樣設(shè)計的好處是不需要全程高頻轟炸對系統(tǒng)友好也不容易被風(fēng)控。每次請求之間我加了隨機(jī)延遲不是固定間隔而是 2 到 5 秒內(nèi)的隨機(jī)值這樣請求節(jié)奏更接近人的行為也避免了所有請求在同一時刻發(fā)出造成的瞬時擁塞。3.4 定時觸發(fā)與結(jié)果通知選課不是全天開放的系統(tǒng)會在某個時刻統(tǒng)一放名額。monitor.py 里我通過比較本地時間與配置的選課開始時間來切換狀態(tài)時間沒到就處于等待狀態(tài)時間一到自動進(jìn)入監(jiān)控狀態(tài)。這里要注意服務(wù)器時間可能和本地時間不一致我建議第一次運(yùn)行時先從教務(wù)系統(tǒng)響應(yīng)頭里拿一次服務(wù)器時間以它為準(zhǔn)。通知模塊用的方式比較輕成功后通過郵件或 Server醬 推一條消息到微信。我實際用下來推送比看日志靠譜得多選課成功的那一刻你未必在電腦前但手機(jī)通知一定不會錯過。失敗次數(shù)太多時也會推送一條提示方便及時調(diào)整策略。4. 完整實操流程與運(yùn)行調(diào)優(yōu)4.1 環(huán)境準(zhǔn)備與項目啟動先用 Python 3.8 以上版本跑通依賴創(chuàng)建虛擬環(huán)境后安裝這幾個包pip install requests pyyaml beautifulsoup4 lxml在 config.yaml 里填好學(xué)號和密碼配置目標(biāo)課程信息account: username: 你的學(xué)號 password: 你的密碼 targets: - name: 羽毛球初級 campus: 本部 time: 周六上午 schedule: start_time: 2025-02-20 10:00:00 interval_wait: 30 interval_query: 3 notify: email_enabled: false serverchan_key: 第一次運(yùn)行建議只測試登錄流程不開啟選課監(jiān)控??吹饺罩据敵龅卿洺晒Α⒄n程列表解析正常之后再帶入真實選課時間去跑完整流程。4.2 輪詢間隔與并發(fā)參數(shù)調(diào)優(yōu)輪詢間隔直接決定腳本的搶課成功率也決定了腳本給系統(tǒng)帶來的壓力。我實測下來interval_query 設(shè)為 3 秒比較合適能保證在名額釋放后的幾秒內(nèi)發(fā)起提交又不至于因為請求太密集導(dǎo)致自己的 IP 被臨時限制。設(shè)置成 1 秒以內(nèi)反而容易觸發(fā)風(fēng)控。提交選課時使用了線程池限制并發(fā)數(shù)默認(rèn)只支持一個目標(biāo)課程同時提交。因為多門課同時提交會引發(fā)沖突校驗比如同一時間段的課同時選系統(tǒng)會判定沖突。如果確實想同時搶多門不同時間的課每個課程跑一個獨立監(jiān)控實例并且每個請求之間保持 1 秒以上的間隔優(yōu)先級從上到下排列。4.3 日志觀測與成功判定運(yùn)行過程中最重要的是看日志。我在每個關(guān)鍵環(huán)節(jié)都加了日志登錄成功、課程匹配到幾條、進(jìn)入監(jiān)控狀態(tài)、檢測到余量、提交結(jié)果、成功通知。日志格式統(tǒng)一為時間、級別、模塊、內(nèi)容四段方便 grep2025-02-20 10:00:03,234 INFO couse.py 目標(biāo)課程羽毛球初級匹配到2條記錄 2025-02-20 10:00:06,531 INFO selector.py 第1次提交,返回碼200,選課成功成功判定必須看系統(tǒng)返回的狀態(tài)碼而不是只看 HTTP 200。有些系統(tǒng) HTTP 返回 200 但內(nèi)容里寫著“選課失敗時間沖突”。我把常見結(jié)果碼整理成映射表遇到未定義的狀態(tài)碼直接輸出原始報文方便后續(xù)補(bǔ)充。5. 常見問題與排查技巧實錄5.1 登錄狀態(tài)失效導(dǎo)致后續(xù)全部報錯最常見的問題是腳本跑著跑著突然連續(xù)報“未登錄”或“無權(quán)限”。原因是統(tǒng)一身份認(rèn)證的 session 過期或者因為多次請求被系統(tǒng)踢下線。排查時先看日志里是不是登錄模塊才有的錯誤確認(rèn)后就重新登錄。我的處理辦法是在 monitor.py 里做一個響應(yīng)狀態(tài)攔截器所有接口返回“未登錄”狀態(tài)碼時自動觸發(fā)一次重新登錄然后繼續(xù)任務(wù)。這樣 session 過期對用戶來說幾乎無感知只要賬號沒掉線就不會中斷監(jiān)控。5.2 請求被臨時限制頻率設(shè)置過高時接口會返回請求頻率超限或直接返回驗證碼頁面。這個問題的本質(zhì)是請求模式太像機(jī)器了。我調(diào)整過幾個參數(shù)后效果很明顯輪詢間隔從固定的 2 秒改成 2 到 5 秒之間的隨機(jī)值每次提交選課請求之前額外 sleep 0.3 到 0.8 秒去掉了多線程并發(fā)查詢同一個接口的邏輯。真實心塞的是有段時間我把并發(fā)調(diào)得過高IP 被限制了好幾個小時正好錯過了當(dāng)天名額。后來學(xué)乖了搶課腳本不能貪快穩(wěn)定比快更重要。5.3 課程名匹配到多條記錄導(dǎo)致選錯課課程名匹配得太寬泛可能會出現(xiàn)“命中了 5 條記錄、腳本默認(rèn)選了第一條結(jié)果”的情況。我踩過這個坑差點選錯課。解決方式前面提到過匹配到多條記錄時腳本不自動選擇先停住輸出備選列表同時詢問人工在終端輸入序號確認(rèn)。雖然多了一步交互但針對“課程名容易重復(fù)”的場景這個安全冗余是值得的。后來我還給 course.py 增加了緩存功能把匹配結(jié)果按課程名存成 JSON下次運(yùn)行同一門課直接復(fù)用人工確認(rèn)過的結(jié)果不用重復(fù)操作。5.4 明明有余量卻提交選課失敗這種情況有兩種原因。第一是選課條件不滿足比如有先修課程沒通過、專業(yè)限制不對或者該課程只對特定年級開放。第二種原因是提交參數(shù)缺少必要字段比如必須勾選的“同意調(diào)課”復(fù)選框?qū)?yīng)的請求參數(shù)沒有帶上。排查思路很直接先用瀏覽器手工選一次這門課注意觀察網(wǎng)絡(luò)面板里發(fā)出的請求把里面選課接口的參數(shù)和腳本發(fā)出去的參數(shù)做逐一對比。我按照這個思路解決過兩次問題一次是漏傳了學(xué)期字段一次是漏傳了選課類型字段加上之后立刻恢復(fù)正常。6. 合規(guī)使用與個人實操心得6.1 腳本的合規(guī)邊界和使用原則關(guān)于 BUPTtakeCourse 這類腳本我想認(rèn)真地提醒幾點。它只能用于自己的賬號只能用于學(xué)校允許你選的課程只能用于正常補(bǔ)選需求。不能拿它去大量擠占名額、替別人刷課、或者以任何方式影響選課系統(tǒng)的公平性。我明確拒絕了幾個功能需求批量替多個賬號搶課、在未開放時段強(qiáng)行請求、繞過課程限制條件。這些功能技術(shù)上都能做但越過安全的邊界就意味著給自己找麻煩。選課名額是公共資源腳本是公平競爭的手速增強(qiáng)器而不是破壞規(guī)則的工具。務(wù)必確認(rèn)自己是否了解并接受學(xué)校對自動選課的相關(guān)管理規(guī)定。6.2 我在實際調(diào)試中踩過的坑第一次寫登錄模塊時一直卡在密碼加密上后來通過斷點調(diào)試在瀏覽器開發(fā)者工具里查看請求提交的參數(shù)名才發(fā)現(xiàn)加密字段名和普通密碼字段不同處理起來就順暢了。這個經(jīng)驗對任何校內(nèi)腳本都適用先用瀏覽器手動操作一次在開發(fā)者工具的 Network 面板里觀察請求和響應(yīng)大多數(shù)接口規(guī)律都能摸清楚。另一個坑是本地時間與服務(wù)器時間不一致導(dǎo)致監(jiān)控提前進(jìn)入高頻狀態(tài)。當(dāng)時看著日志里到了設(shè)定時間但沒有余量空跑了一個小時。后來增加了一次 NTP 校準(zhǔn)——從系統(tǒng)響應(yīng)頭里讀取服務(wù)器時間字段雖然差異通常只有幾秒但對搶課來說已經(jīng)足夠了。6.3 后續(xù)可以怎么擴(kuò)展如果只想解決一門課的問題現(xiàn)在的代碼已經(jīng)夠了。但如果你想把這個項目當(dāng)成一個長期維護(hù)的工具我會建議做這幾件事。第一是增加 Web 配置界面不用每次改 YAML 文件第二是把輪詢狀態(tài)機(jī)抽象成更通用的選課引擎適配其他學(xué)校的教務(wù)系統(tǒng)第三是增加重試退避策略的單元測試保證頻繁重構(gòu)時核心邏輯不會被改壞。如果你只是想安穩(wěn)選上一門課不需要把這些擴(kuò)展全部做出來把 config.yaml 配好、跑一次看日志就夠了。希望我踩過的這些坑能幫你少熬幾個選課季的夜。本文還有配套的精品資源點擊獲取