iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 26
1

定義目標

昨天我們實作了台鐵半自動台鐵訂票,雖然已經大幅減少我們人工流程的時間,但肯定不是完美,我們今天就來將驗證碼的部分也自動化。

先 demo 一下今天結果。


實際探訪

台鐵使用的驗證碼其實算是挺簡單的,是利用 session 來產生一張數字變形和干擾的圖片,然後讓使用者辨別數字,在將數字與 session 一起驗證。

而一般要解決 captcha,我們要不是去找出他所使用的加密演算法,要不就是尋找流程漏洞,而最後、最直接的辦法就是與他正面對決。一些夥伴熟悉 AI、圖形辨識的,可能會使用一些 opensource 的圖形辨識工具,然後再 training 出可以辨識的 model,不過小弟在那個領域外行,所以我就直接使用最傳統的做法,花錢解決XDD

這次的我們使用第三方的 service 對決驗證碼,我所使用的是 deathbycaptcha(以下簡稱 DBC),每次的費用大概是 NTD 0.05 元,算是挺經濟的,我自好幾年前買的 5000 個 request,到現在都還沒用完就是。

坊間還有許多其他類似的服務和工具,也歡迎有研究的夥伴一起留言討論。


分解研究

我們可以先測試一下 DBC 能不能正常解決台鐵的驗證碼,可以直接到 DBC 的網站註冊帳號,然後應該可以 upload 圖片,我們將台鐵的驗證碼下載下來後上傳試試看,確認可以順利破解,不過破解的時間不算快,但還能接受就是。

接下來我們可以看一下 DBC 所提供的破解 api,他還有提供很許多不同語言的 library,但沒有 javascript,不過下面有提到可以自己 call api,所以最不濟我們就自幹。但是強大的 npm 裡面肯定是已經有社群夥伴撰寫,我們搜尋一下,確實是有的,然後就能夠進入程式碼實作了。


實作程式碼

我們這次是改寫昨天的程式碼,昨天在處理驗證碼的步驟是:

  1. 下載圖片
  2. 將圖片顯示於 console
  3. 人工輸入驗證碼

那我們今天要把步驟改成

  1. 下載圖片
  2. 呼叫 dbc.solve

solveCaptcha function

我們先來撰寫 solveCaptcha,主要就是 call dbc.solve 然後將結果傳給 callback。

function solveCaptcha() {
  return new Promise(done => {
    dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
      console.log('驗證碼 => '+solution);
      done(solution)
    });
  })
}

改寫流程

我們先 mark 原本顯示圖片於 console 和接受使用者 input 的部分,然後將 solveCaptcha 加進去,這樣就改寫完成了。

initCookie()
  .then(() => {
    console.log('-> 取得驗證碼');
    return fillInfo()
  })
  
  // .then(() => {
  //   consoleJpeg.attachTo(console)
  //   return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
  // })
  // .then(() => {
  //   return getCodeFromConsole()
  // })
  
  .then(() => {
    console.log('-> 等待 DBC 破解驗證碼');
    return solveCaptcha()
  })
  .then((code) => {
    console.log('-> 執行訂票');
    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 fs = require('fs');
const DeathByCaptcha = require("deathbycaptcha");
var dbc = new DeathByCaptcha("DBC 帳號", "DBC 密碼");


initCookie()
  .then(() => {
    console.log('-> 取得驗證碼');
    return fillInfo()
  })
  
  // .then(() => {
  //   consoleJpeg.attachTo(console)
  //   return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
  // })
  // .then(() => {
  //   return getCodeFromConsole()
  // })
  
  .then(() => {
    console.log('-> 等待 DBC 破解驗證碼');
    return solveCaptcha()
  })
  .then((code) => {
    console.log('-> 執行訂票');
    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 solveCaptcha() {
  return new Promise(done => {
    dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
      console.log('驗證碼 => '+solution);
      done(solution)
    });
  })
}

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())
    })
  })
}


衍伸應用

DBC 的破解率雖然很高,但並不是百分之百成功,所以我們應該要再加上失敗時候的回報機制,這樣才能避免跑自動流程失敗的時候一直在浪費資源。

另外目前 DBC 也能夠破解 reCAPTCHA,但我自己是還沒有嘗試過,也挖坑給各位去嘗試挑戰看看。


上一篇
台鐵(半)自動訂票
下一篇
使用模擬器做台鐵訂票爬蟲
系列文
爬蟲始終來自於墮性34

尚未有邦友留言

立即登入留言