编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Node.js 做一个抢票小工具(抢票github)

wxchong 2024-08-25 16:32:35 开源技术 9 ℃ 0 评论

今天带领大家一起用Node.js

实现抢票小工具&短信通知提醒


获取接口信息

查看页面结构


这个就是订票页面,显示当前月的车票情况,根据图示,红色为已满,绿色为已购,灰色为不可选



如果是可选就是白色的小方块,并且在下面显示余票,如下图所示:



我们可以这么做

1、定时抓取返回的接口信息

2、根据接口返回值判断是否有余票


审查下源代码看下接口信息现在微信浏览器没办法审查源代码


使用chrome 调试


首先面临个问题,如果直接copy公众号网页Url在chrome打开的话,就会显示这个画面,他被302重定向到了这个页面,所以是行不通的,只有获取OAuth2.0授权才能进去



我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取。

所以我们得先通过抓包工具,知道手机访问微信公众号网页的时候,需要带什么信息过去,这时候我们就得借助抓包工具,这里用的是Charles花瓶。



借助这个工具,我们只需3步就可以轻松搞定手机数据抓包:


1、获取本机IP地址和端口

2、设置代理手机上网

3、依次执行上面两步


获取本机IP地址和端口


第一步,找到端口号,一般默认是8088,为了确认可以打开Proxy/Proxy Setting看下


然后找到Charles的help/Local IP Address,点击它就会看到自己的本机地址,找到本机地址记下来,然后进行下一步



设置代理手机上网


首先保证手机跟电脑连接的是同一个wifi,然后在wifi设置那里会有设置代理信息。

设置如下:输入上一步获取主机名,端口号就ok了



输入完成,点击确定后。Charles就会弹出一个对话框,问你是否同意接入代理,点击确定allow就行了。


用手机访问目标网页


我们用手机访问微信公众号【xxxx】进入到抢票页面后,发现Charles已经成功抓包到了网页信息,当我们进入这个抢票页面的时候,他会发起两个请求,一个是获取document文档内容,一个post请求获取票务信息。


仔细分析了下,大概明白了业务逻辑:

整个项目技术站是java+jsp,传统写法,用户身份验证主要是cookie+session方案,前端这一块主要是使用jQuery。


当用户进入页面的时候,会携带查询参数,如起始站点,时间,车次等信息和cookie请求document文档, 也就是圈起来的这一块



而我们想要的核心内容:日历表,一开始是不显示的因为还要在请求一次。


第二次请求,携带cookie和以上的查询参数发起一个post请求,获取当月的车票信息,也就是日历表内容


下面这个是请求当月票务信息,然而发现他返回的是一堆html节点,估计是获取到之后直接append到div里面的,然后渲染生成日历表内容



接着在手机上操作,选择两个日期,然后点击下单,发送购票请求,拉取购票接口,我们看下购票接口的请求和返回内容:


看下request 内容,根据字段的意思大概明白是线路,时间,以及车票金额,还有支付方式


在看看返回的内容:返回一个json字符串数据,里面大概涵盖了下单的成功返回码,时间,id号等等信息



记录所需要的信息内容


根据上面的分析,总结下内容:整个项目用户身份验证是使用cookie和session方案,请求数据用的是form data方式,请求字段啥的我们也都清楚。


唯独有一点,就是请求余票的时候,返回的是html节点代码,而不是我们预期的json数据,这样就有个麻烦,我们没办法一目了然的明白他余票的时候是如何显示的


所以我们只能通过chrome进行调试,才能得出他是如何判断余票的。


1、请求余票接口和购票接口的url地址

2、cookie信息

3、各自的request参数字段

4、user-Agent信息

5、各自的response返回内容


设置chrome


有以上信息后,我们就可以开始用chrome调试了, 首先打开More tools/Network conditions


把user-Agent填入到Custom里面


Charles抓包本地请求


因为我们要把获取到的cookie填入到chrome里面,以我们的用户身份去访问网页,所以我们需要在请求目标地址的时候,改包修改cookie


首先我们需要开启 macOS Proxy,抓包我们的http请求



打开chrome访问目标网址,我们可以看到Charles上已经抓包到了我们访问的目标url地址,然后给目标url地址打上断点,方便调试

然后再次访问,这时候断点就生效了,弹出一个tab名为break points,可以看到之所以我们还是不能访问到目标网址,是因为sessionId不对,所以我们把抓取到的cookie在填入到里面,点击execute

这时候,能够正确跳到目标页面了。



大概看了下他整体布局,和jQuery代码CSS代码,特别是日历表那一块


审查了下元素发现:

1、小方块的结构为:<td class="b"> <span>这里为日期</span> <span>如果有余票则显示余票数量</span> </td> 复制代码

2、td的样式名为a代表不可选

3、样式名为e代表已满

4、样式名为d代表已购

5、样式名为b则是我们要找的,代表可选,也就是有余票


到这一步,整个购票流程就清楚了

到时候我们通过Node.js请求的时候,处理返回数据,用正则去判断是否有余票的class名b ,有余票的话,在获取div里面的余票数量内容就Ok了

Node.js 请求目标接口


分析需要开发的功能点


写代码之前我们需要想好功能点,我们需要什么功能:

1、请求余票接口

2、定时请求任务

3、有余票则自动请求购票接口下订单

4、调用腾讯云短信api接口发送短信通知

5、多个用户抢票功能

6、抢某个日期的票


首先mkdir ticket 创建名为ticket的文件夹,接着cd ticket进入文件夹npm init一路瞎几把回车也无妨。下面开始安装依赖,根据上面的功能需求,我们大概需要:


请求工具,这里选择用的是axios,毕竟axios在node端底层也是调用http.request



cnpm install axios --save


定时任务 node-schedule



cnpm install node-schedule --save


node端选择dom节点工具 cheerio



cnpm install cheerio --save


腾讯发短信的依赖包 qcloudsms_js



cnpm install qcloudsms_js ?


开发请求余票接口


接着touch index.js创建核心js文件,开始编码:

首先引入所有依赖

然后我们先定义请求参数,来一个obj

接着声明一个名为queryTicket的类,为啥要用类呢,因为基于第五个需求点,多个用户抢票的时候,我们分别new一下就行了,


同时我们希望能够记录请求余票的次数,和当抢到票后自动停止查询余票得操作,所以给他加上个计数变量times和是否停止的变量,布尔值stop

编写代码:

下面开始定义原型方法,为了方便维护,我们把逻辑拆分成各个函数

所有数据都是基于查询余票的操作,因此我们先开发这部分功能

class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  //constructor代码... 
  }
  //初始化,因为涉及到异步请求,所以我们使用`async await`
   async init(){
          let ticketList = await this.handleQueryTicket() //返回查询到的余票数组
    }
    //查询余票的逻辑
    handleQueryTicket(){ 
    let ticketList = [] //余票数组
    let res = await this.requestTicket()
    this.times++ //计数器,记录请求查询多少次
    let str = res.data.replace(/\\/g, "") //格式化返回值
    let $ = cheerio.load(`<div class="main">${str}</div>`) // cheerio载入查询接口response的html节点数据
    let list = $(".main").find(".b") //查找是否有余票的dom节点
    // 如果没有余票,打印出请求多少次,然后返回,不执行下面的代码
    if (!list.length) {
      console.log(`用户${this.phoneNumber}:无票,已进行${this.times}次`)
      return
    }

    // 如果有余票
    list.each((idx, item) => {
      let str = $(item).html() //str这时格式是<span>21</span><span>&$x4F59;0</span>
      //最后一个span 的内容其实"余0",也就是无票,只不过是被转码了而已
      //因此要在下一步对其进行格式化
      let arr = str.split(/<span>|<\/span>|\&\#x4F59\;/).filter(item => !!item === true) 
      let data = {
        day: arr[0],
        ticketLeft: arr[1]
      }
      
      //如果是要抢指定日期的票
      if (this.day) {
      //如果有指定日期的余票
        if (parseInt(data.day) === parseInt(data.day)) {
          ticketList.push(data)
        }
      } else {
      //如果不是,则返回查询到的所有余票
        ticketList.push(data)
      }
    })
    return ticketList
    }
     //调用查询余票接口
    requestTicket(){
    return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketCalendar', this.postData, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
        "Cookie": this.cookie
      }
    })   
    }
    handleBuyTicket(){} //购票相关逻辑
    requestOrder(){}//调用购票接口
    handleInfoUser(){}//通知用户的逻辑
    sendMSg(){} //发短信接口
}


来解释下那行正则,cheerio抓取到的dom是长这样的,第一个span内容是日期,第二个是余票数量


所以我们要把它格式化变成这种数组,也就是ticketList



开发购票功能


首先我们在init方法里做个判断,如果有余票才去购票。


class QueryTicket{
 ?constructor({ data, phoneNumber, cookie, day }) {
 ?//constructor代码...
 ?}
 ?//初始化
 ? async init(){
 ? ?let ticketList = await this.handleQueryTicket()
 ? ?//如果有余票
 ? ?if (ticketList.length) {
 ? ?//把余票传入购票逻辑方法,返回短信通知所需要的数据
 ? ? ?let resParse = await this.handleBuyTicket(ticketList)
 ? ?}
 ? ?}
 ? ?
 ? ?//查询余票的逻辑
 ? async handleQueryTicket(){
 ? ?// 查询余票代码...
 ? ?}
 ? ?//调用查询余票接口
 ? ?requestTicket(){
 ? ?//调用查询余票接口代码... ? ?
 ? ?}
 ? ?//购票相关逻辑
 ? async handleBuyTicket(ticketList){
 ? ?let year = new Date().getFullYear() //年份,
 ? ?let month = new Date().getMonth() + 1 //月份,拼接购票日期用得上,因为余票接口只返回几号
 ? ?let {
 ? ? ?onStationName,//起始站点名
 ? ? ?offStationName,//结束站点名
 ? ? ?lineId,//线路id
 ? ? ?vehTime,//发车时间
 ? ? ?startTime,//预计上车时间
 ? ? ?onStationId,//上车的站台id
 ? ? ?offStationId //到站的站台id
 ? ? ?} = this.data // 初始化的数据

 ? ?let station = `${onStationName}-${offStationName}` //站点,发短信时候用到:"宝安交通局-深港产学研基地"
 ? ?let dateStr = ""; //车票日期
 ? ?let tickAmount = "" //总张数
 ? ?ticketList.forEach(item => {
 ? ? ?dateStr = dateStr + `${year}-${month}-${item.day},`
 ? ? ?tickAmount = tickAmount + `${item.ticketLeft}张,`
 ? ?})

 ? ?let buyTicket = {
 ? ? ?lineId,//线路id
 ? ? ?vehTime,//发车时间
 ? ? ?startTime,//预计上车时间
 ? ? ?onStationId,//上车的站点id
 ? ? ?offStationId,//目标站点id
 ? ? ?tradePrice: '5', //金额
 ? ? ?saleDates: dateStr.slice(0, -1),
 ? ? ?payType: '2' //支付方式,微信支付
 ? ?}

 ? ?// 调用购票接口
 ? ? let data = querystring.stringify(buyTicket)
 ? ? let res = await this.requestOrder(data) //返回json数据,是否购票成功等等
 ? ? //把发短信所需要数据都要传入
 ? ?return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
 ? ?}//购票相关逻辑
 ? ?//调用购票接口
 ? ?requestOrder(obj){
 ? ?return axios.post('http://weixin.xxxx.net/ebus/front/wxQueryController.do?BcTicketBuy', obj, {
 ? ? ?headers: {
 ? ? ? ?'Content-Type': 'application/x-www-form-urlencoded',
 ? ? ? ?'User-Agent': "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI",
 ? ? ? ?"Cookie": this.cookie
 ? ? ?}
 ? ?})
 ? ?}
 ? ?handleInfoUser(){}//通知用户的逻辑
 ? ?sendMSg(){} //发短信接口
}

到这里,查询余票,购票这两个核心操作已经完成。

目前还剩下,如何通知用户是否购票成功。

这里使用的是安装腾讯云的SDK,部署了一套发短信的功能。


腾讯云短信的相关内容


可以百度腾讯云的文档,可以copy文档,注意看短信单发那部分

如果有企业认证的话,看快速入门这里就行了,一步步跟着操作


看下短信正文,{Number}这些里面的数字是变量。


就是说短信的模板是固定的,但是里面有{Number}的内容可以自定义

调用的时候,里面的数字对应着传过去的参数数组序号,{1}代表数组[0]参数,以此类推

提交审核,审核一般很快就通过,也就是几十万毫秒吧



开发通知功能


class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  //constructor代码... 
  }
  //初始化
   async init(){
    let ticketList = await this.handleQueryTicket()
    //如果有余票
    if (ticketList.length) {
    //把余票传入购票逻辑方法,返回短信通知所需要的数据
      let resParse = await this.handleBuyTicket(ticketList)
    //执行通知逻辑
     this.handleInfoUser(resParse)
    }
    }
    
    //查询余票的逻辑
   async handleQueryTicket(){
    // 查询余票代码...
    }
    //调用查询余票接口
    requestTicket(){
    //调用查询余票接口代码...    
    } 
    //购票相关逻辑
   async handleBuyTicket(ticketList){
    //购票代码...
    }
    //调用购票接口
    requestOrder(obj){
    //购票接口请求代码...
    }
    //通知用户的逻辑
    async handleInfoUser(parseData){
    //获取上一步购票的response数据和我们拼接的数据
    let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
    //如果购票成功,则返回500
    if (returnCode === "500") {
      let res = await this.sendMsg({
        dateStr, //日期
        tickAmount: tickAmount.slice(0, -1), //总张数
        station, //站点
        lineName, //巴士名称/路线名称
        tradePrice,//总价
        startTime,//出发时间
        phoneNumber: this.phoneNumber,//手机号
      })
      //如果发信成功,则不再进行抢票操作
      if (res.result === 0 && res.errmsg === "OK") {
        this.setStop(true)
      } else {
      //失败不做任何操作
        console.log(res.errmsg)
      }
    } else {
      //失败不做任何操作
      console.log(resParse['returnInfo'])
    }        
    }
    //发短信接口
    sendMSg(){
    let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
    let appid = 140034324;  // SDK AppID 以1400开头
    // 短信应用 SDK AppKey
    let appkey = "asdfdsvajwienin23493nadsnzxc";
    // 短信模板 ID,需要在短信控制台中申请
    let templateId = 7839;  // NOTE: 这里的模板ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请
    // 签名
    let smsSign = "测试短信";  // NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台申请
    // 实例化 QcloudSms
    let qcloudsms = QcloudSms(appid, appkey);
    let ssender = qcloudsms.SmsSingleSender();
    // 这里的params就是短信里面可以自定义的内容,也就是填入{1}{2}..的内容
    let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
    //用promise来封装下异步操作
    return new Promise((resolve, reject) => {
      ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, "", "", function (err, res, resData) {
        if (err) {
          reject(err)
        } else {
          resolve(resData)
        }
      });
    })
    } 
}


如果发信成功,返回result:0



定时任务


也声明一个类,这里我们用到的是schedule

// 定时任务
class SetInter {
  constructor({ timer, fn }) {
    this.timer = timer // 每几秒执行
    this.fn = fn //执行的回调
    this.rule = new schedule.RecurrenceRule(); //实例化一个对象
    this.rule.second = this.setRule() // 调用原型方法,schedule的语法而已
    this.init()
  }
  setRule() {
    let rule = [];
    let i = 1;
    while (i < 60) {
      rule.push(i)
      i += this.timer
    }
    return rule //假设传入的timer为5,则表示定时任务每5秒执行一次
    // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] 
  }
  init() {
    schedule.scheduleJob(this.rule, () => {
      this.fn() // 定时调用传入的回调方法
    });
  }
}

多个用户抢票


data: { //用户1
    lineId: 111130,
    vehTime: 0722,
    startTime: 0751,
    onStationId: 564492,
    offStationId: 17990,
    onStationName: '宝安交通运输局③',
    offStationName: "深港产学研基地",
    tradePrice: 0,
    saleDates: '',
    beginDate: '',
  },
  phoneNumber: 123123123,
  cookie: 'JSESSIONID=TESTCOOKIE',
  day: "17"
}
let obj2 = { //用户2
  data: {
    lineId: 134423,
    vehTime: 1820,
    startTime: 1855,
    onStationId: 4322,
    offStationId: 53231,
    onStationName: '百度国际大厦',
    offStationName: "裕安路口",
    tradePrice: 0,
    saleDates: '',
    beginDate: '',
  },
  phoneNumber: 175932123124,
  cookie: 'JSESSIONID=TESTCOOKIE',
  day: "" 
}
let ticket = new QueryTicket(obj) //用户1
let ticket2 = new QueryTicket(obj2) //用户2

new SetInter({
  timer: 1, //每秒执行一次,建议5秒,不然怕被ip拉黑,我这里只是为了方便下面截图
  fn: function () {
    [ticket,ticket2].map(item => { //同时进行两个用户的抢票
      if (!item.getStop()) {  //调用实例的原型方法,判断是否停止抢票,如果没有则继续抢
        item.init()
      } else { // 如果抢到票了,则不继续抢票
        console.log('stop')
      }
    })
  }
})


假设我们有两个用户要抢票,所以定义两个obj,实例化下QueryTicket类


node index.js 运行下,跑起来了

如果抢到票的话,我们就会收到短信通知:

打开手机,看下订单信息

原文链接:https://mp.weixin.qq.com/s/IqM2wW1CZB_JpG4neSSHGw
作者:前端迷

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表