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
- Periodically capture the returned interface information
- 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
- Obtain the local IP address and port number
- Set the proxy mobile phone to access the Internet
- 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:
- Request the remaining ticket interface and ticket purchase interface
url
address cookie
information- The respective
request
Parameter field user-Agent
information- The respective
response
Returns 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:
- 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
- Td style name
a
Unelectable - The style is called
e
On behalf of the full - The style is called
d
On behalf of the already bought - The style is called
b
That’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:
- Request residual ticket interface
- Scheduled request task
- Spare tickets will automatically request the ticket interface to order
- The Tencent Cloud SMS API is used to send SMS notifications
- Multiple users grab the ticket function
- 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:
- Request tools, see personal habits here, you can also use native ones
http.request
I’ve chosen to use hereaxios
After all,axios
It is also called at the bottom of the nodehttp.request
cnpm install axios --save
Copy the code
- Timing task
node-schedule
cnpm install node-schedule --save
Copy the code
- On the Node end, select the DOM node tool
cheerio
cnpm install cheerio --save
Copy the code
- Tencent SMS dependency package
qcloudsms_js
cnpm install qcloudsms_js
Copy the code
- 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