懶人法寶:定時訂票詳解

前言

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

背景

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

功能目標

  1. 自動登陸功能(無驗證碼!html

  2. 自動選擇預約場地、時間等信息,並提交表單python

  3. 支持多帳號同時進行刷票任務git

  4. 定時任務github

  5. 郵件提醒搶票結果chrome

工具模塊

  1. pythonshell

  2. splinterbash

  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 = "844582201@qq.com"  
password = "*****"  
from_addr = "m13072163887@163.com"  
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 = "mynameislps@sina.com"  
    smtpaddr = "smtp.sina.com"
    password = "*****"  
    subject = "這是郵件的主題"
    toaddrs = ["844582201@qq.com"]
    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/tic...

某些蛋疼的問題

  • 須要將按鈕/連接顯示在視野範圍內才能進行點擊操做。上文程序中諸如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到預約的消息格式,那豈不是更加簡便了!嗯,我以爲頗有道理,不過沒有做嘗試,我對真正的那些刷票軟件也很是感興趣,可是如今尚未時間去研究,也歡迎大牛指點!

相關文章
相關標籤/搜索