我曾經接到一個案主的 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 就是關鍵。
搞定研究之後我們來想想程式流程,我們的步驟計畫分成下面幾步:
不過因為整個抓取會太多,所以我們取花蓮當範例就好。
因為 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});
接下來我們來取分頁數量,因為他的 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));
})
}
接下來我們來呼叫每個分頁的 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,每次抓取的時間過長,這些都會是另外研究的課題。
你好 按照您的方法
將URL拿去request後得到的response是591網站已經過期的資訊
請問這樣的情形如果改變headers資訊有用嗎
我猜大概是 ajax 多了 CSRF token 的保護, 如果是 postman 工具的話, 記得把 CSRF token 資訊也貼到 cookie 內.