美文网首页自动化
懒人法宝:定时订票详解

懒人法宝:定时订票详解

作者: 51reboot | 来源:发表于2017-09-11 16:06 被阅读364次

    现在科技越来越发达,人们的生活越来越便捷,但是这样子却导致人类越来越懒了!到底是懒惰推动了科技,还是科技助长了懒惰。

    背景

    订票网站:韵动株洲游泳馆订票网站
    订票规则:用户当天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

    相关文章

      网友评论

        本文标题:懒人法宝:定时订票详解

        本文链接:https://www.haomeiwen.com/subject/sxqfsxtx.html