iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 27
1
Software Development

爬蟲始終來自於墮性系列 第 31

使用模擬器做台鐵訂票爬蟲

定義目標

在一連串爬蟲的研究之後會體悟一點,玩爬蟲是不是都必須具備高深的 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 你必須注意的是點了點哪個欄位、輸入了什麼數值、點了什麼按鈕、等待什麼產生...等等。

若我們要成功地完成訂購,那麼我們會將流程切分成以下四個:

  1. 填入乘車資訊流程
  2. 處理驗證碼
  3. 送出驗證碼
  4. 取得訂票代號

填入乘車資訊流程

一樣進入 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 取出,這就是我們的訂票代號了。

完成研究後,我們就接著來進入實作吧。


實作程式碼

solveCaptcha function 和 reportCaptcha function

因為我這次還是一樣會用到 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()
    });
  })
}

init

接下來我們把 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 是我們最後的手段。


上一篇
改造台鐵訂票全自動
下一篇
NBA 即時比分
系列文
爬蟲始終來自於墮性34

尚未有邦友留言

立即登入留言