iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 16
0

定義目標

我曾經接過一個 case,案主的需求是他想監測競業對手商店的價格,然後根據對手的價格去調整自己商品的價格,打價格戰。他之前的作法就是 hire 一個工讀生每天都去掃一遍對手的商城,一樣一樣的用 excel 記錄下來,然後再一樣一樣的調整自家商品。整個流程下來,這位工讀生大概就要花半天在這件事情上面,那我們今天就來試著解決這件事情。

ps.我在網路上隨便找的,就以電器批發來做 demo。


實際探訪

進到 Yahoo 超級商城的電商頁面,若要抓取全部商品,肯定就是要把所有分頁都走過一遍,然後每個分頁抓取上面的產品清單,包含商品的名稱、圖片、價格、連結等。

接著觀察到他每一分頁會陳列 48 個商品,然後按下面分頁選單後會跳頁,網址參數有個關鍵的 b=48 ,在按下一個分頁,這個參數變成 b=96,我合理猜測這個 b 是 begin 的意思,這個分頁會是第 96 個商品開始陳列的意思。

探訪結束,也就是說只要能爬完這全部分頁,應該就可以抓完所有商品。


分解研究

所以我們可以把步驟定義成以下兩個動作:

  1. 取得分頁數量
  2. 抓取所有分頁商品

取得分頁數量

我們可以抓取畫面上的多少比數符合結果,這個數字目前是 908,也就是說總共會有 908 個商品。接下來是數學問題,一頁最多 48 個,908 那麼 908 個會是幾個分頁呢?908/48 = 18.91,也就是說會有 19 個分頁。那根據網址參數,只需要將分頁減一在乘上 48 就是參數 b 的 value,我們用 postman 來測試其中一個分頁看看,確定是可以拿到資料那就沒問題了。

抓取所有分頁商品

接下來我們做 selector 來抓取這個分頁的商品列表。這邊發現產品列表的 html 格式挺特別的,他不是一個產品在一個 dom 裡面,而是分成三個 tr,這我也是醉了...。好在我們觀察到可以用 tr.border 來取得每個項目的分界,那應該就能抓取了,確定可以順利抓取後就能進入實作了。


實作程式碼

getTotal function

我們先實作取得總數和分頁的 function,取得後將分頁陣列丟給 callback。

function getTotal(callback){
  request('https://tw.mall.yahoo.com/search?sid=jiaying5328', (err, res, body)=>{
    var $ = cheerio.load(body)
    var total = $('#ypssnav strong').text()
    var pages = Math.ceil(total/48)
    callback(Array.from(new Array(pages), (val, index) => index))
  })
}

getPageProducts function

接下來我們來實作每個分頁取得分頁產品的 function,並將產品清單回傳 callback。

function getPageProducts(page, callback) {
  request(`https://tw.mall.yahoo.com/search?sid=jiaying5328&b=${page * 48}`, (err, res, body) => {
    var $ = cheerio.load(body)
    var products = $('tr.border').map((index, obj) => {
      return {
        title: $(obj).prev().prev().find('.title').text(),
        image: $(obj).prev().prev().find('img').attr('src'),
        price: $(obj).prev().find('em').text(),
        link: $(obj).prev().prev().find('.title a').attr('href'),
      }
    }).get()
    callback(products)
  })
}

積木組合

最後我們來處理整個流程邏輯,先取得分頁,然後用 async.map 來跑 getPageProducts,最後再將 array 扁平化,這樣就大功告成了。

getTotal((pages) => {
  async.map(pages, (page, callback) => {
    getPageProducts(page, (products) => {
      callback(null, products)
    })
  }, (err, results) => {
    console.log([].concat.apply([], results));
  })
})

完整程式碼

const request = require('request');
const cheerio = require('cheerio');
const async = require('async');


getTotal((pages) => {
  async.map(pages, (page, callback) => {
    getPageProducts(page, (products) => {
      callback(null, products)
    })
  }, (err, results) => {
    console.log([].concat.apply([], results));
  })
})

function getTotal(callback) {
  request('https://tw.mall.yahoo.com/search?sid=jiaying5328', (err, res, body) => {
    var $ = cheerio.load(body)
    var total = $('#ypssnav strong').text()
    var pages = Math.ceil(total / 48)
    callback(Array.from(new Array(pages), (val, index) => index))
  })
}

function getPageProducts(page, callback) {
  request(`https://tw.mall.yahoo.com/search?sid=jiaying5328&b=${page * 48}`, (err, res, body) => {
    var $ = cheerio.load(body)
    var products = $('tr.border').map((index, obj) => {
      return {
        title: $(obj).prev().prev().find('.title').text(),
        image: $(obj).prev().prev().find('img').attr('src'),
        price: $(obj).prev().find('em').text(),
        link: $(obj).prev().prev().find('.title a').attr('href'),
      }
    }).get()
    callback(products)
  })
}


衍伸應用

取得商品清單只是整個動作的其中一環,之後還有比對商品、修改價格等動作。不過掌握了爬蟲之後,這些動作都可以自動化,可以將一個人花半天的時間縮短為五分鐘,而我想這就是一種成就感,能多出來的人力應該要用在更重要的事情上,而能做到這點的工程師,也才會真正突顯其價值。


上一篇
日幣匯率訂閱
下一篇
2017/12/19 23:46:00 原始數據統計 (json)
系列文
爬蟲始終來自於墮性34

尚未有邦友留言

立即登入留言