现在科技越来越发达,人们的生活越来越便捷,但是这样子却导致人类越来越懒了!到底是懒惰推动了科技,还是科技助长了懒惰。
背景
订票网站:韵动株洲游泳馆订票网站
订票规则:用户当天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
网友评论