iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 12
4

定義目標

我曾經接到一個案主的 case,他需要我寫隻爬蟲去爬租屋網站上的物件,其實這類的需求在平台或電子商務常常出現,那我們就以 591 來嘗試抓取所有物件挑戰看看。


實際探訪

一進入 591 網站就出現一個選擇縣市的 popup,預設為所在地點,當我們選了以後重新整理網頁,然後看到網址多帶了一個 region,應該是代表城市的意思,看起來瀏覽器會記錄我們所選擇的地點。

接著可以看到這個城市的所有物件列表,畫面的最下方有分頁物件。點擊分頁,發現他並沒有轉頁,那肯定是用前端 render,肯定是有 ajax api。

打開 dev tool 確認一下,確定是只有 XHR tab 有多新東西。這個 request 的 response 看起來就是這整頁的物件。

這個 response 其實有很多資訊,包含這個篩選的城市物件總筆數,還有每個物件的詳細資料。

再往下觀察,點選分頁後網址多了 firstRow 這個參數,直覺這個就是控制分頁起始的因素,因為第一頁是 0~29,所以第二頁起始就會是 30 開始。

進入研究之前我們先想一下,每次只能 request 30 筆,也就是說若總筆數是 9,535 那總共要發出 317 個 request 才能抓完這個城市所有物件,很顯然是個挑戰。

分解研究

我們很直覺得打開 postman 用 get request 試試看,可以很順利地得到結果。

然後我們嘗試改變 firstRow 抓取分頁試試看,一樣可以很順利的拿到結果。

最後我們試試看改變 region,但結果並沒有改變,可見這個 region 應該是個幌子,真正影響城市選擇的並不是他。

既然這樣,那就需要來交叉比對兩個不同城市的 request,兩者看起來基本上除了 region 外的參數沒有什麼不同,那肯定問題就在 header。

header 看起來最可疑的當然就是 cookie,而且剛剛一進網站就記住我的地點,可見 cookie 裡面絕對有城市資訊。

這時候我們可以嘗試用刪去法,一個一個來測試是否會有影響,最後發現 urlJumpIp 這個 cookie key 就是關鍵。


實作程式碼

搞定研究之後我們來想想程式流程,我們的步驟計畫分成下面幾步:

  1. 取得城市所有代碼
  2. 取得分頁數量
  3. 對每個城市分頁做 api request

不過因為整個抓取會太多,所以我們取花蓮當範例就好。

設定 cookie

因為 591 選擇程式是用 cookie 來做設定,所以我們需要先來設定 request 的 cookie。

var request = require('request');
var j = request.jar();
var url = 'https://rent.591.com.tw';

j.setCookie(request.cookie('urlJumpIp=23'), url);
request = request.defaults({jar: j});

getPagesCount function

接下來我們來取分頁數量,因為他的 api response 都有給總量,所以只需要將總量除以 30 就可以了。我們可以把 page 數量丟給 callback 就好了,但之後要做 async.map,所以我這邊會把分頁數量變成陣列。

function getPagesCount(callback){
  request('https://rent.591.com.tw/home/search/rsList', (err, res, body)=>{
    var result = JSON.parse(body)
    var pagesCount =Math.floor(result.records/30)
    callback(Array.from(new Array(pagesCount), (val, index) => index));
  })
}

getPage function

接下來我們來呼叫每個分頁的 api,取得 data 並將物件資料回傳。

function getPage(pageNumber, callback){
  request('https://rent.591.com.tw/home/search/rsList?firstRow='+pageNumber*30, (err, res, body)=>{
    var result = JSON.parse(body)
    callback(result.data.data)
  })
}

組合積木

所有元件都準備完了,那我們就可以用 async.map 來組裝邏輯了。

getPagesCount((pages)=>{
  async.map(pages, (page, callback)=>{
    getPage(page, (result)=>{
     callback(null, result);
   })
  }, (err, results)=>{
    console.log([].concat.apply([], results));
  })
})

完整程式碼

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

var j = request.jar();
var url = 'https://rent.591.com.tw';

j.setCookie(request.cookie('urlJumpIp=23'), url);
request = request.defaults({jar: j});

getPagesCount((pages)=>{
  async.map(pages, (page, callback)=>{
    getPage(page, (result)=>{
     callback(null, result);
   })
  }, (err, results)=>{
    console.log([].concat.apply([], results));
  })
})

function getPage(pageNumber, callback){
  request('https://rent.591.com.tw/home/search/rsList?firstRow='+pageNumber*30, (err, res, body)=>{
    var result = JSON.parse(body)
    callback(result.data.data)
  })
}

function getPagesCount(callback){
  request('https://rent.591.com.tw/home/search/rsList', (err, res, body)=>{
    var result = JSON.parse(body)
    var pagesCount =Math.floor(result.records/30)
    callback(Array.from(new Array(pagesCount), (val, index) => index));
  })
}

衍伸應用

抓取這類型的資料是很常見的行為,抓取一次也不是特別困難的事,但是在商業應用上,肯定都會是想要做到同步、即時更新,如此一來,有很多行為問題會需要考慮,例如 server request limit,每次抓取的時間過長,這些都會是另外研究的課題。


上一篇
PTT 表特版 API
下一篇
台鐵時刻表
系列文
爬蟲始終來自於墮性34

1 則留言

0
tomatoprince
iT邦見習生 0 級 ‧ 2019-04-13 18:32:48

你好 按照您的方法
將URL拿去request後得到的response是591網站已經過期的資訊
請問這樣的情形如果改變headers資訊有用嗎

我要留言

立即登入留言