在一連串爬蟲的研究之後會體悟一點,玩爬蟲是不是都必須具備高深的 javascript 和網路概念的人才能玩?答案是否的,不過你具備這些能力的話,會更事半功倍。基本上爬蟲就是模擬人類的動作去執行,所以其實我們可以透過一些模擬器來做到同樣的動作。
在「工欲善其事,必先利其器(下)」這篇底下 Wolke 有提到 puppeteer 這套 google 原生的 High-level browser animation,這次我們就來延伸這幾天台鐵訂票的主題,用 simulator 來改寫訂票流程吧。
ps. 模擬器的本意其實是拿來做前端測試用的XD
先 demo 一下成果...
Puppeteer 的文件非常的完整,尤其是他的 api document,乾淨且清楚,也有許多範例和衍伸應用,所以對剛接觸的朋友應該會覺得非常的親切。
而台鐵的訂票網站,我們在其幾篇已經探訪過很多次了,這邊就不再多綴了,就讓我們一起進入研究部分吧。
做純 Javascript 的爬蟲跟 simulator 的爬蟲在探訪上其實很不一樣,js 的爬蟲要看的重點是在 network、request、response 這些,但 simulator 在探訪上是去紀錄人在流程上所操作的每個動作。
例如說我們要送個表單,對於 js request 你要注意的是 url、method、cookie、header、query string、form body...等等。而對比 simulator 你必須注意的是點了點哪個欄位、輸入了什麼數值、點了什麼按鈕、等待什麼產生...等等。
若我們要成功地完成訂購,那麼我們會將流程切分成以下四個:
一樣進入 http://railway.hinet.net/ctno1.htm ,然後會依序填入五個輸入項目,包含 #person_id
、#from_station
、#to_station
、#re_getin_date
、#train_no
,最後再點擊 button
來送出。
我們似乎看到一開始的時候驗證碼是破圖的狀況,然後才又加載,所以等一下我們必須先等待驗證碼出現在繼續動作。接著我們會將驗證碼 #idRandomPic
做一個 screenshot 存至檔案,然後透過昨天介紹的 DBC 來解析取得驗證碼。
接著我幫會將得到的驗證碼填入 #randInput
,然後點擊 #sbutton
來送出驗證碼。
最後我們會將 #spanOrderCode
的 innerHTML 取出,這就是我們的訂票代號了。
完成研究後,我們就接著來進入實作吧。
因為我這次還是一樣會用到 DBC,所以我們將昨天用到的 DBC solve 和 report function 一樣的撰寫出來。
function solveCaptcha() {
return new Promise(done => {
dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
captchaId = id
done(solution)
});
})
}
function reportCaptcha() {
return new Promise(done => {
dbc.report( captchaId, function(err) {
done()
});
})
}
接下來我們把 puppeteer 和 DBC 該引入的和該設定的處理一下。這邊注意到,我們在 puppeteer.launch 的時候傳了一個 headless: false
參數,這表示我們要用 debug 模式,他就會出現 chrome 的視窗,而在完成後我們拿掉這個參數,那麼就不會出現 chrome 的視窗了。
const puppeteer = require('puppeteer');
const DeathByCaptcha = require("deathbycaptcha");
var dbc = new DeathByCaptcha("DBC 帳號", "DBC 密碼");
var captchaId;
(async () => {
const browser = await puppeteer.launch({
headless: false
})
const page = await browser.newPage()
// 接下來的操作流程
await browser.close()
})()
接著我們來處理乘車資訊的填入,首先先 goto http://railway.hinet.net/ctno1.htm ,然後依序填入所有欄位,最後在點擊 button 送出。
await page.goto('http://railway.hinet.net/ctno1.htm')
await page.type('#person_id', 'A134405743')
await page.type('#from_station', '175')
await page.type('#to_station', '185')
await page.select('#getin_date', '2018/01/01-04')
await page.type('#train_no', '105')
await page.click('button')
接著我們等待 #idRandomPic
的產生,然後將其 screenshot,接著把他交給 DBC 去解析。
await page.waitForSelector('#idRandomPic')
var image = await page.$('#idRandomPic')
await image.screenshot({
path: 'code.jpeg',
})
var code = await solveCaptcha()
console.log('-> 取得驗證碼 => ' + code)
接著輸入驗證碼再點擊 button 送出。
await page.type('#randInput', code)
await page.click('#sbutton')
最後我們等待 #spanOrderCode
的產生,然後用 evaluate 去取得 #spanOrderCode
的 innerHTML,這樣整個流程就完成了。
await page.waitForSelector('#spanOrderCode')
var orderNumber = await page.evaluate(el => el.innerHTML, await page.$('#spanOrderCode'));
console.log('-> 訂位代號 => ' + orderNumber)
const puppeteer = require('puppeteer');
const fs = require('fs');
const DeathByCaptcha = require("deathbycaptcha");
var dbc = new DeathByCaptcha("DBC 帳號", "DBC 密碼");
var captchaId;
(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('http://railway.hinet.net/ctno1.htm')
await page.type('#person_id', 'A134405743')
await page.type('#from_station', '175')
await page.type('#to_station', '185')
await page.select('#getin_date', '2018/01/01-04')
await page.type('#train_no', '105')
await page.click('button')
await page.waitForSelector('#idRandomPic')
var image = await page.$('#idRandomPic')
await image.screenshot({
path: 'code.jpeg',
})
var code = await solveCaptcha()
console.log('-> 取得驗證碼 => ' + code)
await page.type('#randInput', code)
await page.click('#sbutton')
await page.waitForSelector('#spanOrderCode')
var orderNumber = await page.evaluate(el => el.innerHTML, await page.$('#spanOrderCode'));
console.log('-> 訂位代號 => ' + orderNumber)
if (!orderNumber) {
console.log('-> 訂票失敗');
console.log('-> 回報 DBC 破解失敗');
await reportCaptcha()
}
await browser.close()
})()
function solveCaptcha() {
return new Promise(done => {
dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
captchaId = id
done(solution)
});
})
}
function reportCaptcha() {
return new Promise(done => {
dbc.report( captchaId, function(err) {
done()
});
})
}
不可否認的,透過 simulator 來做爬蟲其實是比較輕鬆的,但在效能上肯定不會比純 javascript 來處理得好,例如我們之前處理過的「台彩的銷售地點」,這就不適合用 simulator 來處理,若要好好的鑽研爬蟲,那麼還是需要把這些基本的網路能力給補齊。
不過有時候純 javascript 要鑽研一個行為流程是要花很多時間和精力的,像是這次台鐵的訂票,雖然我們前幾篇有用 js 處理,但那是因為他有分成兩種訂票 cookie,我大概花了兩天的時間,都還沒辦法搞定另外一個有加密混淆的,所以在無法順利研究 request 的狀況,simulator 是我們最後的手段。