Written in the preface

Want to know to go to work in Shenzhen is very painful thing, especially I go to work in the science and technology park this piece, go to the people very much, go to work every day with the Spring Festival transportation, if I can change to the previous big rush to work that happiness, unfortunately, can not change.

Especially I this station wait for the car of much of a pen, go to work bus crowded of not line, when the car is full only a few people can hard squeeze up. I usually only use two words to describe them: “Bus freak.”

Think that year my friend thin of like a monkey still can go up, Lao tze height 182 weight 72kg squeeze a bus, not a problem, backhand a block, stuffy sound make a fortune, front of the aunt you hurry up aunt, don’t whine haw, quickly go up ah aunt, eh? Are you trying to squeeze me out? Can you squeeze me out? You can squeeze me! I’m on the spot! Eat the car!

.

Ahem, crowded bus is impossible to crowded bus drop, because today I found a network bus can be customized routes of the public number [XXXXXX]

At the same time, I also found that sometimes people would refund the tickets, and then there would be free tickets. The key is that I can’t always keep an eye on the public account, so I wrote a small tool of ticket grabbing + SMS notification

Obtaining Interface Information

View the page structure

This is the booking page, showing the current month’s tickets, according to the picture, red is full, green is purchased, gray is not available

  1. Periodically capture the returned interface information
  2. Determine whether there are more tickets based on the interface return value

Okay, look at the source code, look at the interface information, wait, wechat browser can’t look at the source code, so

Use Chrome to debug wechat official account web page

First of all, there is a problem. If the webpage Url of the official account is directly copied and opened in Chrome, this screen will be displayed. He is redirected to this page by 302, so it is not feasible

So we need to use a packet capture tool to find out what information we need to bring when we visit our wechat official website. We need to use a packet capture tool because I’m a Mac and I can’t use Fiddler. I use a Charles vase

  1. Obtain the local IP address and port number
  2. Set the proxy mobile phone to access the Internet
  3. Perform the preceding two steps in sequence

Obtain the local IP address and port number

The first step is to find the port number. The default port number is 8088, but to make sure, you can open Proxy/Proxy Setting. Oh, I set it to 8888

Charles
help
Local IP Address

Set the proxy mobile phone to access the Internet

First of all, make sure that the phone and computer are connected to the same wifi, and then there will be Settings agent information in the wifi Settings, such as my monkey mi… No, mi 9! The Settings are as follows:

Enter the host name in the previous step, and the port number will be OK

Enter finished, click OK. Charles will pop up a dialog and ask you if you agree to access the agent. Just click ALLOW.

Access the target web page from your phone

We visited the wechat official account [XXXX] with our mobile phone and entered the ticket grabbing page, and found that Charles had successfully captured the webpage information. When we entered the ticket grabbing page, he would initiate two requests, one to obtain the document content and the other to obtain the ticket information.

After careful analysis, the business logic is generally understood:

The whole project technology station is Java + JSP, traditional writing method, user authentication is mainly cookie+session scheme, the front end of this piece is mainly using jQuery.

When the user enters the page, it carries query parameters, such as the starting site, the time, the train number and the cookie request the document, which is circled,

Because I’m gonna ask you again

The second request, with the cookie and the above query parameters, initiates a POST request to obtain the ticket information of the current month, namely the contents of the calendar

The following is a request for the current month’s ticket information, only to find that it returns a bunch of HTML nodes

Ok… Append it directly to the div, and then render it to generate the calendar table content

Then operate on the mobile phone, select the two dates, then click order, send the ticket purchase request, pull the ticket purchase interface, let’s look at the request and return content of the ticket purchase interface:

Take a look at what’s returned: return a JSON string of data, which roughly covers the order success return code, time, ID number, and so on

Record the required information content

According to the above analysis, summarize the content: The whole project user authentication is using cookie and session scheme, the request data is using form data, we also know what the request field is, except that when the request ticket, the return is HTML node code, instead of the EXPECTED JSON data, so there is a problem. We can’t understand at a glance how his spare ticket is displayed

So we had to debug Chrome to figure out how he judged the remaining votes.

Let’s get a notepad and write down the information. It says something like:

  1. Request the remaining ticket interface and ticket purchase interfaceurladdress
  2. cookieinformation
  3. The respectiverequestParameter field
  4. user-Agentinformation
  5. The respectiveresponseReturns the content

Set the chrome

With this information, we can start debugging with Chrome. First, open More Tools /Network Conditions

user-Agent
Custom

Charles Local request for packet capture

Because we need to fill the obtained cookie into Chrome and visit the web page as our user, we need to change the package and modify the cookie when requesting the target address

First we need to enable macOS Proxy to capture our HTTP requests

Charles

break points
sessionId
cookie
execute

jQuery
CSS

The following element findings were reviewed:

  1. The structure of the small square is:
<td class="b">
<span>Here is the date</span>
<span>If there are more tickets, show the number of tickets remaining</span>
</td>
Copy the code
  1. Td style nameaUnelectable
  2. The style is calledeOn behalf of the full
  3. The style is calleddOn behalf of the already bought
  4. The style is calledbThat’s what we’re looking for, which means optional, which means spare

At this point, the entire ticket purchase process is clear

When we make the request through Node.js, we process the returned data and use the regular expression to determine whether there is more than the class name b of the ticket. If there is more than the ticket, we can get the number of tickets in the div

Node.js requests the target interface

Analyze function points that need to be developed

Before we write code we need to figure out what features we need:

  1. Request residual ticket interface
  2. Scheduled request task
  3. Spare tickets will automatically request the ticket interface to order
  4. The Tencent Cloud SMS API is used to send SMS notifications
  5. Multiple users grab the ticket function
  6. Grab a ticket for a certain date

Mkdir ticket, create a folder called ticket, then CD ticket, enter the folder NPM init. Let’s start installing dependencies. Based on the above functional requirements, we will need:

  1. Request tools, see personal habits here, you can also use native oneshttp.requestI’ve chosen to use hereaxiosAfter all,axiosIt is also called at the bottom of the nodehttp.request
cnpm install axios --save
Copy the code
  1. Timing tasknode-schedule
cnpm install node-schedule --save
Copy the code
  1. On the Node end, select the DOM node toolcheerio
cnpm install cheerio --save
Copy the code
  1. Tencent SMS dependency packageqcloudsms_js
cnpm install qcloudsms_js 
Copy the code
  1. Hot update bag, Nuto’s mom,nodemon(You don’t have to.)
cnpm install nodemon --save-dev
Copy the code

Develop the request balance invoice interface

Then touch index.js to create the core JS file and start coding:

First introduce all dependencies


const axios = require('axios')
const querystring = require("querystring"); // Serialize objects, qs is ok, all the same
let QcloudSms = require("qcloudsms_js");
let cheerio = require('cheerio');
let schedule = require('node-schedule');

Copy the code

And then we’re going to define the request parameters, we’re going to do an obj

let obj = {
  data: {
    lineId: 111130.Id / / line
    vehTime: 0722.// Time of departure,
    startTime: 0751.// Estimated time of boarding
    onStationId: 564492.// The scheduled site ID
    offStationId: 17990./ / station id
    onStationName: Baoan Transport Bureau ③.// The name of the scheduled site
    offStationName: "Shenzhen-hong Kong Industry-University-Research Base".// The name of the scheduled destination
    tradePrice: 0./ / total amount
    saleDates: '17'.// Ticket date
    beginDate: ' '.// Booking time, empty, used to grab the rest of the ticket to fill in the data
  },
  phoneNumber: 123123123.// User's mobile phone number, the mobile phone number to receive the SMS
  cookie: 'JSESSIONID=TESTCOOKIE'.// The fetched cookie
  day: "17" // Book tickets on the 17th. This is mainly used to grab tickets on a specified date. If the tickets are empty, it means to grab all the remaining tickets of that month
}

Copy the code

Then declare a class called queryTicket. Why do we use a class? Because based on the fifth requirement point, when multiple users grab tickets, we just new each one.

At the same time, we want to record the number of requests for the remaining tickets and automatically stop the operation of querying the remaining tickets after grabbing the tickets, so we add a counting variable times and the variable whether to stop, the Boolean value stop

Write code:

class QueryTicket{
  /** *Creates an instance of QueryTicket. * @param {Object} { data, phoneNumber, cookie, Day} * @param data {Object} requery parameter * @param phoneNumber {Number} user phoneNumber, * @param cookie {String} Cookie information * @params Day {String} Ticket of a certain day, for example, '18' * @memberof QueryTicket Interface for requesting remaining tickets */
  constructor({ data, phoneNumber, cookie, day }) {
    this.data = data 
    this.cookie = cookie
    this.day = day
    this.phoneNumber = phoneNumber
    this.postData = querystring.stringify(data)
    this.times = 0;   // Record times
    let stop = false // The stop value can be changed only on a specific interface to prevent arbitrary external string changes
    this.getStop = function () { // Get whether to stop
      return stop 
    }
    this.setStop = function (ifStop) { // Set whether to stop
      stop = ifStop
    }
  }
}

Copy the code

Let’s start by defining the prototype method, breaking the logic into functions for easy maintenance

class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  / / constructor code...
  }
    init(){}/ / initialization
    handleQueryTicket(){}// Query the logic of the remaining ticket
    requestTicket(){} // Call the query residual ticket interface
    handleBuyTicket(){} // Ticket logic
    requestOrder(){}// Call the ticketing interface
    handleInfoUser(){}// Logic for notifying users
    sendMSg(){} // SMS interface
}

Copy the code

All the data is based on the operation of querying residual tickets, so we developed this part of the function first

class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  / / constructor code...
  }
  Async await 'is involved, so we use' async await '
   async init(){
          let ticketList = await this.handleQueryTicket() // Return the queried remainder group
    }
    // Query the logic of the remaining ticket
    handleQueryTicket(){ 
    let ticketList = [] // Residual votes group
    let res = await this.requestTicket()
    this.times++ // Count the number of times the query was requested
    let str = res.data.replace(/\\/g."") // Format the return value
    let $ = cheerio.load(`<div class="main">${str}</div>`) Cheerio loads the HTML node data for the query interface response
    let list = $(".main").find(".b") // Check whether the DOM node has more tickets
    // If there is no ticket left, print out how many times the request was made and return, not executing the code below
    if(! list.length) {console.log(` userThe ${this.phoneNumber}: No ticket, doneThe ${this.times}Time `)
      return
    }

    // If there are tickets available
    list.each((idx, item) = > {
      let str = $(item).html() $x4F59; $x4F59; $x4F59; 0
      // The content of the last span is "remainder 0", that is, no ticket, but it is transcoded
      // So format it in the next step
      let arr = str.split(/|<\/span>|\&\#x4F59\; /).filter(item= >!!!!! item ===true) 
      let data = {
        day: arr[0].ticketLeft: arr[1]}// If you want to grab a ticket on a specified date
      if (this.day) {
      // If there is a spare ticket for the specified date
        if (parseInt(data.day) === parseInt(data.day)) {
          ticketList.push(data)
        }
      } else {
      // If not, return all the queried residual tickets
        ticketList.push(data)
      }
    })
    return ticketList
    }
     // Call the query residual ticket interface
    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 Like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI"."Cookie": this.cookie
      }
    })   
    }
    handleBuyTicket(){} // Ticket logic
    requestOrder(){}// Call the ticketing interface
    handleInfoUser(){}// Logic for notifying users
    sendMSg(){} // SMS interface
}

Copy the code

To explain that regular line, the DOM that Cheerio grabs looks like this, the first span is the date, and the second is the number of remaining tickets

ticketList

Develop the function of purchasing tickets

First we make a judgment in init method, if there are more tickets to buy tickets, there is no ticket to buy a dime

class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  / / constructor code...
  }
  / / initialization
   async init(){
    let ticketList = await this.handleQueryTicket()
    // If there are tickets available
    if (ticketList.length) {
    // Pass the remaining tickets into the ticketing logic method and return the data required for the SMS notification
      let resParse = await this.handleBuyTicket(ticketList)
    }
    }
    
    // Query the logic of the remaining ticket
   async handleQueryTicket(){
    // Query the code of the remaining ticket...
    }
    // Call the query residual ticket interface
    requestTicket(){
    // Call query residual ticket interface code...
    } 
    // Ticket logic
   async handleBuyTicket(ticketList){
    let year = new Date().getFullYear() / / year,
    let month = new Date().getMonth() + 1 // Month is useful because the remaining ticket interface only returns a number
    let {
      onStationName,// Roll call at the departure station
      offStationName,// End site name
      lineId,/ / the line id
      vehTime,// Departure time
      startTime,// Estimated time of boarding
      onStationId,// Platform ID
      offStationId // Platform ID of the arrival station
      } = this.data // Initialize the data

    let station = `${onStationName}-${offStationName}` // The site is used to send text messages :" Baoan Traffic Bureau - Shenzhen-Hong Kong Industry-University-Research Base"
    let dateStr = ""; // Ticket date
    let tickAmount = "" / / the total number of sheets
    ticketList.forEach(item= > {
      dateStr = dateStr + `${year}-${month}-${item.day}, `
      tickAmount = tickAmount + `${item.ticketLeft}Zhang, `
    })

    let buyTicket = {
      lineId,/ / the line id
      vehTime,// Departure time
      startTime,// Estimated time of boarding
      onStationId,// The id of the boarding station
      offStationId,// Id of the target site
      tradePrice: '5'./ / the amount
      saleDates: dateStr.slice(0.- 1),
      payType: '2' // The payment method is wechat Pay
    }

    // Call the ticketing interface
     let data = querystring.stringify(buyTicket)
     let res = await this.requestOrder(data) // Return json data, whether ticket purchase was successful, etc
     // Send in all the data you need to send a text message
    return Object.assign({}, JSON.parse(res.data), { queryParam: { dateStr, tickAmount, startTime, station } })
    }// Ticket logic
    // Call the ticketing interface
    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 Like Gecko) Mobile/12A365 MicroMessenger/5.4.1 NetType/WIFI"."Cookie": this.cookie
      }
    })
    }
    handleInfoUser(){}// Logic for notifying users
    sendMSg(){} // SMS interface
}

Copy the code

At this point, the two core operations of querying remaining tickets and purchasing tickets have been completed.

What remains is how to notify the user whether the ticket has been purchased successfully.

Before, I tried to use THE SMTP service of QQ mailbox to send email notification after I got the ticket, but I think it is not easy to use, mainly because I do not have the habit of opening the mailbox and can not receive it without Internet, so I did not adopt this plan.

In addition, I registered an official account certified by an enterprise before, and Tencent Cloud sent me 1000 SMS notifications for free, and the SMS is relatively intuitive, so I installed the SDK of Tencent Cloud and deployed a set of SMS functions here.

Tencent Cloud SMS related content

Actually look at the document on the line, I also copy the document, pay attention to the SMS single sent that part

Cloud.tencent.com/document/pr…

If you are certified like me, look at the Quick Start here and follow the steps

{Number}

The text message template is fixed, but there is a {Number} content can be customized

When called, the number corresponds to the number of the argument array passed, with {1} representing the array [0] argument, and so on

Submit it for review, and the review usually goes through very quickly, just a few hundred thousand milliseconds

Development notification

class QueryTicket{
  constructor({ data, phoneNumber, cookie, day }) {
  / / constructor code...
  }
  / / initialization
   async init(){
    let ticketList = await this.handleQueryTicket()
    // If there are tickets available
    if (ticketList.length) {
    // Pass the remaining tickets into the ticketing logic method and return the data required for the SMS notification
      let resParse = await this.handleBuyTicket(ticketList)
    // Execute the notification logic
     this.handleInfoUser(resParse)
    }
    }
    
    // Query the logic of the remaining ticket
   async handleQueryTicket(){
    // Query the code of the remaining ticket...
    }
    // Call the query residual ticket interface
    requestTicket(){
    // Call query residual ticket interface code...
    } 
    // Ticket logic
   async handleBuyTicket(ticketList){
    // Ticket code...
    }
    // Call the ticketing interface
    requestOrder(obj){
    // Ticket interface request code...
    }
    // Logic for notifying users
    async handleInfoUser(parseData){
    // Obtain the response data from the previous step of ticket purchase and the data we spliced
    let { returnCode, returnData: { main: { lineName, tradePrice } }, queryParam: { dateStr, tickAmount, startTime, station } } = parseData
    // If the ticket is purchased successfully, 500 is returned
    if (returnCode === "500") {
      let res = await this.sendMsg({
        dateStr, / / date
        tickAmount: tickAmount.slice(0.- 1), / / the total number of sheets
        station, / / site
        lineName, // Bus name/route name
        tradePrice,/ / the total price
        startTime,// Departure time
        phoneNumber: this.phoneNumber,/ / cell phone number
      })
      // If the message is sent successfully, the ticket will not be snatched
      if (res.result === 0 && res.errmsg === "OK") {
        this.setStop(true)}else {
      // Fail to do any operation
        console.log(res.errmsg)
      }
    } else {
      // Fail to do any operation
      console.log(resParse['returnInfo'])}}// SMS interface
    sendMSg(){
    let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
    let appid = 140034324;  // SDK AppID starts with 1400
    // SMS application SDK AppKey
    let appkey = "asdfdsvajwienin23493nadsnzxc";
    // ID of the SMS template. You need to apply for it on the SMS console
    let templateId = 7839;  // NOTE:The template ID '7839' here is just an example. The real template ID needs to be applied in the SMS console
    / / signature
    let smsSign = "Test SMS";  // NOTE:The signature argument uses' signature content 'instead of' signature ID '. The signature "Tengcloud" here is just an example, the real signature needs to be applied in the SMS console
    // Instantiate QcloudSms
    let qcloudsms = QcloudSms(appid, appkey);
    let ssender = qcloudsms.SmsSingleSender();
    // Params = {1}{2}.. The content of the
    let params = [dateStr, station, lineName, startTime, tickAmount, tradePrice];
    // Use promise to encapsulate asynchronous operations
    return new Promise((resolve, reject) = > {
      ssender.sendWithParam(86, phoneNumber, templateId, params, smsSign, ""."".function (err, res, resData) {
        if (err) {
          reject(err)
        } else{ resolve(resData) } }); }}})Copy the code

If the message is sent successfully, result:0 is returned

At this point, most of the requirements have been completed and one scheduled task remains

Timing task

We also declare a class, and in this case we’re using schedule

// Scheduled task
class SetInter {
  constructor({ timer, fn }) {
    this.timer = timer // Execute every few seconds
    this.fn = fn // The callback to be performed
    this.rule = new schedule.RecurrenceRule(); // Instantiate an object
    this.rule.second = this.setRule() // Call the prototype method
    this.init()
  }
  setRule() {
    let rule = [];
    let i = 1;
    while (i < 60) {
      rule.push(i)
      i += this.timer
    }
    return rule // If the incoming timer is 5, the scheduled task is executed every 5 seconds
    // [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56] 
  }
  init() {
    schedule.scheduleJob(this.rule, () => {
      this.fn() // Call the incoming callback method regularly}); }}Copy the code

Multiple users grab tickets

Let’s say we have two users who want to grab tickets, so we define two objs and instantiate QueryTicket

  data: { 1 / / users
    lineId: 111130.vehTime: 0722.startTime: 0751.onStationId: 564492.offStationId: 17990.onStationName: Baoan Transport Bureau ③.offStationName: "Shenzhen-hong Kong Industry-University-Research Base".tradePrice: 0.saleDates: ' '.beginDate: ' ',},phoneNumber: 123123123.cookie: 'JSESSIONID=TESTCOOKIE'.day: "17"
}
let obj2 = { 2 / / users
  data: {
    lineId: 134423.vehTime: 1820.startTime: 1855.onStationId: 4322.offStationId: 53231.onStationName: 'Baidu International Building'.offStationName: "Yu on Junction".tradePrice: 0.saleDates: ' '.beginDate: ' ',},phoneNumber: 175932123124.cookie: 'JSESSIONID=TESTCOOKIE'.day: "" 
}
let ticket = new QueryTicket(obj) 1 / / users
let ticket2 = new QueryTicket(obj2) 2 / / users

new SetInter({
  timer: 1.// Execute once per second, recommended 5 seconds, otherwise afraid of IP mask, I here just for the sake of the screenshot below
  fn: function () {
    [ticket,ticket2].map(item= > { // At the same time
      if(! item.getStop()) {// Call the prototype method of the instance to determine whether to stop grabbing tickets, if not, continue to grab tickets
        item.init()
      } else { // If you get the ticket, you will not continue to get the ticket
        console.log('stop')}})Copy the code

Nodeindex.js is running, and it’s running

Write in the last

In fact, you can add more functions on this basis, such as directly grab the login interface to get cookies, designated route grab tickets, and error handling ah

It is worth noting that the request interface should not be too frequent, it is best to control the frequency of every 5 seconds, otherwise it will cause trouble to others, and it is easy to be masked by IP

If you want to make it into a complete project, it is recommended to use TS blessing. About TS, I recommend reading this article written by JD

Juejin. Cn/post / 684490…

I hope you find it useful

Special statement

This article is only for technical sharing, the code in the article is only for learning purposes, if there is an infringement, please contact me to delete

Irregular update JS all kinds of fun usage, welcome to pay attention, do not send ads, do not rely on this meal