python網路爬蟲應用-facebook社團成員參與度分析

星期六 16 Nov 2019   even  
教學

前言

我一位朋友在經營facebook社團,需要每個月統計成員的參與度,例如某某人PO幾篇文章、按了幾次讚、留言幾次,諸如此類。向我尋求協助,於是就編寫了一段簡單的臉書爬蟲程式,自動統計這些數值。

原本facebook有提供後台API,可以讓人非常輕鬆的撈取資料,然而facebook在幾波個資外洩風暴的影響下,如2018年3月爆發的劍橋事件等,使得此API的功能被設下重重限制,難以像以前一樣輕鬆撈取資料。現今只能使用上篇教學介紹的selenium進行爬蟲。

本篇將會介紹如何使用selenium登入facebook,前往目標社團,解析原始碼,抓取目標資訊,學會本篇的內容後facebook就變成隨你爬的遊樂場了。

本篇教學會以台灣資料工程協會 為範例進行,你們可以改成任意臉書社團。

目的

使用python編寫自動化程式,抓取臉書OOO社團內一個月內,各社團成員的活動紀錄,包含: 張貼文章次數、留言次數、回覆表情符號次數。

制定爬取策略

將社團中的貼文、留言、表情符號回覆的資料中誰與何時抓取下來,如果時間在這一個月內,則把這筆紀錄留下,最後在統計每位的參與度。

誰在何時貼文或留言的資訊在臉書上都一目瞭然,因此只需要直接剖析原始碼,即可得知。

其中有許多資訊需要使用滑鼠點開才會顯示。

表情符號的部分,則須再前往另外一個分頁,才能看到那些人在那個時間對這篇文章或留言回覆。

在表情符號處點下後,會開啟另外的一個小視窗,一一顯示那些人回覆什麼表情符號。也可以將這裡的超連結保存起來,直接以瀏覽器前往。

以這裡為例,共有三位成員回覆表情符號,但是並沒有顯示何時按下表情符號。按讚的時間一定會在原始貼文或留言的時間之後,因此還是能判斷這個讚是在這一個月內按下去的。但如果是對一篇一個月以前的文章按讚的話,就無法分別,只能無視。

觀察到這裡,應該已經能夠擬出一套爬取策略了,步驟如下:

  1. 使用selenium登入臉書,前往目標社團。
  2. 操作selenium將頁面不斷往下捲動。 (臉書的頁面必須不斷的往下拉,舊的內容才會顯示)
  3. 操作selenium將檢視另X留言OOO已回覆 XX則回覆等,一一點開。
  4. 將現在頁面的html原始碼儲存下來。
  5. 使用beautifulsoup、re等套件解析原始碼,將誰在何時貼文與留言抓取下來。
  6. 如果某貼文或留言的時間在一個月內,則將回覆表情符號的超連結額外儲存起來。
  7. 使用selenium一一前往回覆表情符號的超連結,將其中所有的成員記錄下來。
  8. 將所有資料整併,輸出各成員在這段時間的社團餐與狀況。

OK,那我們就來一一執行吧~

登入facebook

使用selenium開啟瀏覽器,前往臉書,輸入帳號密碼,點下登入。

from selenium import webdriver
import time

profile = webdriver.FirefoxProfile() # 新增firefox的設定
profile.set_preference("dom.webnotifications.enabled", False) # 將頁面通知關掉
driver = webdriver.Firefox(firefox_profile=profile)
driver.get("http://www.facebook.com")
time.sleep(3)
driver.find_element_by_id("email").send_keys(USERNAME) # 將USERNAME改為你的臉書帳號
driver.find_element_by_id("pass").send_keys(PASSWORD) # 將PASSWORD改為你的臉書密碼
driver.find_element_by_id("loginbutton").click()
time.sleep(3)
driver.get('https://www.facebook.com/groups/733787316774129/')

由於示範的臉書社團是公開社團,因此可以省略登入,但如果目標是秘密社團,某先你必須先加入那個社團,然後這裡再登入。

我額外加入了profile.set_preference("dom.webnotifications.enabled", False),這段firefox的設定,避免惱人的臉書通知訊息冒出來,阻擋住selenium下一步的動作。

如果不將webnotification關掉就有可能遇到像上圖的狀況,然後就無法命令Selenium執行接下來的步驟。

捲動視窗與點開隱藏留言

要將視窗往下捲,內容才會出現在網頁原始碼上。依照社團熱烈的程度,如果有很多貼文,就必需不斷往下捲,才能將一個月內的貼文都包含到裡面。

for i in range(12): # 捲動12次
    driver.execute_script("window.scrollTo(0, {})".format(4000 * (i + 1))) 每次捲動4000的單位
    time.sleep(2) # 等待2秒鐘讓頁面讀取

目標是一個月內的po文,依照你的目標社團去調整頁面捲動的次數。

接著讓selenium將所有檢視另X留言OOO已回覆 XX則回覆等,一一點開。

有些按鈕是在第一次點開之後才會出現,因此需要重複執行兩輪。

那麼,是需要定位到哪個元素去執行click呢?這裡就需要仔細的檢查元素,可以參考我的教學,如何檢查元素

根據我檢查的結果,目標位在<a test-id="UFI2CommentsPagerRenderer/pager_depth_0"> <a test-id="UFI2CommentsPagerRenderer/pager_depth_1">底下,因此程式碼如下。

def ClickForMore():
    hrefBtns = driver.find_elements_by_tag_name('a')    
    for btn in hrefBtns:
        try:
            s = btn.get_attribute('data-testid')
        except:
            continue
        if s == 'UFI2CommentsPagerRenderer/pager_depth_1' or s == 'UFI2CommentsPagerRenderer/pager_depth_0':
            try:
                btn.click()
                time.sleep(1)
            except:
                continue
ClickForMore()
ClickForMore()

解析原始碼

取得網站的原始碼,進行解析,我先使用beautifulsoup選取每則貼文及留言的區域,再以regex字串比對抓取時間和成員ID。原始碼中有unix格式的時間,我使用datetime來進行簡單的比對,並設定起始與結束日期來設定時間範圍。如果有表情符號回覆,則把連結網址留下來,需要以selenium再次進行爬取。

from bs4 import BeautifulSoup
import datetime
import re

htmltext = driver.page_source # 將網頁原始碼拿出來


def parse_htmltext(htmltext, start_date, end_date):
    '''
    解析臉書貼文與回覆的原始碼。
    htmltext為原始碼,str
    star_date為起始日期,datetime.datetime
    end_date為結束日期,datetime.datetime
    '''   
    post_persons = []
    comment_persons = []
    good_urllist = [] # 回復表情符號超連結
    ustart_date = start_date.timestamp()
    uend_date = end_date.timestamp()
    soup = BeautifulSoup(htmltext, 'html.parser')
    body = soup.find('body')
    posts = body.select('div[id="pagelet_group_mall"]')[0].select('div[aria-label="動態消息"]')[0]
    feed_articles = posts.select('div[role="feed"]')[0].select('div[role="article"]')
    other_articles = posts.select('div[role="article"]')
    articles = feed_articles + other_articles # 所有貼文或留言
    
    for article in articles:
        if article.has_attr('id'):
            try:
                post_person = re.findall('title="(.{2,20})"><div class=', str(article))[0]
            except:
                continue
            post_time = int(re.findall('data-utime="(.*?)"', str(article))[0])        
            if post_time >= ustart_date and post_time <= uend_date:                
                post_persons.append(post_person)
            try:
                good_urllist.append(re.findall('"(/ufi/reaction/profile/browser/\?.*?)"', str(article))[0])
            except:
                pass
    
        elif article.has_attr('data-testid'):            
            comment_person = re.findall('directed_target_id.*?href=".*?">(.*?)</a>', str(article))[0]  
            comment_time = int(re.findall('data-utime="(.*?)"', str(article))[0])
            if comment_time >= ustart_date and post_time <= uend_date:                    
                comment_persons.append(comment_person)                    
                try:
                    good_urllist.append(re.findall('"(/ufi/reaction/profile/browser/\?.*?)"', str(article))[0])
                except:
                    pass
    
    return post_persons, comment_persons, good_urllist

post_persons, comment_persons, good_urllist = parse_htmltext(htmltext, datetime.datetime(2019, 10, 15), datetime.datetime(2019, 11, 15))

臉書的原始碼很難解析,花了許多時間才成功爬取出來我要的資訊,細節我就不一一介紹,總之就是一再的察看原始碼的模式,然後想辦法用beautifulsoup選取到正確的區域,再以re去比對抓取。

再次抓取表情符號

經我的測試,一定要登入臉書才能拜訪表成符號的連結。每個頁面的內容非常統一而且簡單,因此就直接進行解析。

表情符號有很多種,但這裡就不一一區隔開來。如果你想知道誰按了幾次某種表情符號,則在這裡解析原始碼的地方設法將它區隔開來。

def parse_good_urllist(urllist):
    
    output = []

    profile = webdriver.FirefoxProfile()
    profile.set_preference("dom.webnotifications.enabled", False)
    profile.update_preferences()
    driver = webdriver.Firefox(firefox_profile=profile)
    driver.get("http://www.facebook.com")
    driver.find_element_by_id("email").send_keys(USERNAME) # 將USERNAME改為你的臉書帳號
    driver.find_element_by_id("pass").send_keys(PASSWORD) # 將PASSWORD改為你的臉書帳號
    driver.find_element_by_id("loginbutton").click()
    time.sleep(3)

    for url in urllist:
        driver.get('http://www.facebook.com/' + url)
        htmltext = driver.page_source
        soup = BeautifulSoup(htmltext, 'html.parser')
        for raw_text in soup.select('li[class="_5i_q"]'):
            output += re.findall(re.compile('aria-label="(.*?)" class="_s'),str(raw_text))            

    driver.close()
    return output

emoji_replies = parse_good_urllist(good_urllist)

整理數據,完工

將前面產出的post_personscomment_personsemoji_persons,轉換成計次,再將結果打包成excel檔輸出。

import pandas as pd

def tidy_up_data(post_persons, comment_persons, emoji_persons):
    
    all_persons = list(set(post_persons+comment_persons+emoji_persons))
    post_times = []
    comment_times = []
    emoji_times = []
    
    for p in all_persons:
        post_times.append(post_persons.count(p))
        comment_times.append(comment_persons.count(p))
        emoji_times.append(emoji_persons.count(p))
    
    return pd.DataFrame(dict(成員ID=all_persons, 貼文次數=post_times, 回文次數=comment_times, 回覆表情符號次數=emoji_times))
        
df = tidy_up_data(post_persons, comment_persons, emoji_persons)
df.to_excel('member_activity.xlsx', index=False)

將最後結果輸出為member_activity.xlsx,輸出的資料如下:(只呈現部分資料,並將成員ID遮起來)

成員ID 貼文次數 回文次數 回覆表情符號次數
OOO 0 0 2
OOO 0 0 2
OOO 0 0 8
OOO 0 0 6
OOO 0 0 14
OOO 0 0 2
OOO 10 0 6
OOO 0 0 2
OOO 0 0 10

完美!

結語

恭喜你學會如何抓取臉書社團資料了~

步驟很多,關關難過關關過,最後就能得的你要的結果。編寫爬蟲程式是需要時間慢慢磨的,從一再的檢查網頁,擬定爬取策略,到細部解析原始碼的正規表達式寫法,都是需要來來回回的嘗試。尤其是解析原始碼的部分,非常麻煩,你可以嘗試不用我的程式,自己慢慢編寫看看,說不定會比我的方式更加簡潔。

另外,我原本擔心會不會因為臉書經常更新,而導致隔一陣子程式碼就無法作用,不過情況還好,在這一年內只有改動過一次小地方。如果到時候沒辦法運行的話,就再想辦法修復吧!

下一篇我會介紹另外一種臉書爬蟲的應用,抓取照片,各位好好期待吧~

感謝你的耐心閱讀到最後,希望你有學到些什麼,我們有緣再見~

完整程式碼

from selenium import webdriver
from bs4 import BeautifulSoup
import pandas as pd
import re
import time
import datetime

def get_htmltext(username, password):
    '''
    username 你的臉書帳號
    password 你的臉書密碼
    '''    
    profile = webdriver.FirefoxProfile()
    profile.set_preference("dom.webnotifications.enabled", False)        
    driver = webdriver.Firefox(firefox_profile=profile)    
    driver.get("http://www.facebook.com")
    time.sleep(3)
    driver.find_element_by_id("email").send_keys(username)
    driver.find_element_by_id("pass").send_keys(password)
    driver.find_element_by_id("loginbutton").click()
    time.sleep(3)
    driver.get('https://www.facebook.com/groups/733787316774129/')
    time.sleep(3)
    for i in range(12):
        y = 4000 * (i + 1)
        driver.execute_script(f"window.scrollTo(0, {y})")
        time.sleep(2)

    def ClickForMore():
        hrefBtns = driver.find_elements_by_tag_name('a')    
        for btn in hrefBtns:
            try:
                s = btn.get_attribute('data-testid')
            except:
                continue
            if s == 'UFI2CommentsPagerRenderer/pager_depth_1' or s == 'UFI2CommentsPagerRenderer/pager_depth_0':
                try:
                    btn.click()
                    time.sleep(1)
                except:
                    continue
                
    ClickForMore()
    ClickForMore()      

    htmltext = driver.page_source
    driver.close()
    
    return htmltext

def parse_htmltext(htmltext, start_date, end_date):
    '''
    解析臉書貼文與回覆的原始碼。
    htmltext為原始碼,str
    star_date為起始日期,datetime.datetime
    end_date為結束日期,datetime.datetime
    '''
    post_persons = []
    comment_persons = []
    good_urllist = []
    ustart_date = start_date.timestamp()
    uend_date = end_date.timestamp()
    soup = BeautifulSoup(htmltext, 'html.parser')
    body = soup.find('body')
    posts = body.select('div[id="pagelet_group_mall"]')[0].select('div[aria-label="動態消息"]')[0]
    feed_articles = posts.select('div[role="feed"]')[0].select('div[role="article"]')
    other_articles = posts.select('div[role="article"]')
    articles = feed_articles + other_articles
    
    for article in articles:
        if article.has_attr('id'):
            try:
                post_person = re.findall('title="(.{2,20})"><div class=', str(article))[0]
            except:
                continue
            post_time = int(re.findall('data-utime="(.*?)"', str(article))[0])        
            if post_time >= ustart_date and post_time <= uend_date:                
                post_persons.append(post_person)
            try:
                good_urllist.append(re.findall('"(/ufi/reaction/profile/browser/\?.*?)"', str(article))[0])
            except:
                pass
    
        elif article.has_attr('data-testid'):            
            comment_person = re.findall('directed_target_id.*?href=".*?">(.*?)</a>', str(article))[0]  
            comment_time = int(re.findall('data-utime="(.*?)"', str(article))[0])
            if comment_time >= ustart_date and post_time <= uend_date:                    
                comment_persons.append(comment_person)                    
                try:
                    good_urllist.append(re.findall('"(/ufi/reaction/profile/browser/\?.*?)"', str(article))[0])
                except:
                    pass
    
    return post_persons, comment_persons, good_urllist

def parse_good_urllist(username, password,urllist):
    '''
    username 你的臉書帳號
    password 你的臉書密碼
    ''' 
    
    output = []

    profile = webdriver.FirefoxProfile()
    profile.set_preference("dom.webnotifications.enabled", False)  # Finally, turned off webnotifications...
    profile.update_preferences()
    driver = webdriver.Firefox(firefox_profile=profile)
    driver.get("http://www.facebook.com")
    time.sleep(3)
    driver.find_element_by_id("email").send_keys(username)
    driver.find_element_by_id("pass").send_keys(password)
    driver.find_element_by_id("loginbutton").click()
    time.sleep(3)

    for url in urllist:
        driver.get('http://www.facebook.com/' + url)
        htmltext = driver.page_source
        soup = BeautifulSoup(htmltext, 'html.parser')
        for raw_text in soup.select('li[class="_5i_q"]'):
            output += re.findall(re.compile('aria-label="(.*?)" class="_s'),str(raw_text))            

    driver.close()
    return output

emoji_persons = parse_good_urllist(good_urllist)

def tidy_up_data(post_persons, comment_persons, emoji_persons):
    
    all_persons = list(set(post_persons+comment_persons+emoji_persons))
    post_times = []
    comment_times = []
    emoji_times = []
    
    for p in all_persons:
        post_times.append(post_persons.count(p))
        comment_times.append(comment_persons.count(p))
        emoji_times.append(emoji_persons.count(p))
    
    return pd.DataFrame(dict(成員ID=all_persons, 貼文次數=post_times, 回文次數=comment_times, 回覆表情符號次數=emoji_times))

if __name__ == '__main__':
    username = 'YOUR USERNAME'
    password = 'YOUR PASSWORD'

    htmltext = get_htmltext(username, password)
    post_persons, comment_persons, good_urllist = parse_htmltext(htmltext, datetime.datetime(2019,10,15), datetime.datetime(2019,11,15))
    emoji_persons = parse_good_urllist(username, password, good_urllist)
    df = tidy_up_data(post_persons, comment_persons, emoji_persons)
    df.to_excel('member_activity.xlsx', index=False)
python 網頁爬蟲教學
python網路爬蟲簡介
python網路爬蟲基本工具(1)
python網路爬蟲教學-實戰篇(1) 蘋果日報馬網
使用偽裝user-agent爬取蝦皮購物網
撈取深網中的資料-蝦皮購物API
以POST方式抓取資料-政府電子採購網
python網路爬蟲教學-Selenium基本操作
python網路爬蟲應用-facebook社團成員參與度分析

相關文章:

>