台鐵網站一直都很古老,訂票網站更是落後,看了一下似乎有 app 做出一鍵訂票的功能,但好像後續又被台鐵擋住(?)。那麼作為爬蟲,理應是人工能做的流程爬蟲就幾乎能做,而我們今天的主題就來走一下台鐵的訂票流程吧。
先 demo 一下成果...
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
台鐵的網站位置在 http://railway.hinet.net/ ,進去網站後點選「車次訂單程票」,然後再輸入身分證字號等資訊,接著會到輸入驗證碼的頁面,輸入完驗證碼送出就完成訂票流程。
ps. 這次的過程當中會有用到身分證,是使用身分證產生器來處理。
這邊有觀察到一點,當點擊連結的時候,畫面內容會切換,但網址並沒有改變,看起來似乎像是前端 render!!?但觀察過後其實不是...是古老的 iframe,那我們就能直接使用 iframe 的網址當作起始就可以了。
接下來看看填寫表單頁面,網址是 http://railway.hinet.net/ctno1.htm ,填入之後會送出一個 post request,form 裡面有我們所填入的資料。
然後我們需要填入驗證碼,這部分肯定就是爬蟲會挺難處理的其中一個步驟,我們先手動輸入驗證碼,然後觀察他送出了一個 post request,內容包含驗證碼和剛剛填入的訂單資訊。
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
這樣看起來,我們只需要搞定三個 request,包含一開始的 init get request,一個輸入訂單的 post request,一個包含驗證碼的 post request。
要走自動化訂票流程,我們要解決以下三個步驟:
照正常的流程走,一進到台鐵的網站就會 set cookie,而在送出訂票資訊的時候也會取得驗證碼,照正常的邏輯,驗證碼一定跟 cookie 有關,所以我們勢必要先模擬出 cookie,測試看看直接 request 台鐵首頁,確定 header 有 set cookie 就可以了。
在取得 cookie 後,我們就用 postman 來測試看看送出訂票資訊,這邊一開始送出是失敗的,因為台鐵網站的 post 都有檢查 request 的 referer,所以我們在 header 加上 referer 就可以成功送出了。
接著來看看畫面中的驗證碼的位置,是在 #idRandomPic
裡面,那就回到 response 裡面找找看,可以順利找到,但發現他是夾在 noscript
tag 裡面,所以等下實作的時候,直接 select 是取不到的,要額外把這個 html append 出來。
接著我們試著來取得驗證碼看看,可以順利取得驗證碼。
接著我們來試著模擬一下包含驗證碼的訂票 request,記得帶上 cookie 和 referer,就能夠順利取得訂票代號了。
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
這次我們的 async 步驟會比較多,所以這次我們使用 promise 來實作。
我們這次因為會先手動輸入驗證碼,所以來一個好玩的套件,能將圖片輸出至 console,在過程當中我們會用到,有興趣的朋友可以看看 console-jpeg 。
因為這整段步驟我們都需要使用 cookie,所以直接在 request require 就設定好 defaults,另外我們會使用 NSC_BQQMF=ffffffffaf121a1e45525d5f4f58455e445a4a423660
這個預設 cookie,在衍伸應用會補充。
另外我們也先準備好我們要訂購的車票資訊。
const request = require('request').defaults({
jar: true,
headers: {
cookie: 'NSC_BQQMF=ffffffffaf121a1e45525d5f4f58455e445a4a423660',
}
})
var reservationInfo = {
person_id: 'A134405743', // 這是 fake 身分證
from_station: '175',
to_station: '185',
getin_date: '2018/01/01-06',
train_no: '105',
order_qty_str: '1',
returnTicket: '0',
}
這部分我們只需要送出一個 get request 就可以了。
function initCookie() {
return new Promise(done => {
request('http://railway.hinet.net', (err, res, body) => {
done()
})
})
}
我們模擬送出訂購資訊的 post request,然後在 response 中取得驗證碼圖片的 url,這邊注意到,因為圖片的 don 是放在 noscript
裡面,所以我們必須先把內容取出 append 到 body,才能夠直接 select 到圖片物件。
取完圖片路徑後,再用 request 將圖片載到我們的資料夾裡面。
function fillInfo() {
return new Promise(done => {
var options = {
url: 'http://railway.hinet.net/check_ctno1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/ctno1.htm',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
$('body').append($('table noscript').html())
request('http://railway.hinet.net/' + $('#idRandomPic').attr('src')).pipe(fs.createWriteStream('code.jpeg')).on('close', done)
})
})
}
因為我們還沒有導入自動辨識圖片的功能,所以必須人工辨識圖片再手動輸入驗證碼,這部分就是讓 console 等待使用者的輸入,我們使用 prompt 來取得輸入。
function getCodeFromConsole() {
return new Promise(done => {
prompt.start();
prompt.get(['code'], function(err, result) {
done(result.code)
});
})
}
最後我們在送出包含驗證碼的訂購資訊,取得 response 之後再將 #spanOrderCode
選取出就可以了。
function takeOrder(code) {
return new Promise(done => {
reservationInfo.randInput = code
var options = {
url: 'http://railway.hinet.net/order_no1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/check_ctno1.jsp',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
done($('#spanOrderCode').text())
})
})
}
這邊我們使用 promise then 來組合所有的流程邏輯,先 initCookie,然後做 fillInfo,取得圖片之後,我們呼叫 consoleJpeg 將圖片顯示在 console,然後由使用者手動輸入驗證碼,最後下訂單成功取得訂單號碼。
initCookie()
.then(() => {
return fillInfo()
})
.then(() => {
consoleJpeg.attachTo(console)
return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
})
.then(() => {
return getCodeFromConsole()
})
.then((code) => {
return takeOrder(code);
})
.then((orderNumber) => {
console.log('訂位代號 => '+orderNumber);
})
const request = require('request').defaults({
jar: true,
headers: {
cookie: 'NSC_BQQMF=ffffffffaf121a1e45525d5f4f58455e445a4a423660',
}
})
const cheerio = require('cheerio');
const consoleJpeg = require('console-jpeg');
const prompt = require('prompt');
const fs = require('fs');
initCookie()
.then(() => {
return fillInfo()
})
.then(() => {
consoleJpeg.attachTo(console)
return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
})
.then(() => {
return getCodeFromConsole()
})
.then((code) => {
return takeOrder(code);
})
.then((orderNumber) => {
console.log('訂位代號 => '+orderNumber);
})
var reservationInfo = {
person_id: 'A134405743', // 這是 fake 身分證
from_station: '175',
to_station: '185',
getin_date: '2018/01/01-06',
train_no: '105',
order_qty_str: '1',
returnTicket: '0',
}
function initCookie() {
return new Promise(done => {
request('http://railway.hinet.net', (err, res, body) => {
done()
})
})
}
function fillInfo() {
return new Promise(done => {
var options = {
url: 'http://railway.hinet.net/check_ctno1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/ctno1.htm',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
$('body').append($('table noscript').html())
request('http://railway.hinet.net/' + $('#idRandomPic').attr('src')).pipe(fs.createWriteStream('code.jpeg')).on('close', done)
})
})
}
function getCodeFromConsole() {
return new Promise(done => {
prompt.start();
prompt.get(['code'], function(err, result) {
done(result.code)
});
})
}
function takeOrder(code) {
return new Promise(done => {
reservationInfo.randInput = code
var options = {
url: 'http://railway.hinet.net/order_no1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/check_ctno1.jsp',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
done($('#spanOrderCode').text())
})
})
}
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
*** 訂票成功要記得取消掉 ***
台鐵的訂票似乎有兩種不同模式的網頁,長得一樣,但內容 js 完全不同,兩種判別方式也不同,今天介紹這只是其中一種,而你可能會隨機遇到一種(難道是傳說中的 AB test??,肯定是我想太多,長得一樣是要 test 什麼)
若要肯定的執行其中一種,那麼可以透過 cookie 將方式固定,所以我們 init 那段就加了 cookie: 'NSC_BQQMF=ffffffffaf121a1e45525d5f4f58455e445a4a423660'
來固定購票方式。