iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 21
3

定義目標

有許多電商界的朋友都曾經向我詢問過一個需求,他們想要抓到粉絲頁的按讚或粉絲名單,但是可惜的,FB 不給的你不能要。不過他們都會再進階的問一個問題,能否抓到文章點讚的名單?基本上 FB graph api 是只允許抓自己粉絲頁的,但對爬蟲而言卻是「所見即所得」,今天我們的主題就來挑戰一下抓取 FB 某篇文章的按讚名單


實際探訪

這次我們來使用這篇文章來做,同時公關一下,這家草莓大福很有名,預約都排到三個月以後了。

首先進入網址後會看到這篇文章的主體,在文章下面,有按讚的總人數,點擊這個數字之後,我們就可以看到按讚的名單。同時我們發現,當我們點擊的時候,送出了一個 get request,裡面包含了許多 query 參數。在 response 裡面,我們若全部展開,會發現這個 request 將按讚名單的 html 夾在 jsmodsmarkup 裡面,在這個 html 除了包含按讚的名單外,也包含了下一個分頁的連結,也就是說,若我們能夠模擬出這個 request,那我們就能順利取得名單。

再來我們發現這個 request 其實只有第一頁,名單捲到最底部有一個「更多」的按鈕,按下去之後會讀取下一個分頁的名單。同時我們也發現,點擊「更多」之後,送出了一個 get request,裡面也包含了許多 query 參數,同時也在 response 裡面找到下一個分頁的按讚名單,不過這邊發現,資料的位置跟第一個 request 不同,這次的資料是放在 domops 裡面,名單的 html 放在 domops[0] 裡面,而再下一頁的連結放在 domops[1] 裡面。

若我們能搞定這兩個 request,就能取得這篇文章的按讚名單了。


分解研究

要取得所有按讚名單,那麼會拆解成以下四個步驟:

  1. 第一個分頁的 request
  2. 解析第一個分頁的 response
  3. 更多分頁的 request
  4. 解析更多分頁的 response

第一個分頁的 request

首先我們模擬一下這個 request,記得帶上 cookieuser-agent,確定是可以順利取得 response。

解析第一個分頁的 response

接下來我們將 response 的 json 擷取下來,並且把前面的贅字拿掉,再用 chrome 解析一下 json,然後將目標 html 建立一個網頁來觀察一下,確定是可以 select 到所有的名單,再確認一下下一個分頁的 url,也是可以拿到的。

更多分頁的 request

將上面取的 url 輸入 postman 試試看,結果發現並沒有辦法順利取得數值,兩相比對一下 network 裡面的網址,發現我們取得的下一頁網址少了些參數。經過測試之後,發現只要沒有 ft_ent_identifier__a 就不會成功,所以我們必須手動加上 ft_ent_identifier__a,這樣就能順利的得 response 了。

解析更多分頁的 response

我們一樣把 response 放到空的 html 去解析,確定可以 select 到所有的名單。再把更多分頁的 html 也解析,一樣能夠順利拿到 url,那整個過程應該就沒問題了。


實作程式碼

getActionList function

我們先來實作發出第一個 request 的 getActionList function,收一個 url 當作參數,然後將按讚名單傳給 callback。這邊比較要注意的是,因為我們取得更多連結的時候,要手動將 ft_ent_identifier__a 帶上,所以這邊我們會先去 pasre 一下參數傳進來的 url 中的 ft_ent_identifier,並在我們取得「更多」按鈕連結的時候將其加上,而若有「更多」的按鈕我們則會去 call getMoreList function,這個 function 我們等下會實作。

const parse = require('url-parse');

function getActionList(url , callback){

  var ft_ent_identifier = parse(url, true).query.ft_ent_identifier
  request(url, (err, res, body)=>{

      var data = JSON.parse(body.replace('for (;;);', ''))
      var html = data.jsmods.markup[0][1].__html
      var $ = cheerio.load(html)
      var list = $('._5j0e a').map((index, obj)=>{
        return {
            id: JSON.parse($(obj).attr('data-gt')).engagement.eng_tid,
            name: $(obj).text(),
            link: $(obj).attr('href'),
        }
      }).get()

      if($('.uiMorePager').length){
        var url = 'https://www.facebook.com' + $('.uiMorePager a').attr('href') + `&ft_ent_identifier=${ft_ent_identifier}&__a=1`
        getMoreList(url, (moreList)=>{
          callback(list.concat(moreList))
        })
      }else{
          callback(list)
      }
  })
}

getMoreList function

接下來實作取得更多連結的 request,這邊要注意的是 response 的名單和「更多」的按鈕在不同的位置。而若有「更多」的按鈕,那我們就遞迴呼叫 getMoreList 自己。

function getMoreList(url, callback){
  var ft_ent_identifier = parse(url, true).query.ft_ent_identifier
  request(url, (err, res, body)=>{
    var data = JSON.parse(body.replace('for (;;);', ''))
    var html = data.domops[0][3].__html;
    var $ = cheerio.load(html)
    var list = $('._5j0e a').map((index, obj)=>{
      return {
          id: JSON.parse($(obj).attr('data-gt')).engagement.eng_tid,
          name: $(obj).text(),
          link: $(obj).attr('href'),
      }
    }).get()

    if(data.domops[1][3]){
      var html = data.domops[1][3].__html;
      var $ = cheerio.load(html)
      var url = 'https://www.facebook.com' + $('.uiMorePager a').attr('href') + `&ft_ent_identifier=${ft_ent_identifier}&__a=1`
      getMoreList(url, (moreList)=>{
        callback(list.concat(moreList))
      })
    }else{
        callback(list)
    }
  })
}

積木組合

最後我們要將按讚數字的那個連結傳給 getActionList function,這樣就OK了。

var url = 'https://www.facebook.com/ufi/reaction/profile/dialog/?ft_ent_identifier=2051019961809577&av=1402351764&dpr=2&__asyncDialog=2&__user=1402351764&__a=1&__dyn=5V4cjLx2ByK5A9UkKHqAyqomzFEbFbGAdyemt94WqF1eq7UnGdwIhEpyEybGqK6qxeqaxu2GcyWDyubnyogyEnGi4FpeuUzxeAcU8UqDodEHzoK26aAUg-nDLzA5KcyF8O49ElwQUlByECii8yEqx6exu2m5HxirVe48G6EvG5FA5u59XgK5Ey4UlDBgS8x2i2eqaCzu5oV1yWLBx6695UCUZqBxeybaWzQQ25jUyq2mbLAAzUxa9BKazodotK8zoymf-Egy8Gdx69hEqAzULgSa-FpF-23Rh8Gmm8h63Cmh2ubJ2oC&__req=1l&__be=1&__pc=PHASED%3ADEFAULT&__rev=3544586&__spin_r=3544586&__spin_b=trunk&__spin_t=1513907702&ft[tn]=]%2BZ*F&ft[type]=44';

getActionList(url, (list)=>{
  console.log(list);
})

完整程式碼

const 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'
  }
});
const cheerio = require('cheerio');
const async = require('async');
const parse = require('url-parse');



var url = 'https://www.facebook.com/ufi/reaction/profile/dialog/?ft_ent_identifier=2051019961809577&av=1402351764&dpr=2&__asyncDialog=2&__user=1402351764&__a=1&__dyn=5V4cjLx2ByK5A9UkKHqAyqomzFEbFbGAdyemt94WqF1eq7UnGdwIhEpyEybGqK6qxeqaxu2GcyWDyubnyogyEnGi4FpeuUzxeAcU8UqDodEHzoK26aAUg-nDLzA5KcyF8O49ElwQUlByECii8yEqx6exu2m5HxirVe48G6EvG5FA5u59XgK5Ey4UlDBgS8x2i2eqaCzu5oV1yWLBx6695UCUZqBxeybaWzQQ25jUyq2mbLAAzUxa9BKazodotK8zoymf-Egy8Gdx69hEqAzULgSa-FpF-23Rh8Gmm8h63Cmh2ubJ2oC&__req=1l&__be=1&__pc=PHASED%3ADEFAULT&__rev=3544586&__spin_r=3544586&__spin_b=trunk&__spin_t=1513907702&ft[tn]=]%2BZ*F&ft[type]=44';

getActionList(url, (list)=>{
  console.log(list);
})


function getActionList(url , callback){

  var ft_ent_identifier = parse(url, true).query.ft_ent_identifier
  request(url, (err, res, body)=>{

      var data = JSON.parse(body.replace('for (;;);', ''))
      var html = data.jsmods.markup[0][1].__html
      var $ = cheerio.load(html)
      var list = $('._5j0e a').map((index, obj)=>{
        return {
            id: JSON.parse($(obj).attr('data-gt')).engagement.eng_tid,
            name: $(obj).text(),
            link: $(obj).attr('href'),
        }
      }).get()

      if($('.uiMorePager').length){
        var url = 'https://www.facebook.com' + $('.uiMorePager a').attr('href') + `&ft_ent_identifier=${ft_ent_identifier}&__a=1`
        getMoreList(url, (moreList)=>{
          callback(list.concat(moreList))
        })
      }else{
          callback(list)
      }
  })
}

function getMoreList(url, callback){
  var ft_ent_identifier = parse(url, true).query.ft_ent_identifier
  request(url, (err, res, body)=>{
    var data = JSON.parse(body.replace('for (;;);', ''))
    var html = data.domops[0][3].__html;
    var $ = cheerio.load(html)
    var list = $('._5j0e a').map((index, obj)=>{
      return {
          id: JSON.parse($(obj).attr('data-gt')).engagement.eng_tid,
          name: $(obj).text(),
          link: $(obj).attr('href'),
      }
    }).get()

    if(data.domops[1][3]){
      var html = data.domops[1][3].__html;
      var $ = cheerio.load(html)
      var url = 'https://www.facebook.com' + $('.uiMorePager a').attr('href') + `&ft_ent_identifier=${ft_ent_identifier}&__a=1`
      getMoreList(url, (moreList)=>{
        callback(list.concat(moreList))
      })
    }else{
        callback(list)
    }
  })
}


衍伸應用

老實說這種爬蟲對電商圈的朋友來說還挺有價值的,而通常懂電商的人不會懂這些技術,懂這些技術的人不會懂資料的價值,我一直期望能夠辦個黑客松主題是「電商x技術」,由電商圈的朋友們來許願,然後由技術夥伴在週末完成,我想這個會非常有價值。


上一篇
Facebook 好友生日列表
下一篇
Facebook 個人相簿
系列文
爬蟲始終來自於墮性34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言