懶人法寶:定時訂票詳解

前言

暑假閒來無事,每天上午的寶貴時間想去游泳,減減肚子,練練耐力,正好我們那個地方游泳館上午提供免費的票,但是,需要前一天早上七點開始預定第二天上午的免費游泳票。往年暑假,我是每天早上六點五十五準時起牀,眼睛半睜不睜的等着七點一到,立馬搶票!搶完一臉解脫地癱倒在牀上繼續睡覺。簡直就是煎熬啊,我在學校都沒起這麼早過。
今年暑假,我實在是不想再早起了,考慮到訂票網站的訂票流程非常簡易,是否能寫一個腳本代替我每天早上完成訂票任務呢。答案是肯定的。最後我大概雖然其實用到的方法很簡單,但是既然是在生活中難得遇到的實際問題,我也做一個分享。之前我是沒有任何刷票、爬蟲經歷的。(本人專注數據挖掘)
技術改變生活,本篇博客的目的僅僅是分享並記錄一下用互聯網方法解決懶人在生活中的實際問題。

背景

訂票網站:韻動株洲游泳館訂票網站
訂票規則:用戶當天7:00—22:00,預約第二日免費游泳公益券領取資格,每位用戶每天只能預訂一張(如有餘票當天也可預訂)。
游泳館概況:(嘿嘿,我大株洲就是厲害)
這裏寫圖片描述
這裏寫圖片描述
注意:本腳本只實現簡單的訂票功能,因爲該網站無需驗證碼(很多外行的朋友,雖然我也是外行,都問我能不能幫忙去12306搶票。。。)

功能目標

  1. 自動登錄功能(無驗證碼!
  2. 自動選擇預定場地、時間等信息,並提交表單
  3. 支持多賬號同時進行刷票任務
  4. 定時任務
  5. 郵件提醒搶票結果

工具模塊

  1. python
  2. splinter
  3. shell
  4. crontabplist

流程分析

直接進入游泳館預訂界面(還有很多其他的運動項目可以預約哦,羽毛球、室內足球…真想給株洲政府點個贊)
這裏寫圖片描述
點擊右上角登錄按鈕進入登錄頁面
這裏寫圖片描述
輸入手機賬號和密碼,點擊登錄按鈕進入登錄狀態,此時頁面會跳轉到預訂界面
這裏寫圖片描述
選擇好預定日期、預定時間,點擊確認預訂按鈕確認預訂
這裏寫圖片描述
確認對話框點擊確認,完成所有預訂過程(非預訂時間或者預定完了所以這裏顯示”undefined”)
以上就是整個預定流程,很簡單吧!正是這麼簡單,讓我萌生了花點時間寫個腳本來代替我訂票的邪惡想法!

功能實現

Splinter環境配置

訪問游泳館預定界面

from splinter.browser import Browser
from time import sleep
import datetime
import mail
import sys
url = "http://www.wentiyun.cn/venue-722.html"
#配置自己的chrome驅動路徑
executable_path = {'executable_path':'/usr/local/Cellar/chromedriver/2.31/bin/chromedriver'}

def visitWeb(url):
    #訪問網站
    b = Browser('chrome', **executable_path)
    b.visit(url)
    return b

進入登錄頁面並賬號密碼登錄

def login(b, username, passwd):
    try:
        lf = b.find_link_by_text(u"登錄")#登錄按鈕是鏈接的形式
        sleep(0.1)
        b.execute_script("window.scrollBy(300,0)")#下滑滾輪,將輸入框和確認按鈕移動至視野範圍內
        lf.click()
        b.fill("username",username) # username部分輸入自己的賬號
        b.fill("password",passwd) # passwd部分輸入賬號密碼
        button = b.find_by_name("subButton")
        button.click()
    except Exception, e:
        print "登錄失敗,請檢查登陸相關:", e
        sys.exit(1)

持續刷票策略

一旦以用戶的身份進入到預訂界面,就需要按時間、場地信息要求進行選擇,並確認。考慮到很可能提前預約或其他情況導致某次訂票失敗,所以,僅僅一次訂票行爲是不行的,需要反覆訂票行爲,直到訂票成功,於是,訂票策略如下:
1. 反覆訂票行爲,退出條件:訂票一分鐘,即到七點過一分後退出,或預訂成功後退出
2. 一次完整的訂票退出後(滿足1退出條件),爲了保險,重啓chrome,繼續預訂操作,十次操作後,退出預訂程序
3. 時間選擇:獲取明天日期,選擇預訂明天的游泳票

def getBookTime():
    #今天訂明天,時間邏輯
    date = datetime.datetime.now() + datetime.timedelta(days=1)
    dateStr = date.strftime('%Y-%m-%d')
    year, month, day = dateStr.split('-')
    date = '/'.join([month, day])
    return date
def timeCondition(h=7.0,m=1.0,s=0.0):
    #退出時間判斷
    now = datetime.datetime.now()
    dateStr = now.strftime('%H-%M-%S')
    hour, minute, second = dateStr.split('-')
    t1 = h*60.0 + m + s/60.0
    t2 = float(hour)*60.0 + float(minute) + float(second)/60.0
    if t1 >= t2:
        return True
    return False
def book(b):
    #反覆訂票行爲,直到時間條件達到或預訂成功退出
    while(True):
        start = datetime.datetime.now()
        startStr = start.strftime('%Y-%m-%d %H:%M:%S')
        print "********** %s ********" % startStr
        try:
            #選擇日期
            date = getBookTime()
            b.find_link_by_text(date).click()
            #按鈕移到視野範圍內
            b.execute_script("window.scrollBy(0,100)")
            #css顯示確認按鈕
            js = "var i=document.getElementsByClassName(\"btn_box\");i[0].style=\"display:true;\""
            b.execute_script(js)
            #點擊確認
            b.find_by_name('btn_submit').click()
            sleep(0.1)
            b.find_by_id('popup_ok').click()
            sleep(0.1)
            #測試彈出框
            #test(b)
            #sleep(0.1)
            result = b.evaluate_script("document.getElementById(\"popup_message\").innerText")
            b.find_by_id('popup_ok').click()
            sleep(0.1)
            print result
            end = datetime.datetime.now()
            print "預訂頁面刷票耗時:%s秒" % (end-start).seconds
            if result == "預訂成功!".decode("utf-8"):
                return True
            elif not timeCondition():
                return False
            b.reload()
        except Exception, e:
            print '預訂頁面刷票失敗,原因:', e
            end = datetime.datetime.now()
            print "共耗時:%s秒" % (end-start).seconds
            #判讀當前時間如果是7點過5分了,放棄訂票
            if not timeCondition():
                return False
            b.reload()
def tryBook(username, passwd):
    #持續刷票10次後,退出程序
    r = False
    for i in xrange(10):
        try:
            start = datetime.datetime.now()
            startStr = start.strftime('%Y-%m-%d %H:%M:%S')
            print "========== 第%s次嘗試,開始時間%s ========" % (i, startStr)
            b = visitWeb(url)
            login(b, username, passwd)
            r = book(b)
            if r:
                print "book finish!"
                b.quit()
                break
            else:
                print "try %s again, 已經七點1分,搶票進入尾聲" % i
                b.quit()
            end = datetime.datetime.now()
            print "========== 第%s次嘗試結束,共耗時%s秒 ========" % (i, (end-start).seconds)
        except Exception, e:
            print '第%s次嘗試失敗,原因:%s' % (i, e)
            end = datetime.datetime.now()
            print "========== 第%s次嘗試結束,共耗時%s秒 ========" % (i, (end-start).seconds)
            return False
    return r

郵件服務

  • 參考廖雪峯老師的實現哦,程序其實不麻煩,主要是郵箱的SMTP服務!
  • 需要郵箱開通SMTP代理服務,如果你qq號是很久之前註冊的了,那我不推薦使用qq郵箱,一系列的密保會讓你崩潰。推薦使用新浪郵箱。
  • 發送程序如下mail.py
import smtplib  
import traceback  
from email.mime.text import MIMEText  
from email.mime.multipart import MIMEMultipart  
from email.header import Header
from email.utils import parseaddr, formataddr
'''
to_addr = "[email protected]"  
password = "*****"  
from_addr = "[email protected]"  
msg = MIMEText('hello, send by Python...', 'plain', 'utf-8')
server = smtplib.SMTP("smtp.163.com") # SMTP協議默認端口是25
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
'''
'''
    @subject:郵件主題 
    @msg:郵件內容 
    @toaddrs:收信人的郵箱地址 
    @fromaddr:發信人的郵箱地址 
    @smtpaddr:smtp服務地址,可以在郵箱看,比如163郵箱爲smtp.163.com 
    @password:發信人的郵箱密碼 
''' 
def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr((Header(name, 'utf-8').encode(), addr))

def sendmail(subject,msg,toaddrs,fromaddr,smtpaddr,password):  
    mail_msg = MIMEMultipart()  
    if not isinstance(subject,unicode):  
        subject = unicode(subject, 'utf-8')  
    mail_msg['Subject'] = subject  
    mail_msg['From'] = _format_addr('Python-auto <%s>' % fromaddr)
    mail_msg['To'] = ','.join(toaddrs)  
    mail_msg.attach(MIMEText(msg, 'plain', 'utf-8'))  
    try:  
        s = smtplib.SMTP()  
        s.set_debuglevel(1)
        s.connect(smtpaddr,25)  #連接smtp服務器  
        s.login(fromaddr,password)  #登錄郵箱  
        s.sendmail(fromaddr, toaddrs, mail_msg.as_string()) #發送郵件  
        s.quit()  
    except Exception,e:  
       print "Error: unable to send email", e  
       print traceback.format_exc()  

def send(msg):
    fromaddr = "[email protected]"  
    smtpaddr = "smtp.sina.com"
    password = "*****"  
    subject = "這是郵件的主題"
    toaddrs = ["[email protected]"]
    sendmail(subject,msg,toaddrs,fromaddr,smtpaddr,password)

定時任務策略

每天七點,搶票開始。爲了保險並且考慮到上文所構建的搶票策略,我們可以六點五十九分開始操作(考慮到還要訪問預訂頁面、登錄頁面以及登錄操作等,萬一有一定的延時)。於是我們將任務佈置在每天早上的六點五十九分。
定時任務的工具有兩種,一種是使用Linux自帶的定時工具crontab,一種是使用比較優雅的Mac自帶的定時工具plist。這兩種工具非常簡單實用,這裏也不做太多介紹。

多賬號同時訂票操作策略

這就需要藉助強大的shell腳本,我們把需要訂票的帳號密碼信息配置在shell內,同時shell根據這些帳號信息啓動不同的進程來同時完成訂票任務。

#!/bin/bash
my_array=("130****3887" "****"\
        "187****4631" "****")
#待操作用戶個數
len=${#my_array[@]}
len=`expr $len / 2`
i=0
while (($i < $len))
do 
    echo "第($i)個用戶爲: ${my_array[2*i]}"
    logname="/Users/lps/work/program/ticketReservation/log/${my_array[2*i]}.log"
    nohup /Users/lps/anaconda/bin/python /Users/lps/work/program/ticketReservation/book.py ${my_array[2*i]} ${my_array[2*i+1]} > ${logname} 2>&1 &
    i=`expr $i + 1`
done

日誌服務

良好、健壯的程序需要一套比較完備的日誌系統,本程序的日誌服務都在上文中的程序中反映了,當然不見得是最好的。僅供參考。這方便我們定位錯誤或失敗的發生位置!

完整的工程在Github上:https://github.com/lps683/ticketBook

某些蛋疼的問題

  • 需要將按鈕/鏈接顯示在視野範圍內才能進行點擊操作。上文程序中諸如b.execute_script("window.scrollBy(300,0)")等操作都是上下調整頁面位置,將按鈕顯示在視野範圍內;如果某些按鈕是invisible的,那麼我們可以通過修改JS中控件的屬性來顯示按鈕。如上文程序中的
#css顯示確認按鈕
js = "var i=document.getElementsByClassName(\"btn_box\");i[0].style=\"display:true;\""
b.execute_script(js)
  • 彈出框定位問題:最後預定成功會彈出一個確認框:
    這裏寫圖片描述
    那要獲得這個對話框並不容易。我嘗試過諸如alert = browser.get_alert() alert.text alert.accept() alert.dismiss()之類的辦法都沒有成功。最後右鍵這個對話框,找到它的源碼,根據ID信息找到這個對話框才解決的!

總結

  1. 技術上來說,本文並沒有什麼亮點,如果要應付12306等一系列的網站,那還有很多很麻煩的東西要研究。但是,能用技術來解決生活中的實際問題,何樂而不爲呢!
  2. 其實這個定時訂票程序是一個很流程化的東西,實際上就是程序在模擬人的各種行爲,所以在coding前一定要好好測試網站訂票流程,把握訂票的規律。
  3. 有和同學交流,如果能catch到預定的消息格式,那豈不是更加簡便了!嗯,我覺得很有道理,不過沒有作嘗試,我對真正的那些刷票軟件也非常感興趣,但是現在還沒有時間去研究,也歡迎大牛指點!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章