[Python] [網路爬蟲] 用 selenium 不用登入就能爬過 Facebook

因為某些原因決定來寫個爬蟲,可以計算 FB 某則貼文中某個關鍵字的數量,然後是可以在不登入的情況下使用的。這篇文章記錄了一些要處理、注意的東西,那麼我們就開始吧!

4/2 補:因為 FB 網頁的原始碼有改動,所以程式碼有作更新,但內文還是舊的,因為重要的步驟都沒變,所以只更新文章底部放的程式碼。

  首先來個 10 秒入門 Selenium:
from selenium import webdriver
driver = webdriver.Firefox(executable_path=r'<存放 Firefox driver 的路徑>')  # 建立 driver 物件
driver.get(r'<你想爬的網頁>')  # 連線至指定的網頁
driver.quit()  # 關閉網頁並退出 driver
  好的,這個範例應該挺淺顯易懂的,那我們要繼續下去了。

  • 開始爬取 FB 貼文
  當你在沒有登入的情況下開啟了一個 FB 貼文,過一小段時間會跳出這樣的畫面:
  它擋住了我們要爬的文章!一定要想個辦法除掉它。
  (1/21 補:現在的 FB 好像不會跳出這個大框框了,如果確定沒有的話,就可以跳過下一步)
    • 尋找與等待:By、WebDriverWait
  雖然我們迫不及待地想要按下那個「稍後再說」的按鈕,但這個大方塊並不是一開始就存在的,而是過了一段時間才會跳出來,那麼到底是多久呢?  不知道。

  那我們要怎麼知道它已經出現了?幸好 selenium 有提供 WebDriverWait 物件,可以使用他來檢查某個條件是否已經達成,我們先看看程式碼:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from selenium.common.exceptions import TimeoutException

try:
    ele = WebDriverWait(driver, 10).until(
        ec.visibility_of_element_located((By.ID, 'expanding_cta_close_button'))
    )
except TimeoutException:
    print('超過時間還是找不到要找的東西')
  接著來拆解這段程式碼:首先是建立 WebDriverWait 物件,要放入兩個參數:剛剛的 driver 與 timeout,如果過了 timeout 秒之後都沒有達成,就會抛出 TimeoutException 例外。

  然後我們使用了他的 .until() 方法:參數為一個函式,而這個做為參數的函式本身又接受另一個參數:前面的 driver(9/8 :這個 driver 會由 until() 幫忙傳,不用自己寫)。如此一來,WebDriverWait 物件會反覆檢查並等到這個函式回傳了不是 False 的東西,或是超時了,才會離開這個 until;另外,until() 方法回傳的物件就是他本身的參數最後回傳的物件。

  最後我們在 until() 方法中放入了 ec.visibility_of_element_located 物件,此類別需要一個 tuple 進行初始化,而這個 tuple 的第一個元素是 By 類別底下所定義的屬性,在這裡我們選擇使用 By.ID,第二個元素則是我們想要尋找的 id,而透過右鍵 > 檢查元素,我們已經知道這個「稍後再說」的 id 是 「expanding_cta_close_button」。接下來就只要把這些程式拼在一起就可以了!另外,By 類別有提供一些好用的尋找方法,像是:
      • ID
      • CLASS_NAME
      • CSS_SELECTOR
      • TAG_NAME
      • XPATH
  喔喔!我忘了說明 ec 和 ec.visibility_of_element_located 這兩個東西是什麼了,ec 的原名是 expected_conditions,是個模組。

  那麼 visibility_of_element_located 又是什麼呢?他是所謂 excepted conditions(期望條件)的其中之一,代表的是具有指定條件的元素是否可見,在這裡就是是否有一個 id 長得像那樣的元素存在於當前 DOM 中且是可看見的。其他的期望條件還有:
      • title_is
      • presence_of_element_located
      • text_to_be_present_in_element
      • element_to_be_clickable
等等很多好用的類別。
    • 展開所有留言
  接著就進入頁面的主體-留言了,在這一小節,我們要一步一步的打開所有留言,先讓我們來看看有哪些種類的連結可以點:
  有了這些資訊後,我們就可以寫出這樣的程式:
# 省略剛剛的 import

while True:
    ele = WebDriverWait(driver, 5).until(
              ec.visibility_of_element_located((By.CLASS_NAME, 'UFIPagerLink')))
    ele.click()

  Oh-oh,如果運氣不太好的話程式就會出錯給你看:
....StaleElementReferenceException: Message: The element reference of <a class="UFIPagerLink" href="#">
is stale; either the element is no longer attached to the DOM, it is not in the current frame context,
or the document has been refreshed
會出現這樣的問題是因我們的 driver 按太快了,不小心按到舊的連結,如果要修這個 bug 的話,可以利用正在載入時的一個小圈圈:
←這個圈圈的 class 是  mls  img  _55ym  _55yn  _55yo
  先檢查這個小圈圈是不是已經不見了,再去取得其他的按鈕來按,就不會按太快了。

  但是要怎麼判斷它是不是已經消失了?剛剛的 EC 列表中好像沒有適合的...  這時候就要使用 WebDriverWait 物件的 .until_not() 方法啦,跟 until() 唯一的差別就在於它會等到作為參數的函式回傳 False 止,所以很適合判斷一個元素是否已經消失。
  修正後的程式碼應該長這樣:
# 省略剛剛的 import

while True:
    WebDriverWait(driver, 8).until_not(ec.presence_of_element_located(
        (By.CSS_SELECTOR, '.mls.img._55ym._55yn._55yo')))
    ele = WebDriverWait(driver, 5).until(
        ec.visibility_of_element_located((By.CLASS_NAME, 'UFIPagerLink')))
    ele.click()
  ㄨㄚˊ,結果馬上又出錯了:
....ElementClickInterceptedException: Message: Element <a class="UFIPagerLink" href="#"> is not 
clickable at point (605, 883.833...) because another element <div id="u_0_d" class="_3ob9"> obscures it
  這是因為下面有一條大大的橫板擋住了:
  那要怎麼把它移除呢?這個部分比較麻煩,要叫 driver 執行 JavaScript 的指令,聽起來好像很難,但其實只要使用 driver 的 .execute_script() 方法並傳入內容為指令的字串就可以了。
  在這裡我們要執行的指令是:
document.getElementById('u_0_c').remove();
  其實應該還滿容易理解的,不過或許有眼尖的讀者發現,上面的錯誤訊息不是說他的 id 是 u_0_d 嗎?為什麼這邊刪的是 u_0_c 呢?其實是因為 u_0_c 是 u_0_d 的父節點,刪去前者才算完全根除了這個討厭的大個子。

  所以我們要將程式碼改這樣:
while True:
    try:
        # 不要有正在跑的小圈圈
        WebDriverWait(driver, 8).until_not(ec.presence_of_element_located(
            (By.CSS_SELECTOR, '.mls.img._55ym._55yn._55yo')))
        # 找「顯示先前留言」、「查看更多回覆」
        ele = WebDriverWait(driver, 5).until(ec.visibility_of_element_located((By.CLASS_NAME, 'UFIPagerLink')))
        ele.click()
    except ElementClickInterceptedException:
        print('remove')
        # 移除下面的橫幕
        js = "document.getElementById('u_0_c').remove();"
        driver.execute_script(js)
    except TimeoutException:
        print('ok (1)')
        break
  我也順便加入了在超過時限時就離開的部分(第 14 行),因為圈圈不太可能很久都不消失,所以 TimeoutException 應該會是在找按鈕時拋出的,那找不到的話就代表這裡告一段落了。

  這一節的最後要做的事是把每個「查看更多」點開,就來看程式碼吧:
for ele in driver.find_elements(By.CLASS, '_5v47 fss'):
    ele.click()
  然後你就會發現:
  原因在於 selenium 不支援對多重 class 的尋找,所以找不出含有空格的 class 屬性,如果要克服這個問題的話,可以改用 CSS Selector 或 XPath,在這裡前者就已經很夠用了,所以我們要這樣寫:
for ele in driver.find_elements(By.CSS_SELECTOR, '._5v47.fss'):
    ele.click()
print('ok (2)\n')
  就可以找到我們要的東西了。

    • 找尋目標並計算數量
  終於進入我們的重頭戲啦!費了千辛萬苦,總算要來找目標字串了,在繼續之前,我們要先知道一件事:純留言的部分是個 span,它的 class 是 UFICommentBody。
  有了這樣的資訊我們可以再利用一次 CSS Selector 來找所有的留言,並使用每個 WebElement 的 text 屬性取得留言字串,最後使用 re 模組去找一個留言中有多少個相符的字串:
gex = re.compile(r'你好')
count = 0
for comment in driver.find_elements_by_css_selector('span.UFICommentBody'):
    tmp = len(gex.findall(comment.text))
    print(f'+{tmp}', end='  ')
    count += tmp
print('\nfinish.\n')
print(f'共 {count} 個「你好」')
  咦?為什麼這裡尋找元素用的方法不太一樣啊?跟上面那個找「查看更多」有什麼不同嗎?  
  沒有喔,只是想換換口味而已。

  這樣就完成我們的爬蟲了!這邊補充一張圖:
  另外,如果不想要執行的時候跑出瀏覽器的話,可以在建立 driver 的時候這麼做:  
options = webdriver.FirefoxOptions()
options.add_argument('-headless')  # headless 模式
driver = webdriver.Firefox(executable_path=r'<存放 Firefox driver 的路徑>', options=options)

  好了,那這次就到這邊!完整的程式碼我放在這邊了,謝謝大家的閱讀m(_ _)m,如果有疑慮或指正歡迎留言提出。

參考資料:
  1) 下載 Firefox driver
  2) Selenium with Python (非官方文件)
    2.1) WaitsWebDriver API

後記:
  其實不用登入是因為我還不會處理登入XD 雖然應該是直接搷入欄位再 submit 就好了,但這樣不知道是不是最好的做法,所以就下次再說吧(如果有的話)
  另外我有查到 FB 本身提供的 Graph API,但我真的沒他的慧根,看不太懂要怎麼用 那這個一樣也下次再說吧XD

留言

  1. 最近也在研究這一塊 FB的API你可以暫時不用考慮了
    因為需要送審才會讓你用 如果你是單純要自用的話
    很難過審 時間很長 浪費時間

    回覆刪除
    回覆
    1. 其實我後來去仔細看了他的文件,發現功能其實不太夠,就不太想用了

      不過原來還要送審,我都沒注意到,那這樣就真的沒有必要去用他了XD

      刪除
  2. 超棒的文章 讀得也很開心 讚讚

    回覆刪除
  3. 作者已經移除這則留言。

    回覆刪除
  4. 后面发现被封号了,开心

    回覆刪除
    回覆
    1. 剛剛測試的結果好像是 FB 把 class 改掉了(還是頁面版本不同?)
      只要把 class 的部分修正就可以了。

      刪除
  5. 感謝版主整理!
    想請問一下有關圈圈(class 是 mls img _55ym _55yn _55yo)的class查詢您如何做的呢?
    嘗試了chrome暫停(Esc)及F12(Network)目前只抓的到圈圈圖片..
    感謝!

    回覆刪除
    回覆
    1. 只要在點開留言前先斷網,就可以讓這個圈圈停留很久,
      然後再用右鍵 > 檢查就可以看到了!

      刪除
  6. 嗨~~~我很喜歡妳的這篇教學文章,但有些粉專訪客沒辦法看到所有文章,要登入帳號才能看到,所以有些粉專還是要登入才能抓到文章:)

    回覆刪除
    回覆
    1. 還有如果要抓各表情的數量,必須登入才可以抓

      刪除

張貼留言

這個網誌中的熱門文章

[C] 每天來點字串用法 (2) - strcpy()、strncpy()

[Python] *args 和 **kwargs 是什麼?一次搞懂它們!

[C] 每天來點字串用法 (5) - strcat()、strncat()