iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
4
Software Development

爬蟲始終來自於墮性系列 第 8

好想工作室與他的參賽者們

定義目標

這次好想工作室大概有二十多位夥伴參加鐵人賽,為此我們也拉了一個 slack channel 來討論和分享彼此的心得,除了互相取暖外,我們也互相激勵。比賽也進入了第六天,每天看看其他人發的文章狀況也就成了每個人的 daily mission,但每次都要打開十多個人的主題也是挺煩人的,既然有麻煩事,當個網頁工程師自幹個爬蟲也是挺理所當然的,那我們今天的任務就是寫個爬蟲來抓取所有工作是參賽者的狀況吧!


實際探訪

要看一位參賽主題,肯定要先有 url,而這個 url 會長這樣 https://ithelp.ithome.com.tw/users/20107159/ironman/1325 ,接著我們進去參賽主題的網頁就能看到一系列相關數字,這個任務看起來挺簡單的,在一個頁面就能得到所有資訊。

在這個頁面裡面,會有興趣的大概是以下幾項:

  • 選手名稱
  • 參賽主題
  • 追蹤人數
  • 參賽天數
  • 參賽文章數
  • 訂閱人數
  • 文章列表
    • 文章主題
    • 發文時間
    • like 數
    • 留言數
    • 瀏覽數

分解研究

觀察頁面

觀察參賽主題
只需要一個 get request,那就不需要打開 postman 了,看來這個難度在於 dom 的選擇器上,我們必須在這個頁面裡面把我們需要的資選出來。用 dev tool 檢測一下參賽主題,發現他的 class 是 qa-list__title qa-list__title--ironman,看起來用的 class 都是具有辨識性的。

測試每個抓取的物件

測試每個物件
接下來再觀察一下其他的相關項目,每個都具有辨識性的 class,這樣很便利於我們抓取資料,看起來稍微複雜一點的大概會是在文章列表。那我們就直接使用 dev tool 的 console 來測試選擇看看這些物件吧。

// 選手名稱 (name) 
$('.profile-header__name').text().trim()

// 參賽主題 (title)
$('.qa-list__title--ironman').text().trim().replace(' 系列', '')

// 參賽天數 (joinDays)
$('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')

// 參賽文章數 (posts)
$('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')

// 追蹤人數 (subscriber)
$('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')

取得文章列表

取得文章列表
文章列表的部分看起來會需要一個 loop 來抓取 class 為 qa-list profile-list ir-profile-list 的物件們,然後每個再去抓他們各自的文章相關資訊,來試試看能不能順利選擇文章列表。

$('.qa-list')

取得文章內容

取得文章內容
接下來看文章的資訊,這邊比較特別一點,文章的 like、留言、瀏覽並不是有個別的 class name,而是一起使用 qa-condition__count class 來放這些數字,但我們應該可以用第幾個來辨識他們。

$('.qa-list').map((index, obj) => {
      return {
        title: $(obj).find('.qa-list__title').text().trim(),
        like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
        comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
        view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
        date: $(obj).find('.qa-list__info-time').text().trim(),
        url: $(obj).find('.qa-list__title a').attr('href').trim(),
      }
}).get()

實作程式碼

困難的都研究完了,接下來就像組裝積木把它組起來爾已。

取得一個主題內容

首先我們先來寫個抓取單一 url 的 function,因為這個 function 會用到 request,所以肯定會是一個非同步的 function,因此,我們除了要給 url 外,還要傳個 callback 進去。

const request = require('request')
function getInfo(url, callback){
  request(url, function(err, res, body){
      // 處理內容
  })
}

接下來我們用 cheerio 來模擬 jQuery 的選擇器去選取我們要抓的資料,最後將拿到的資訊傳到 callback 裡面,那麼 getInfo function 就完成了。

const request = require('request')
const cheerio = require('cheerio')
function getInfo(url, callback){
  request(url, function(err, res, body){
    var $ = cheerio.load(body)
    var link = url
    var name = $('.profile-header__name').text().trim()
    var title = $('.qa-list__title--ironman').text().trim().replace(' 系列', '')
    var joinDays = $('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')
    var posts = $('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')
    var subscriber = $('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')
    var postList = $('.qa-list').map((index, obj) => {
      return {
        title: $(obj).find('.qa-list__title').text().trim(),
        like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
        comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
        view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
        date: $(obj).find('.qa-list__info-time').text().trim(),
        url: $(obj).find('.qa-list__title a').attr('href').trim(),
      }
    }).get()

    callback(null, {
      name, title, link, joinDays, posts, subscriber, postList
    });
  })
}

列表所有主題

接著我們會需要一個 array 來裝所有參賽者的主題網址。

const ironmans = [
  'https://ithelp.ithome.com.tw/users/20107159/ironman/1325',
  'https://ithelp.ithome.com.tw/users/20107356/ironman/1315',
  'https://ithelp.ithome.com.tw/users/20107440/ironman/1355',
  'https://ithelp.ithome.com.tw/users/20107334/ironman/1335',
  'https://ithelp.ithome.com.tw/users/20107329/ironman/1286',
  'https://ithelp.ithome.com.tw/users/20091297/ironman/1330',
  'https://ithelp.ithome.com.tw/users/20075633/ironman/1375',
  'https://ithelp.ithome.com.tw/users/20107247/ironman/1312',
  'https://ithelp.ithome.com.tw/users/20107335/ironman/1337',
  'https://ithelp.ithome.com.tw/users/20106699/ironman/1283',
  'https://ithelp.ithome.com.tw/users/20107420/ironman/1381',
]

跑 loop

最後我們使用 async 的 map 來跑 ironmans,並讓 array 裡面的每個物件去呼叫剛剛寫的 getInfo function,這樣整體就完成了。

async.map( ironmans, getInfo, (err, results)=>{
  console.log(results);
})

完整程式碼

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

const ironmans = [
  'https://ithelp.ithome.com.tw/users/20107159/ironman/1325',
  'https://ithelp.ithome.com.tw/users/20107356/ironman/1315',
  'https://ithelp.ithome.com.tw/users/20107440/ironman/1355',
  'https://ithelp.ithome.com.tw/users/20107334/ironman/1335',
  'https://ithelp.ithome.com.tw/users/20107329/ironman/1286',
  'https://ithelp.ithome.com.tw/users/20091297/ironman/1330',
  'https://ithelp.ithome.com.tw/users/20075633/ironman/1375',
  'https://ithelp.ithome.com.tw/users/20107247/ironman/1312',
  'https://ithelp.ithome.com.tw/users/20107335/ironman/1337',
  'https://ithelp.ithome.com.tw/users/20106699/ironman/1283',
  'https://ithelp.ithome.com.tw/users/20107420/ironman/1381',
]

async.map( ironmans, getInfo, (err, results)=>{
  console.log(results);
})

function getInfo(url, callback){
  request(url, function(err, res, body){
    var $ = cheerio.load(body)
    var link = url
    var name = $('.profile-header__name').text().trim()
    var title = $('.qa-list__title--ironman').text().trim().replace(' 系列', '')
    var joinDays = $('.qa-list__info--ironman span').eq(0).text().replace(/[^0-9]/g,'')
    var posts = $('.qa-list__info--ironman span').eq(1).text().replace(/[^0-9]/g,'')
    var subscriber = $('.qa-list__info--ironman span').eq(2).text().replace(/[^0-9]/g,'')
    var postList = $('.qa-list').map((index, obj) => {
      return {
        title: $(obj).find('.qa-list__title').text().trim(),
        like: $(obj).find('.qa-condition__count').eq(0).text().trim(),
        comment: $(obj).find('.qa-condition__count').eq(1).text().trim(),
        view: $(obj).find('.qa-condition__count').eq(2).text().trim(),
        date: $(obj).find('.qa-list__info-time').text().trim(),
        url: $(obj).find('.qa-list__title a').attr('href').trim(),
      }
    }).get()
    
    callback(null, {
      name, title, link, joinDays, posts, subscriber, postList
    });
  })
}

衍生應用

既然已經可以抓取資料了,那麼我們可以找個地方放,讓他變成是一個 api service,可以選擇放在 aws lambda、google function、heroku,那麼就可以讓其他人直接取得資訊加以應用了。

http://ironmans.goodideas-studio.com/
這邊是好想工作室所有參賽者的主題資訊,也歡迎各位點閱追蹤。


上一篇
小插曲 #2 - www-form-urlencoded 的 space
下一篇
番外篇 #1 - 養成持續分享的習慣
系列文
爬蟲始終來自於墮性34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
Wolke
iT邦新手 1 級 ‧ 2017-12-09 00:14:00

沒有報團體組

Howard iT邦新手 4 級 ‧ 2017-12-09 00:25:11 檢舉

我其實看不懂團報的方式
/images/emoticon/emoticon06.gif

0
utopia
iT邦新手 3 級 ‧ 2017-12-11 11:46:29

版大的文章有得獎相! 加油!

Howard iT邦新手 4 級 ‧ 2017-12-11 14:17:59 檢舉

感謝支持,教學相長,分享才是最重要的目的

0
jia0
iT邦新手 5 級 ‧ 2018-01-19 10:51:43

這系列好好玩

Howard iT邦新手 4 級 ‧ 2018-01-19 11:01:55 檢舉

感謝支持

0
kiancaca
iT邦新手 5 級 ‧ 2019-03-15 17:47:15

你好,想知道你怎麼處理分頁裡的文章列表?
因為完整程式碼跟 http://ironmans.goodideas-studio.com/ 的結果不一樣。

我要留言

立即登入留言