Node.js 實現(xiàn)搶票小工具 & 短信通知提醒功能
要知道在深圳上班是非常痛苦的事情,特別是我上班的科興科技園這一塊,去的人非常多,每天上班跟春運一樣,如果我能換到以前的大沖上班那就幸福了,可惜,換不得。
尤其是我這個站等車的多的一筆,上班公交擠的不行,車滿的時候只有少部分人能硬擠上去。通常我只會用兩個字來形容這種人:“公交怪”
想當年我朋友瘦的像只猴還能上去,老子身高182體重72kg擠個公交,不成問題,反手一個阻擋,悶聲發(fā)大財,前面的阿姨你快點阿姨,別磨磨唧唧的,快上去啊阿姨,嗯?你還想擠掉我?你能擠掉我?你能擠掉我!我當場!把車吃了!
....
咳咳,擠公交是不可能擠公交滴,因為今天我發(fā)現(xiàn)了一個可以定制路線的網約巴士公眾號【深圳xxx】
但是呢,票經常會被搶光,同時我還我發(fā)現(xiàn),有時候會有人退票,這時候就有空余票了,關鍵是我不可能時時都在公眾號上盯著,于是,我就寫了一個搶票+短信通知的小工具
獲取接口信息
1.查看頁面結構
這個就是訂票頁面,顯示當前月的車票情況,根據圖示,紅色為已滿,綠色為已購,灰色為不可選

如果是可選就是白色的小方塊,并且在下面顯示余票,如下圖所示:

我們打算這么做,
- 定時抓取返回的接口信息
- 根據接口返回值判斷是否有余票
好,審查下源代碼看下接口信息,等等,微信瀏覽器沒辦法審查源代碼,于是
2.使用chrome調試微信公眾號網頁頁面
首先面臨個問題,如果直接copy公眾號網頁Url在chrome打開的話,就會顯示這個畫面,他被302重定向到了這個頁面,所以是行不通的,只有獲取OAuth2.0授權才能進去

所以我們得先通過抓包工具,知道手機訪問微信公眾號網頁的時候,需要帶什么信息過去,這時候我們就得借助抓包工具,因為我電腦是Mac,用不了 Fiddler ,我用的是 Charles 花瓶,就是下面這位仁兄

借助這個工具,我們只需3步就可以輕松搞定手機數據抓包:
- 獲取本機IP地址和端口
- 設置代理手機上網
- 依次執(zhí)行上面兩步
2-1 獲取本機IP地址和端口
第一步,找到端口號,一般默認是8088,但是為了確認可以打開 Proxy / Proxy Setting 看下,哦原來我之前設置成了8888

然后找到 Charles 的 help / Local IP Address,點擊它就會看到自己的本機地址,找到本機地址記下來,然后進行下一步

2-2設置代理手機上網
首先保證手機跟電腦連接的是同一個wifi,然后在wifi設置那里會有設置代理信息,比如我的猴米...不對,小米9手機!設置如下:
輸入上一步獲取主機名,端口號就ok了

輸入完成,點擊確定后。 Charles 就會彈出一個對話框,問你是否同意接入代理,點擊確定allow就行了。
2-3 用手機訪問目標網頁
我們用手機訪問微信公眾號【深圳x出行】進入到搶票頁面后,發(fā)現(xiàn) Charles 已經成功抓包到了網頁信息,當我們進入這個搶票頁面的時候,他會發(fā)起兩個請求,一個是獲取document文檔內容,一個post請求獲取票務信息。
仔細分析了下,大概明白了業(yè)務邏輯:
整個項目技術站是java+jsp,傳統(tǒng)寫法,用戶身份驗證主要是cookie+session方案,前端這一塊主要是使用 jQuery 。
當用戶進入頁面的時候,會攜帶查詢參數,如起始站點,時間,車次等信息和cookie請求document文檔, 也就是圈起來的這一塊,

而我們想要的核心內容:日歷表,一開始是不顯示的
因為還要在請求一次
第二次請求,攜帶cookie和以上的查詢參數發(fā)起一個post請求,獲取當月的車票信息,也就是日歷表內容
下面這個是請求當月票務信息,然而發(fā)現(xiàn)他返回的是一堆html節(jié)點
好吧...估計是獲取到之后直接 append 到 div 里面的,然后渲染生成日歷表內容

接著在手機上操作,選擇兩個日期,然后點擊下單,發(fā)送購票請求,拉取購票接口,我們看下購票接口的請求和返回內容:

看下request 內容,根據字段的意思大概明白是線路,時間,以及車票金額,還有支付方式
在看看返回的內容:返回一個json字符串數據,里面大概涵蓋了下單的成功返回碼,時間,id號等等信息

2-4 記錄所需要的信息內容
根據上面的分析,總結下內容: 整個項目用戶身份驗證是使用 cookie 和 session 方案,請求數據用的是 form data 方式,請求字段啥的我們也都清楚,唯獨有一點,就是請求余票的時候,返回的是html節(jié)點代碼,而不是我們預期的json數據,這樣就有個麻煩,我們沒辦法一目了然的明白他余票的時候是如何顯示的
所以我們只能通過 chrome 進行調試,才能得出他是如何判斷余票的。
我們找個記事本,記錄下信息,記錄的內容有:
- 請求余票接口和購票接口的
url地址 cookie信息 各自的request參數字段user-Agent信息- 各自的
response返回內容
2-5 設置chrome
有以上信息后,我們就可以開始用chrome調試了, 首先打開 More tools / Network conditions

把 user-Agent 填入到 Custom里面

2-6 Charles抓包本地請求
因為我們要把獲取到的cookie填入到chrome里面,以我們的用戶身份去訪問網頁,所以我們需要在請求目標地址的時候,改包修改cookie
首先我們需要開啟 macOS Proxy ,抓包我們的http請求

打開chrome訪問目標網址,我們可以看到 Charles
上已經抓包到了我們訪問的目標url地址,然后給目標url地址打上斷點,方便調試

然后再次訪問,這時候斷點就生效了,彈出一個tab名為 break points ,可以看到之所以我們還是不能訪問到目標網址,是因為 sessionId 不對,所以我們把抓取到的 cookie 在填入到里面,點擊 execute

這時候,能夠正確跳到目標頁面了。

大概看了下他整體布局,和 jQuery 代碼 CSS
代碼,特別是日歷表那一塊
審查了下元素發(fā)現(xiàn):
小方塊的結構為:
<td class="b"> <span>這里為日期</span> <span>如果有余票則顯示余票數量</span> </td>
- td的樣式名為
a代表不可選 - 樣式名為
e代表已滿 - 樣式名為
d代表已購 - 樣式名為
b則是我們要找的,代表可選,也就是有余票
到這一步,整個購票流程就清楚了
到時候我們通過Node.js請求的時候,處理返回數據,用正則去判斷是否有余票的class名 b ,有余票的話,在獲取div里面的余票數量內容就Ok了
2.Node.js 請求目標接口
2-1 分析需要開發(fā)的功能點
寫代碼之前我們需要想好功能點,我們需要什么功能:
- 請求余票接口
- 定時請求任務
- 有余票則自動請求購票接口下訂單
- 調用騰訊云短信api接口發(fā)送短信通知
- 多個用戶搶票功能
- 搶某個日期的票
首先 mkdir ticket 創(chuàng)建名為ticket的文件夾,接著 cd ticket 進入文件夾 npm init 一路瞎幾把回車也無妨。 下面開始安裝依賴,根據上面的功能需求,我們大概需要:
1.請求工具,這里看個人習慣,你也可以使用原生的 http.request ,我這里選擇用的是 axios ,畢竟 axios 在node端底層也是調用 http.request
cnpm install axios --save
2.定時任務 node-schedule
cnpm install node-schedule --save
3.node端選擇dom節(jié)點工具 cheerio
cnpm install cheerio --save
4.騰訊發(fā)短信的依賴包 qcloudsms_js
cnpm install qcloudsms_js
5.熱更新包,諾豆的媽媽, nodemom (其實不用也可以)
cnpm install nodemom --save-dev
2-2開發(fā)請求余票接口
接著 touch index.js 創(chuàng)建核心js文件,開始編碼:
首先引入所有依賴
const axios = require('axios')
const querystring = require("querystring"); //序列化對象,用qs也行,都一樣
var QcloudSms = require("qcloudsms_js");
var cheerio = require('cheerio');
var schedule = require('node-schedule');
然后我們先定義請求參數,來一個obj
var obj = {
data: {
lineId: 111130, //路線id
vehTime: 0722, //發(fā)車時間,
startTime: 0751, //預計上車時間
onStationId: 564492, //預定的站點id
offStationId: 17990,//到站id
onStationName: '寶安交通運輸局③', //預定的站點名稱
offStationName: "深港產學研基地",//預定到站名稱
tradePrice: 0,//總金額
saleDates: '17',//車票日期
beginDate: '',//訂票時間,滯空,用于抓取到余票后填入數據
},
phoneNumber: 123123123, //用戶手機號,接收短信的手機號
cookie: 'JSESSIONID=TESTCOOKIE', // 抓取到的cookie
day: "17" //定17號的票,這個主要是用于搶指定日期的票,滯空則為搶當月所有余票
}
接著聲明一個名為 queryTicket 的類,為啥要用類呢,因為基于第五個需求點,多個用戶搶票的時候,我們分別 new 一下就行了,
同時我們希望能夠記錄請求余票的次數,和當搶到票后自動停止查詢余票得操作,所以給他加上個計數變量 times 和是否停止的變量,布爾值 stop
編寫代碼:
class QueryTicket{
/**
*Creates an instance of QueryTicket.
* @param {Object} { data, phoneNumber, cookie, day }
* @param data {Object} 請求余票接口的requery參數
* @param phoneNumber {Number} 用戶手機號,短信需要用到
* @param cookie {String} cookie信息
* @params day {String} 某日的票,如'18'
* @memberof QueryTicket 請求余票接口
*/
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; //記錄次數
var stop = false //通過特定接口才能修改stop值,防止外部隨意串改
this.getStop = function () { //獲取是否停止
return stop
}
this.setStop = function (ifStop) { //設置是否停止
stop = ifStop
}
}
}
下面開始定義原型方法,為了方便維護,我們把邏輯拆分成各個函數
class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代碼...
}
init(){}//初始化
handleQueryTicket(){}//查詢余票的邏輯
requestTicket(){} //調用查詢余票接口
handleBuyTicket(){} //購票相關邏輯
requestOrder(){}//調用購票接口
handleInfoUser(){}//通知用戶的邏輯
sendMSg(){} //發(fā)短信接口
}
所有數據都是基于查詢余票的操作,因此我們先開發(fā)這部分功能
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++ //計數器,記錄請求查詢多少次
var str = res.data.replace(/\\/g, "") //格式化返回值
var $ = cheerio.load(`<div class="main">${str}</div>`) // cheerio載入查詢接口response的html節(jié)點數據
let list = $(".main").find(".b") //查找是否有余票的dom節(jié)點
// 如果沒有余票,打印出請求多少次,然后返回,不執(zhí)行下面的代碼
if (!list.length) {
console.log(`用戶${this.phoneNumber}:無票,已進行${this.times}次`)
return
}
// 如果有余票
list.each((idx, item) => {
var str = $(item).html() //str這時格式是<span>21</span><span>&$x4F59;0</span>
//最后一個span 的內容其實"余0",也就是無票,只不過是被轉碼了而已
//因此要在下一步對其進行格式化
var 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(){} //發(fā)短信接口
}
來解釋下那行正則, cheerio 抓取到的dom是長這樣的,第一個 span 內容是日期,第二個是余票數量
所以我們要把它格式化變成這種數組,也就是 ticketList
2-3開發(fā)購票功能
首先我們在 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,//發(fā)車時間
startTime,//預計上車時間
onStationId,//上車的站臺id
offStationId //到站的站臺id
} = this.data // 初始化的數據
let station = `${onStationName}-${offStationName}` //站點,發(fā)短信時候用到:"寶安交通局-深港產學研基地"
let dateStr = ""; //車票日期
let tickAmount = "" //總張數
ticketList.forEach(item => {
dateStr = dateStr + `${year}-${month}-${item.day},`
tickAmount = tickAmount + `${item.ticketLeft}張,`
})
var buyTicket = {
lineId,//線路id
vehTime,//發(fā)車時間
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數據,是否購票成功等等
//把發(fā)短信所需要數據都要傳入
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(){} //發(fā)短信接口
}
到這里,查詢余票,購票這兩個核心操作已經完成。
目前還剩下,如何通知用戶是否購票成功。
之前我嘗試過使用qq郵箱的smtp服務,搶票成功后發(fā)送郵件通知,但是我覺得吧,并不好用,主要是我沒有打開郵箱的習慣,沒網也收不到,所以,并沒有采納這個方案。
加上之前我注冊過企業(yè)認證的公眾號,騰訊云免費送了我1000條短信通知,而且短信也比較直觀,所以我這里就安裝騰訊云的SDK,部署了一套發(fā)短信的功能。
2-4騰訊云短信的相關內容
其實看看文檔就行了,我也是copy文檔,注意看短信單發(fā)那部分
cloud.tencent.com/document/pr…
如果跟我一樣有企業(yè)認證的話,看快速入門這里就行了,一步步跟著操作
看下短信正文, {Number}
這些里面的數字是變量。
就是說短信的模板是固定的,但是里面有 {Number} 的內容可以自定義
調用的時候,里面的數字對應著傳過去的參數數組序號,{1}代表數組[0]參數,以此類推
提交審核,審核一般很快就通過,也就是幾十萬毫秒吧

2-5 開發(fā)通知功能
class QueryTicket{
constructor({ data, phoneNumber, cookie, day }) {
//constructor代碼...
}
//初始化
async init(){
let ticketList = await this.handleQueryTicket()
//如果有余票
if (ticketList.length) {
//把余票傳入購票邏輯方法,返回短信通知所需要的數據
let resParse = await this.handleBuyTicket(ticketList)
//執(zhí)行通知邏輯
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,//出發(fā)時間
phoneNumber: this.phoneNumber,//手機號
})
//如果發(fā)信成功,則不再進行搶票操作
if (res.result === 0 && res.errmsg === "OK") {
this.setStop(true)
} else {
//失敗不做任何操作
console.log(res.errmsg)
}
} else {
//失敗不做任何操作
console.log(resParse['returnInfo'])
}
}
//發(fā)短信接口
sendMSg(){
let { dateStr, tickAmount, station, lineName, phoneNumber, startTime, tradePrice } = obj
var appid = 140034324; // SDK AppID 以1400開頭
// 短信應用 SDK AppKey
var appkey = "asdfdsvajwienin23493nadsnzxc";
// 短信模板 ID,需要在短信控制臺中申請
var templateId = 7839; // NOTE: 這里的模板ID`7839`只是示例,真實的模板 ID 需要在短信控制臺中申請
// 簽名
var smsSign = "測試短信"; // NOTE: 簽名參數使用的是`簽名內容`,而不是`簽名ID`。這里的簽名"騰訊云"只是示例,真實的簽名需要在短信控制臺申請
// 實例化 QcloudSms
var qcloudsms = QcloudSms(appid, appkey);
var ssender = qcloudsms.SmsSingleSender();
// 這里的params就是短信里面可以自定義的內容,也就是填入{1}{2}..的內容
var 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)
}
});
})
}
}
如果發(fā)信成功,返回 result:0
到這里,大部分需求已經完成了,還剩下一個定時任務
2-6 定時任務
也聲明一個類,這里我們用到的是 schedule
// 定時任務
class SetInter {
constructor({ timer, fn }) {
this.timer = timer // 每幾秒執(zhí)行
this.fn = fn //執(zhí)行的回調
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秒執(zhí)行一次
// [1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51, 56]
}
init() {
schedule.scheduleJob(this.rule, () => {
this.fn() // 定時調用傳入的回調方法
});
}
}
2-7 多個用戶搶票
假設我們有兩個用戶要搶票,所以定義兩個obj,實例化下 QueryTicket 類
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"
}
var 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: ""
}
var ticket = new QueryTicket(obj) //用戶1
var ticket2 = new QueryTicket(obj2) //用戶2
new SetInter({
timer: 1, //每秒執(zhí)行一次,建議5秒,不然怕被ip拉黑,我這里只是為了方便下面截圖
fn: function () {
[ticket,ticket2].map(item => { //同時進行兩個用戶的搶票
if (!item.getStop()) { //調用實例的原型方法,判斷是否停止搶票,如果沒有則繼續(xù)搶
item.init()
} else { // 如果搶到票了,則不繼續(xù)搶票
console.log('stop')
}
})
}
})
node index.js 運行下,跑起來了
如果他搶到票的話,我就會收到短信通知:
打開手機,看下訂單信息
搞定,收工
寫在最后
其實可以在此基礎上還能添加更多功能,比如直接抓取登錄接口獲取cookie,指定路線搶票,還有錯誤處理啊啥的
因為這個只是我個人使用,所以只有這點功能就足夠了。
如果想把它做成一個完整的項目,建議使用ts加持 ,關于ts我推薦閱讀這篇JD前端寫的文章
希望各位能有所收獲
總結
以上所述是小編給大家介紹的Node.js 實現(xiàn)搶票小工具 & 短信通知提醒功能,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對腳本之家網站的支持!
如果你覺得本文對你有幫助,歡迎轉載,煩請注明出處,謝謝!
相關文章
Node.js實現(xiàn)大文件斷點續(xù)傳示例詳解
這篇文章主要為大家介紹了Node.js實現(xiàn)大文件斷點續(xù)傳示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11
NodeJS?Express使用ORM模型訪問關系型數據庫流程詳解
這篇文章主要介紹了NodeJS?Express使用ORM模型訪問關系型數據庫流程,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習吧2023-01-01
解決node-webkit 不支持html5播放mp4視頻的方法
本文給大家分享的是解決node-webkit 不支持html5播放mp4視頻的方法,其原因大概是因為node-webkit沒有購買mp4格式的專利授權,恩,我們來想個辦法來解決這個事情吧。2015-03-03

