现在科技越来越发达,人们的生活越来越便捷,但是这样子却导致人类越来越懒了!到底是懒惰推动了科技,还是科技助长了懒惰。
背景
订票网站:韵动株洲游泳馆订票网站
订票规则:用户当天7:00—22:00,预约第二日免费游泳公益券领取资格,每位用户每天只能预订一张(如有余票当天也可预订)。
游泳馆概况:
注意:本脚本只实现简单的订票功能,因为该网站无需验证码(很多外行的朋友,都问能不能帮忙去12306抢票。。。)
功能目标
自动登录功能(无验证码!)
自动选择预定场地、时间等信息,并提交表单
支持多账号同时进行刷票任务
定时任务
**邮件提醒抢票结果
工具模块
python
splinter
shell
crontab 或 plist
流程分析
直接进入游泳馆预订界面(还有很多其他的运动项目可以预约哦,羽毛球、室内足球...真想给株洲政府点个赞)
点击右上角登录按钮进入登录页面
输入手机账号和密码,点击登录按钮进入登录状态,此时页面会跳转到预订界面
选择好预定日期、预定时间,点击确认预订按钮确认预订
确认对话框点击确认,完成所有预订过程(非预订时间或者预定完了所以这里显示 "undefined" )
以上就是整个预定流程,很简单吧!
功能实现
◆ Splinter 环境配置
下载并安装 splinter
下载并安装 chrome Web 驱动
python 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
◆ 进入登录页面并账号密码登录
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退出条件),为了保险,重启 chrome,继续预订操作,十次操作后,退出预订程序
时间选择:获取明天日期,选择预订明天的游泳票
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
◆ 日志服务
良好、健壮的程序需要一套比较完备的日志系统,本程序的日志服务都在上文中的程序中反映了,当然不见得是最好的。仅供参考。这方便我们定位错误或失败的发生位置!
某些蛋疼的问题
- 需要将按钮/链接显示在视野范围内才能进行点击操作。上文程序中诸如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信息找到这个对话框才解决的!
总结
技术上来说,本文并没有什么亮点,如果要应付 12306 等一系列的网站,那还有很多很麻烦的东西要研究。但是,能用技术来解决生活中的实际问题,何乐而不为呢!
其实这个定时订票程序是一个很流程化的东西,实际上就是程序在模拟人的各种行为,所以在 coding 前一定要好好测试网站订票流程,把握订票的规律。
有和同学交流,如果能 catch 到预定的消息格式,那岂不是更加简便了!嗯,我觉得很有道理,不过没有作尝试,我对真正的那些刷票软件也非常感兴趣,但是现在还没有时间去研究,也欢迎大牛指点!
项目源码:https://github.com/lps683/tic...
原文链接:https://segmentfault.com/a/1190000011008702
转载 | Segmentfault
更多详情关注我们的微信公众号:Reboot51