現(xiàn)在科技越來越發(fā)達(dá),人們的生活越來越便捷,但是這樣子卻導(dǎo)致人類越來越懶了拷淘!到底是懶惰推動(dòng)了科技,還是科技助長了懶惰指孤。
背景
訂票網(wǎng)站:韻動(dòng)株洲游泳館訂票網(wǎng)站
訂票規(guī)則:用戶當(dāng)天7:00—22:00启涯,預(yù)約第二日免費(fèi)游泳公益券領(lǐng)取資格,每位用戶每天只能預(yù)訂一張(如有余票當(dāng)天也可預(yù)訂)恃轩。
游泳館概況:
注意:本腳本只實(shí)現(xiàn)簡單的訂票功能结洼,因?yàn)樵摼W(wǎng)站無需驗(yàn)證碼(很多外行的朋友,都問能不能幫忙去12306搶票叉跛。松忍。。)
功能目標(biāo)
自動(dòng)登錄功能(無驗(yàn)證碼?昀濉)
自動(dòng)選擇預(yù)定場地鸣峭、時(shí)間等信息,并提交表單
支持多賬號(hào)同時(shí)進(jìn)行刷票任務(wù)
定時(shí)任務(wù)
**郵件提醒搶票結(jié)果
工具模塊
python
splinter
shell
crontab 或 plist
流程分析
直接進(jìn)入游泳館預(yù)訂界面(還有很多其他的運(yùn)動(dòng)項(xiàng)目可以預(yù)約哦酥艳,羽毛球摊溶、室內(nèi)足球...真想給株洲政府點(diǎn)個(gè)贊)
點(diǎn)擊右上角登錄按鈕進(jìn)入登錄頁面
輸入手機(jī)賬號(hào)和密碼,點(diǎn)擊登錄按鈕進(jìn)入登錄狀態(tài)充石,此時(shí)頁面會(huì)跳轉(zhuǎn)到預(yù)訂界面
選擇好預(yù)定日期莫换、預(yù)定時(shí)間,點(diǎn)擊確認(rèn)預(yù)訂按鈕確認(rèn)預(yù)訂
確認(rèn)對(duì)話框點(diǎn)擊確認(rèn)骤铃,完成所有預(yù)訂過程(非預(yù)訂時(shí)間或者預(yù)定完了所以這里顯示 "undefined" )
以上就是整個(gè)預(yù)定流程浓镜,很簡單吧!
功能實(shí)現(xiàn)
◆ Splinter 環(huán)境配置
下載并安裝 splinter
下載并安裝 chrome Web 驅(qū)動(dòng)
python splinter 參考教程
◆ 訪問游泳館預(yù)定界面
from splinter.browser import Browser
from time import sleep
import datetime
import mail
import sys
url = "http://www.wentiyun.cn/venue-722.html"
#配置自己的chrome驅(qū)動(dòng)路徑
executable_path = {'executable_path':'/usr/local/Cellar/chromedriver/2.31/bin/chromedriver'}
def visitWeb(url):
#訪問網(wǎng)站
b = Browser('chrome', **executable_path)
b.visit(url)
return b
◆ 進(jìn)入登錄頁面并賬號(hào)密碼登錄
try:
lf = b.find_link_by_text(u"登錄")#登錄按鈕是鏈接的形式
sleep(0.1)
b.execute_script("window.scrollBy(300,0)")#下滑滾輪劲厌,將輸入框和確認(rèn)按鈕移動(dòng)至視野范圍內(nèi)
lf.click()
b.fill("username",username) # username部分輸入自己的賬號(hào)
b.fill("password",passwd) # passwd部分輸入賬號(hào)密碼
button = b.find_by_name("subButton")
button.click()
except Exception, e:
print "登錄失敗膛薛,請檢查登陸相關(guān):", e
sys.exit(1)
◆ 持續(xù)刷票策略
一旦以用戶的身份進(jìn)入到預(yù)訂界面,就需要按時(shí)間补鼻、場地信息要求進(jìn)行選擇哄啄,并確認(rèn)雅任。考慮到很可能提前預(yù)約或其他情況導(dǎo)致某次訂票失敗咨跌,所以沪么,僅僅一次訂票行為是不行的,需要反復(fù)訂票行為锌半,直到訂票成功禽车,于是,訂票策略如下:
反復(fù)訂票行為刊殉,退出條件:訂票一分鐘殉摔,即到七點(diǎn)過一分后退出,或預(yù)訂成功后退出
一次完整的訂票退出后(滿足1退出條件)记焊,為了保險(xiǎn)逸月,重啟 chrome,繼續(xù)預(yù)訂操作遍膜,十次操作后碗硬,退出預(yù)訂程序
時(shí)間選擇:獲取明天日期,選擇預(yù)訂明天的游泳票
def getBookTime():
#今天訂明天瓢颅,時(shí)間邏輯
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):
#退出時(shí)間判斷
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):
#反復(fù)訂票行為,直到時(shí)間條件達(dá)到或預(yù)訂成功退出
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()
#按鈕移到視野范圍內(nèi)
b.execute_script("window.scrollBy(0,100)")
#css顯示確認(rèn)按鈕
js = "var i=document.getElementsByClassName(\"btn_box\");i[0].style=\"display:true;\""
b.execute_script(js)
#點(diǎn)擊確認(rèn)
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 "預(yù)訂頁面刷票耗時(shí):%s秒" % (end-start).seconds
if result == "預(yù)訂成功!".decode("utf-8"):
return True
elif not timeCondition():
return False
b.reload()
except Exception, e:
print '預(yù)訂頁面刷票失敗,原因:', e
end = datetime.datetime.now()
print "共耗時(shí):%s秒" % (end-start).seconds
#判讀當(dāng)前時(shí)間如果是7點(diǎn)過5分了恩尾,放棄訂票
if not timeCondition():
return False
b.reload()
def tryBook(username, passwd):
#持續(xù)刷票10次后,退出程序
r = False
for i in xrange(10):
try:
start = datetime.datetime.now()
startStr = start.strftime('%Y-%m-%d %H:%M:%S')
print "========== 第%s次嘗試,開始時(shí)間%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, 已經(jīng)七點(diǎn)1分挽懦,搶票進(jìn)入尾聲" % i
b.quit()
end = datetime.datetime.now()
print "========== 第%s次嘗試結(jié)束,共耗時(shí)%s秒 ========" % (i, (end-start).seconds)
except Exception, e:
print '第%s次嘗試失敗特笋,原因:%s' % (i, e)
end = datetime.datetime.now()
print "========== 第%s次嘗試結(jié)束,共耗時(shí)%s秒 ========" % (i, (end-start).seconds)
return False
return r
◆ 郵件服務(wù)
參考一些資料實(shí)現(xiàn)的,程序其實(shí)不麻煩巾兆,主要是郵箱的 SMTP 服務(wù)!
需要郵箱開通 SMTP 代理服務(wù)虎囚,如果你 qq 號(hào)是很久之前注冊的了角塑,那我不推薦使用 qq 郵箱,一系列的密保會(huì)讓你崩潰淘讥。推薦使用新浪郵箱圃伶。
發(fā)送程序如下 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協(xié)議默認(rèn)端口是25
server.login(from_addr, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
'''
'''
@subject:郵件主題
@msg:郵件內(nèi)容
@toaddrs:收信人的郵箱地址
@fromaddr:發(fā)信人的郵箱地址
@smtpaddr:smtp服務(wù)地址,可以在郵箱看蒲列,比如163郵箱為smtp.163.com
@password:發(fā)信人的郵箱密碼
'''
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服務(wù)器
s.login(fromaddr,password) #登錄郵箱
s.sendmail(fromaddr, toaddrs, mail_msg.as_string()) #發(fā)送郵件
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)
◆ 定時(shí)任務(wù)策略
每天七點(diǎn)窒朋,搶票開始。為了保險(xiǎn)并且考慮到上文所構(gòu)建的搶票策略蝗岖,我們可以六點(diǎn)五十九分開始操作(考慮到還要訪問預(yù)訂頁面侥猩、登錄頁面以及登錄操作等,萬一有一定的延時(shí))抵赢。于是我們將任務(wù)布置在每天早上的六點(diǎn)五十九分欺劳。
定時(shí)任務(wù)的工具有兩種唧取,一種是使用 Linux 自帶的定時(shí)工具 crontab,一種是使用比較優(yōu)雅的 Mac 自帶的定時(shí)工具 plist划提。這兩種工具非常簡單實(shí)用枫弟,這里也不做太多介紹。
◆ 多賬號(hào)同時(shí)訂票操作策略
這就需要借助強(qiáng)大的 shell 腳本鹏往,我們把需要訂票的帳號(hào)密碼信息配置在 shell內(nèi)淡诗,同時(shí) shell 根據(jù)這些帳號(hào)信息啟動(dòng)不同的進(jìn)程來同時(shí)完成訂票任務(wù)。
#!/bin/bash
my_array=("130****3887" "****"\
"187****4631" "****")
#待操作用戶個(gè)數(shù)
len=${#my_array[@]}
len=`expr $len / 2`
i=0
while (($i < $len))
do
echo "第($i)個(gè)用戶為: ${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
◆ 日志服務(wù)
良好伊履、健壯的程序需要一套比較完備的日志系統(tǒng)韩容,本程序的日志服務(wù)都在上文中的程序中反映了,當(dāng)然不見得是最好的湾碎。僅供參考宙攻。這方便我們定位錯(cuò)誤或失敗的發(fā)生位置!
某些蛋疼的問題
- 需要將按鈕/鏈接顯示在視野范圍內(nèi)才能進(jìn)行點(diǎn)擊操作介褥。上文程序中諸如b.execute_script("window.scrollBy(300,0)") 等操作都是上下調(diào)整頁面位置座掘,將按鈕顯示在視野范圍內(nèi);如果某些按鈕是 invisible 的柔滔,那么我們可以通過修改 JS 中控件的屬性來顯示按鈕溢陪。如上文程序中的
#css顯示確認(rèn)按鈕
js = "var i=document.getElementsByClassName(\"btn_box\");i[0].style=\"display:true;\""
b.execute_script(js)
- 彈出框定位問題:最后預(yù)定成功會(huì)彈出一個(gè)確認(rèn)框:
那要獲得這個(gè)對(duì)話框并不容易。我嘗試過諸如 alert = browser.get_alert() alert.text alert.accept() alert.dismiss() 之類的辦法都沒有成功睛廊。最后右鍵這個(gè)對(duì)話框形真,找到它的源碼,根據(jù)ID信息找到這個(gè)對(duì)話框才解決的超全!
總結(jié)
技術(shù)上來說咆霜,本文并沒有什么亮點(diǎn),如果要應(yīng)付 12306 等一系列的網(wǎng)站嘶朱,那還有很多很麻煩的東西要研究蛾坯。但是,能用技術(shù)來解決生活中的實(shí)際問題疏遏,何樂而不為呢脉课!
其實(shí)這個(gè)定時(shí)訂票程序是一個(gè)很流程化的東西,實(shí)際上就是程序在模擬人的各種行為财异,所以在 coding 前一定要好好測試網(wǎng)站訂票流程倘零,把握訂票的規(guī)律。
有和同學(xué)交流戳寸,如果能 catch 到預(yù)定的消息格式呈驶,那豈不是更加簡便了!嗯疫鹊,我覺得很有道理俐东,不過沒有作嘗試跌穗,我對(duì)真正的那些刷票軟件也非常感興趣,但是現(xiàn)在還沒有時(shí)間去研究虏辫,也歡迎大牛指點(diǎn)蚌吸!
項(xiàng)目源碼:https://github.com/lps683/tic...
原文鏈接:https://segmentfault.com/a/1190000011008702
轉(zhuǎn)載 | Segmentfault
更多詳情關(guān)注我們的微信公眾號(hào):Reboot51