iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 22
2

定義目標

最近因為要結婚了,所以有接觸婚攝這類的事情,然後發現要跟攝影師討論照相風格是一件麻煩的事情,若不是很能掌握照相風格的,就很難去描述你想要的那種風格,因此我們都會藉由挑選範例照片來輔助描述。

不過照篇挑選也是要做註記,所以自己寫了一個挑選工具,自動去下載一些有名的攝影師作品,然後只要動動手指,往上或往下標記喜歡或不喜歡即可。

而那時候遇到了一個問題,就是 fb graph api 只允許抓取粉絲頁的照片,個人版的照片是抓不到的,那我們今天的主題就來抓取個人版照片吧。


實際探訪

我們這次來用鯊魚個人網站的婚紗相簿來 demo,打開網頁之後,就會看到照片,往下捲動到最底下之後,會動態出現更多照片。

應該有注意到,在一開始開網頁的時候照片的讀取是直接出現的,也就是説在一開始的 request 他就會將 images 附在 response 的 html 裡面。而下方讀取更多是透過 ajax 呼叫 request,然後再使用 js 將取得的 response 內的照片附加在網頁上。我們可以透過 dev tools 來確認一下這點。

透過 dev tools 觀察後,發現第一個 request 的照片會放在 #u_0_89 裡面(不過這邊我在測試的時候,發現不同的 cookie 會有不同的 ID,所以比較正確的做法會是去取得 album_photos_pageletcontainer_id,但這邊就先不多贅),而且是註解起來的內容,也就說我們等等研究的時候需要先將這邊註解取消掉。

再來看一下取得更多的 request,發現送出去的 query string 裡面有一個很大串的 data,這裡面有個很有趣的數字 fetch_size,這個看起來有 pagination size 的感覺,等等我們研究時可以來測試看看。同時也注意到在 data 裡面有三個有關聯的參數 setprofile_idlast_fbidsetprofile_id 是由第一個 request 的網址延伸來的,而 last_fbid 則是第個 response 所給的最後一張圖片的 id。

經過探訪後,我們只要搞定第一網址的 request 和取得更多照片的 request 大概就沒有問題了。


分解研究

要取得所有個人相簿照片,那麼會拆解成以下四個步驟:

  1. 第一個 init 的 request
  2. 解析第一個分頁的 response
  3. 讀取分頁的 request
  4. 解析分頁的 response

第一個 init 的 request

接著我們模擬一下送出的 request,一樣用刪去法把不需要送出的 cookie 和 query string 都拿掉,確定是可以拿到結果。

解析第一個分頁的 response

接下來我們將取得的 html 程式碼去掉註解,然後丟到一個空的 html file 看看,同時去 select 圖片位置,這邊要注意到,因為圖片是放在 css 使用 background-image,這邊他有將值做 css escape,所以我們需要 replace 一些被 escape 的符號,確定是可以拿到圖片列表沒問題,不過等等我們實作的時候還要將最後一張圖片的 last_fbid 取出。

讀取分頁的 request

先用 postman 將 request 模擬一下,這邊要注意到,若 request headers 的 accept-encoding 有帶入 gzip, deflate, br,則 response 會回給我們 content-type 為 application/octet-stream stream buffer,所以我們這邊的 headers 記得把 accept-encoding 拿掉,然後確定可以取道結果就沒問題了。

另外我們也來看一下 query string 的 params,一樣刪去法把不必要的東西拿掉,這樣會剩下 data__aajaxpipe_fetch_stream,關鍵點會在 data,裡面要包含有剛剛我們拿到的 last_fbidsetprofile_id,另外我們也可以來調整一下 fetch_size,確定真的是 page size,那我們的分頁就不用 call 那麼多次了。

解析分頁的 response

接下來我們來解析一下分頁的 response,把前面的贅字拿掉後是一個 json format,而包含圖片的 html 會在 payload 裡面。那我們一樣打開一個空白的 html 來解析看看,確定可以拿到圖片就沒問題了。


實作程式碼

getInitImages function

先來製作 init 的 request finction,這個 function 會收 url 參數和 callback,然後先將等等分頁 request 要用的 albumId 先從 url parse 出來。接著我們去解析取得的 response,將註解拿掉,再將 css 的 escape 取代掉。取完圖片以後,我們再將最後一張圖片的 id 拿出來。最後我們在呼叫等等要實作的 getMoreImages function。

function getInitImages(url, callback){
  var albumId = parse(url, true).query.set

  request(url, (err, res, body)=>{
    var $ = cheerio.load(body)
    var html = $('#u_0_89').html()

    var $ = cheerio.load(html.replace('<!-- ','').replace(' -->',''))
    var images = $('._pq3').map((index, obj) => {
      return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\\\3a ', ':').replace(/\\\\3d /g, '=').replace('\\\\26 ', '&')
    }).get()

    var last = images[images.length-1].split('_')[1]
    getMoreImages(albumId, last, (moreImages)=>{
      callback(images.concat(moreImages))
    })
  })
}

getMoreImages function

取得更多圖片的 request function,會收 init request 丟過來的 albumId 和 last_id,然後我們要組合出要發出 request 的 query string,尤其是 data 的部分。拿到 response 之後,將前面贅字拿掉做 json parse,然後取得 payload 裡面的 html,就能夠將 image select 出來,最後在做 css escape 的 replace 就完成了。

function getMoreImages(albumId, last, callback){
  var profile_id = albumId.split('.')[3]
  var options = {
    method: 'GET',
    url: 'https://www.facebook.com/ajax/pagelet/generic.php/TimelinePhotosAlbumPagelet',
    qs: {
      data: `{"last_fbid":${last},"fetch_size":1000,"set":"${albumId}","__a":"1","profile_id":${profile_id}}`,
      __a: '1',
      ajaxpipe_fetch_stream: '1'
    },
  };

  request(options, function(error, response, body) {
    if (error) throw new Error(error);
    var data = JSON.parse(body.replace('for (;;);', ''))
    var $ = cheerio.load(data.payload)
    var images = $('._pq3').map((index, obj) => {
      return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\3a ', ':').replace(/\\3d /g, '=').replace('\\26 ', '&')
    }).get()
    callback(images);
  });
}

積木組合

先將 url 定義好,接下來直接呼叫 getInitImages 就可以了。

var url = 'https://www.facebook.com/SharkJiang.Wedding/media_set?set=a.1381648032142132.1073741831.100008908444745&type=3';
getInitImages(url, (images)=>{
  console.log(images);
})

完整程式碼

var request = require("request").defaults({
  headers: {
    'cookie': 'xxxxxxxxxx',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36',
  }
});
var cheerio = require("cheerio");
const parse = require('url-parse');

var url = 'https://www.facebook.com/SharkJiang.Wedding/media_set?set=a.1381648032142132.1073741831.100008908444745&type=3';
getInitImages(url, (images)=>{
  console.log(images);
})


function getInitImages(url, callback){
  var albumId = parse(url, true).query.set

  request(url, (err, res, body)=>{
    var $ = cheerio.load(body)
    var html = $('#u_0_89').html()

    var $ = cheerio.load(html.replace('<!-- ','').replace(' -->',''))
    var images = $('._pq3').map((index, obj) => {
      return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\\\3a ', ':').replace(/\\\\3d /g, '=').replace('\\\\26 ', '&')
    }).get()

    var last = images[images.length-1].split('_')[1]
    getMoreImages(albumId, last, (moreImages)=>{
      callback(images.concat(moreImages))
    })
  })
}

function getMoreImages(albumId, last, callback){
  var profile_id = albumId.split('.')[3]
  var options = {
    method: 'GET',
    url: 'https://www.facebook.com/ajax/pagelet/generic.php/TimelinePhotosAlbumPagelet',
    qs: {
      data: `{"last_fbid":${last},"fetch_size":1000,"set":"${albumId}","__a":"1","profile_id":${profile_id}}`,
      __a: '1',
      ajaxpipe_fetch_stream: '1'
    },
  };

  request(options, function(error, response, body) {
    if (error) throw new Error(error);
    var data = JSON.parse(body.replace('for (;;);', ''))
    var $ = cheerio.load(data.payload)
    var images = $('._pq3').map((index, obj) => {
      return $(obj).attr('style').match(/url\('(.+)'\)/)[1].replace('\\3a ', ':').replace(/\\3d /g, '=').replace('\\26 ', '&')
    }).get()
    callback(images);
  });
}


衍伸應用

FB graph api 很方便沒錯,但朕不給你的你不能要,所以其實有很多資料並無法從 api 取得,當遇到這個時候,就請發揮所見即所得的精神,自幹一個爬蟲 api 出來,那麼就能創造出別人無法創造的價值。


上一篇
Facebook 按讚名單
下一篇
7-11 超商門市爬取
系列文
爬蟲始終來自於墮性34

尚未有邦友留言

立即登入留言