大學生都知道那種選課時無課可選的痛苦,而我所在的大學甚至對大部分課程都不提供候補系統。我們每天不得不多次登入檢視選課網站。這種機械操作似乎是計算機擅長的事,所以我著手用一些學過的 Python 知識和 Twilio API 來實現選課自動化。
開始階段
由於大學的課程註冊系統需要密碼登入,我們打算使用自建的簡化版網站。出於演示的目的,CS 101 課程的空餘名額將以 1 分鐘 1 次的頻率在 0 和 1 之間切換。
本專案中我們打算使用一些庫來幫助我們。假設你已經安裝了 pip,使用下面的 pip 命令來安裝需要的庫:
1 |
pip install requests==2.17.3 beautifulsoup4==4.6.0 redis==2.10.5 twilio==6.3.0 Flask==0.12.2 |
隨著專案的深入,我們會仔細研究用到的每一個庫。
抓取註冊系統
我們需要寫一個程式來幫助我們確定指定的課程是否有空餘名額。這裡我們使用網頁抓取技術來實現,它將從網路上下載網頁並尋找到重要的欄位(課程名額)。Requests 和 BeautifulSoup 是簡化這個過程的兩個非常流行的庫:Requests 讓獲取網頁變得更加簡單,而 BeautifulSoup 幫我們找到網頁中我們需要的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# scraper.py import requests from bs4 import BeautifulSoup URL = 'http://courses.project.samueltaylor.org/' COURSE_NUM_NDX = 0 SEATS_NDX = 1 def get_open_seats(): r = requests.get(URL) soup = BeautifulSoup(r.text, 'html.parser') courses = {} for row in soup.find_all('tr'): cols = [e.text for e in row.find_all('td')] if cols: courses[cols[COURSE_NUM_NDX]] = int(cols[SEATS_NDX]) return courses |
這裡的關鍵是 get_open_seats 函式。此函式中,我們使用 requests.get 下載網頁的 HTML 原始碼,然後使用 BeautifulSoup 解析它。我們使用 find_all(‘tr’) 獲得表內的所有行,通過更新課程詞典以顯示指定課程的剩餘名額。find_all 具有極其強大的功能,如果你對它感興趣並想要深入瞭解,你可以檢視官方文件。最後,我們返回課程詞典,這樣程式就能看到指定課程還有多少空餘名額(比如,courses[‘CS 101’]是 CS 101 的空餘名額)。
好極了,現在我們可以判斷課程是否有空位了。Python 直譯器是檢驗函式的好辦法。將這段程式碼儲存在檔案中,並命名為 scraper.py,然後執行指令碼並切入互動模式看看函式的功能:
1 2 3 |
$ python -i scraper.py >>> get_open_seats() {'CS 101': 1, 'CS 201': 0} |
儘管一切順利,但我們還沒有解決這個問題,我們還需要在空餘名額出現時,想辦法通知使用者。該是 Twilio SMS 出場的時候了!
通過 SMS 獲取更新
構建使用者介面時,我們希望化繁為簡。本程式中,使用者希望在課程座位開放時收到通知。最簡單的解決辦法是分享課程編號,我們通過建立和處理 webhook 實現訂閱功能。我選擇使用 Redis(提供可以從多個程式訪問的資料結構的工具)來儲存訂閱。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# sms_handler.py from flask import Flask, request import redis twilio_account_sid = 'ACXXXXX' redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) app = Flask(__name__) @app.route('/sms', methods=['POST']) def handle_sms(): user = request.form['From'] course = request.form['Body'].strip().upper() redis_client.sadd(course, user.encode('utf-8')) if __name__ == '__main__': app.run(debug=True) |
現在我們使用一個叫做 Flask 的 Python 網路框架來建立小型服務,用於處理 SMS 資訊。完成一些初始的設定後,我們設定 handle_sms 函式用於處理 /sms 端點的請求。利用這個函式,我們抓取使用者的手機號以及他們尋找的課程,並將其儲存在以課程命名的集合中。
做到獲取訂閱這步,一切還算順利,但有一個明顯的問題:使用者介面太爛,它不能給使用者提供反饋。我們想要回複使用者,通知我們是否能夠立刻服務他們的要求。要做到這一點,我們將提供一個 TwiML 響應,額外需要的程式碼在下方高亮顯示:
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 |
# sms_handler.py from flask import Flask, request import redis from twilio.twiml.messaging_response import MessagingResponse twilio_account_sid = 'ACXXXXX' my_number = '+1XXXXXXXXXX' valid_courses = {'CS 101', 'CS 201'} redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) app = Flask(__name__) def respond(user, body): response = MessagingResponse() response.message(body=body) return str(response) @app.route('/sms', methods=['POST']) def handle_sms(): user = request.form['From'] course = request.form['Body'].strip().upper() if course not in valid_courses: return respond(user, body="Hm, that doesn't look like a valid course. Try something like 'CS 101'.") redis_client.sadd(course, user.encode('utf-8')) return respond(user, body=f"Sweet action. We'll let you know when there are seats available in {course}") if __name__ == '__main__': app.run(debug=True) |
我們針對以上程式碼做了兩個大的改動。首先,我們驗證使用者是否在尋找一個有效的課程。其次,當使用者請求更新時,我們對其響應。在響應函式中,我們建立了一個 TwiML 響應,用指定的資訊內容回覆指定的號碼。
確保已安裝 Redis 並通過 redis 伺服器命令啟動它。將上述程式碼儲存在一個命名為 sms_handler.py 的檔案中,並通過 Python 執行它。
誠然,這裡的響應訊息有點傻,但我驚訝地看到使用者很喜歡它們。有時候一些人為觸發的反饋能帶來更好的使用者體驗。
現在,讓我們擴充套件早期的指令碼,完善通知功能,真正滿足那些想要在課程開放時獲得通知資訊的人。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# scraper.py from twilio.rest import Client client = Client(twilio_account_sid, token) redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) def message(recipient, body): message = client.messages.create(to=recipient, from_=my_number, body=body) if __name__ == '__main__': courses = get_open_seats() for course, seats in courses.items(): if seats == 0: continue to_notify = redis_client.smembers(course) for user in to_notify: message(user.decode('utf-8'), body=f"Good news! Spots opened up in {course}. " + "We'll stop bugging you about this one now.") redis_client.srem(course, user) |
通過 Python 執行 scraper.py,我們對採集程式進行一次性測試。
使用 Cron 密切關注課程
雖然將檢視課程註冊網站的過程簡化成了單個的指令碼,我們仍希望指令碼能夠隔幾分鐘自動執行一次。通過使用 Cron 能夠輕鬆解決這一問題。執行 crontab-e 並新增以下程式碼後,我們可以新增一個每三分鐘執行一次的任務:
1 |
*/3 * * * * /path/to/scraper.py |
寫入程式碼後,Cron 守護程式將每隔三分鐘執行一次我們的採集程式。執行 crontab-l 後,我們可以看到計劃任務。這就完成了!我們訂閱課程更新後,就可以不用管它,專注於我們手邊重要的事情。除了獲得樂趣外,你的朋友會非常感激你,因為你讓他們緊繃的神經得到了舒緩,“輕輕鬆鬆”就能選到想要的課程。依靠這個程式選到了想要的課程無疑是對我努力的最大的回報,但它同時也幫助我周圍的很多人選到了心儀的課程。
(—— CS 101)
(—— 收到,當課程 CS 101 有空位時,我們會立刻通知您!)
(—— 好訊息,偵察到課程 CS 101 存在空位,我們將不再傳送關於此課程的訊息。)
你可以使用本文的技術,為各種不同的事情設定通知。例如,有人用 Ruby 和 Twilio 來跟蹤有沒有工藝啤酒在售。若要獲得本文的所有程式碼,請參見這個 gist。你也可以通過以下方式聯絡我:
免責宣告:請務必確保設定通知不會違反你所在大學的學生系統服務條款。如有疑問,請詢問知情者!