[Python] [網路爬蟲] 用 selenium 不用登入就能爬過 Facebook
因為某些原因決定來寫個爬蟲,可以計算 FB 某則貼文中某個關鍵字的數量,然後是可以在不登入的情況下使用的。這篇文章記錄了一些要處理、注意的東西,那麼我們就開始吧!
4/2 補:因為 FB 網頁的原始碼有改動,所以程式碼有作更新,但內文還是舊的,因為重要的步驟都沒變,所以只更新文章底部放的程式碼。
4/2 補:因為 FB 網頁的原始碼有改動,所以程式碼有作更新,但內文還是舊的,因為重要的步驟都沒變,所以只更新文章底部放的程式碼。
- 使用 Selenium (搭配 Firefox driver)
首先來個 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 好像不會跳出這個大框框了,如果確定沒有的話,就可以跳過下一步)
它擋住了我們要爬的文章!一定要想個辦法除掉它。
(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 的話,可以利用正在載入時的一個小圈圈:先檢查這個小圈圈是不是已經不見了,再去取得其他的按鈕來按,就不會按太快了。
但是要怎麼判斷它是不是已經消失了?剛剛的 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')
就可以找到我們要的東西了。
- 找尋目標並計算數量
有了這樣的資訊我們可以再利用一次 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) Waits、WebDriver API
後記:
其實不用登入是因為我還不會處理登入XD 雖然應該是直接搷入欄位再 submit 就好了,但這樣不知道是不是最好的做法,所以就下次再說吧(如果有的話)
另外我有查到 FB 本身提供的 Graph API,但我真的沒他的慧根,看不太懂要怎麼用 那這個一樣也下次再說吧XD
最近也在研究這一塊 FB的API你可以暫時不用考慮了
回覆刪除因為需要送審才會讓你用 如果你是單純要自用的話
很難過審 時間很長 浪費時間
其實我後來去仔細看了他的文件,發現功能其實不太夠,就不太想用了
刪除不過原來還要送審,我都沒注意到,那這樣就真的沒有必要去用他了XD
超棒的文章 讀得也很開心 讚讚
回覆刪除謝謝你的稱讚!
刪除作者已經移除這則留言。
回覆刪除后面发现被封号了,开心
回覆刪除剛剛測試的結果好像是 FB 把 class 改掉了(還是頁面版本不同?)
刪除只要把 class 的部分修正就可以了。
沒錯
刪除感謝版主整理!
回覆刪除想請問一下有關圈圈(class 是 mls img _55ym _55yn _55yo)的class查詢您如何做的呢?
嘗試了chrome暫停(Esc)及F12(Network)目前只抓的到圈圈圖片..
感謝!
只要在點開留言前先斷網,就可以讓這個圈圈停留很久,
刪除然後再用右鍵 > 檢查就可以看到了!
嗨~~~我很喜歡妳的這篇教學文章,但有些粉專訪客沒辦法看到所有文章,要登入帳號才能看到,所以有些粉專還是要登入才能抓到文章:)
回覆刪除還有如果要抓各表情的數量,必須登入才可以抓
刪除謝謝你的分享~
刪除