過去取得 Facebook 社團貼文的方法為「訂閱FB社團通知郵件」,然後利用 Gmail 設定關鍵字篩選,挑出真正需要的貼文內容,整個優化流程記錄在「操作 Google Apps Script 定時過濾 FB 社團郵件通知」。
不過這套流程最近碰了壁,不知為何 Facebook 不再寄出郵件通知,持續了好一段時間,而且只有某個 FB 帳號收不到郵件通知,我的 FB 主帳號仍可收到社團貼文通知。合理推測為,可能那個 FB 帳號有太多社團郵件通知(主帳號不多),FB 郵件伺服器覺得耗費太多資源,決定封鎖該帳號的郵件通知功能。
既然 Facebook 自己提供的功能也靠不住,只好自己研究寫爬蟲抓 FB 社團貼文的方法。本篇會分享利用 Node.js 操作 Puppeteer,模擬 Chrome 瀏覽器開啟 FB 社團網頁並爬文的範例筆記,以及相關的注意要點。
(圖片出處: piqsels.com)
一、FB 爬蟲注意事項
1. FB 適合爬蟲的網址對於新手而言,FB 網頁版(www.facebook.com)由於常常改版,寫好的爬蟲程式可能過陣子就不能用了。根據這個「FB 爬蟲討論串」,推薦的 FB 爬蟲網址如下:- m.facebook.com:此為 FB 行動版網址
- mbasic.facebook.com:此為 FB 簡易版網址,頁面沒有 Javascript,環境非常單純,是爬蟲程式首選
- 不要使用主帳號作為爬蟲
- 頁面存取要設定間隔一段時間,例如隨機 5~10 秒以上,避免被 FB 偵測到固定行動模式
- 別做看似浪費 FB 資源的事
- FB 網頁版:社團網址加上參數 "?sorting_setting=CHRONOLOGICAL" 即可依照貼文發佈時間排序
- FB 行動版:點擊右上角選單圖示(三條橫線) → 最新動態 → 社團,就可看到所有社團貼文依照發佈時間排序
- FB 簡易版:沒有提供排序的方法
二、Node.js 準備動作
1. Node.js 環境設定使用 Node.js 的初始環境建構,可參考以下:- 「Node.js 爬蟲開發新手技巧」:瞭解 Node.js 基礎知識、開發工具
- 「使用 Node.js 爬蟲定期抓網頁資料」→「一、安裝 Axios、Cheerio 模組」:安裝 Cheerio 操作 DOM 比較方便
- 同一篇的「四、Windows 自動執行程式」可設定排程執行爬蟲程式
- 能模擬 Chrome 瀏覽器的操作,讀取 JS 載入後的頁面 HTML 內容
- 能模擬 FB 輸入帳號密碼的動作,解決帳號登入、網頁讀取 cookie 的問題
三、範例程式碼
以下為簡易的 FB 社團爬蟲範例程式碼,以「Blogger 經營學習資源分享」這個社團作例子,抓 10 篇貼文的內容及貼文網址。 程式碼代表的含意請參考註解文字,並請修改前幾個參數的數值(及帳號密碼等),因為只是能正常執行的簡單範例,需要更多功能請自行修改程式碼:const fbGroupId = "blogger.skill", // FB 社團 ID(為網址上的一串數字), 或社團自訂網址字串
fbHomeUrl = "https://mbasic.facebook.com", // FB 簡易版網址
fbUsername = "xxx@gmail.com", // FB 帳號
fbPassword = "xxxxx", // FB 密碼
maxPosts = 10, // 最多抓幾篇文章
cheerio = require("cheerio"),
puppeteer = require("puppeteer");
(async function() {
let fbPostsArrays = []; // 存放所有 FB 社團貼文
let browser, page;
// FB 初步動作
await fbInit();
// 抓 FB 社團貼文
await getFbGroupPosts();
console.log(fbPostsArrays); // 顯示爬文資料
// 關閉瀏覽器
await browser.close();
// FB 初步動作
async function fbInit() {
// 開啟瀏覽器
browser = await puppeteer.launch();
page = await browser.newPage();
// 載入 FB 首頁
await page.goto(fbHomeUrl);
// 登入 FB
await loginFB(page, fbUsername, fbPassword);
}
// 登入 FB
async function loginFB(page, username, password) {
await page.waitForSelector("#m_login_email");
await page.type("#m_login_email", username);
await page.waitForSelector("#password_input_with_placeholder");
await page.type("#password_input_with_placeholder input", password);
await page.waitForSelector("#login_form");
await page.click("#login_form input[name=login]");
}
// 抓 FB 社團貼文
async function getFbGroupPosts() {
let fbGroupUrl = fbHomeUrl + "/groups/" + fbGroupId;
let postCount = 0; // 已抓取的貼文數量
// 載入 FB 社團首頁
await page.goto(fbGroupUrl);
// 取得單一頁面貼文
await getSinglePagePosts();
// 取得單一頁面貼文
async function getSinglePagePosts() {
let nextPageUrl; // 下一頁連結
// 取得單一頁面所有貼文
await getSinglePagePosts();
// 貼文數量不到 maxPosts 則持續爬取
while (maxPosts > postCount) {
// 休息 10 秒
sleep(10);
// 前往下一頁
await page.goto(nextPageUrl);
// 繼續抓下一頁
await getSinglePagePosts();
}
// 取得單一頁面所有貼文
async function getSinglePagePosts() {
// 等待頁面載入 #m_group_stories_container
await page.waitForSelector("#m_group_stories_container");
//把網頁的body抓出來
let body = await page.content();
//丟給cheerio去處理
let $ = cheerio.load(body);
let $section = $("#m_group_stories_container section");
let $posts = $section.children("article");
let $nextPage = $section.next().children("a");
nextPageUrl = fbHomeUrl + $nextPage.attr("href"); // 下一頁連結
// loop 所有貼文
loopAllPosts();
function loopAllPosts() {
// 取得網址 內容
$posts.each(function() {
// 檢查貼文數量是否已達到 maxPosts
if (maxPosts < postCount){
return false;
}
let $this = $(this);
let $header = $this.find("header:eq(0)");
let $footer = $this.children("footer");
let postUrl = getPostUrl($footer); // 取得貼文連結
let body = $header.next().text(); // 取得貼文內容
fbPostsArrays.push([postUrl, body]);
postCount++; // 紀錄已爬取貼文數
});
}
// 取得貼文連結
function getPostUrl($footer) {
let postUrl;
$footer.find("a").each(function() {
let $this = $(this);
if ($this.text() == "完整動態") {
postUrl = $this.attr("href").split("?")[0];
postUrl = postUrl.replace("mbasic.", "");
return false; // 中止 loop
}
});
return postUrl;
}
}
}
}
// 休息 n 秒
function sleep(sec) {
var sharedArrayBuffer = new SharedArrayBuffer(4),
sharedArray = new Int32Array(sharedArrayBuffer);
Atomics.wait(sharedArray, 0, 0, sec * 1000);
}
})();
執行後的結果如下:
[
[
'https://facebook.com/groups/blogger.skill/permalink/1289039078370664/',
'[標籤選擇的問題] 因為 blogger 沒有把分類跟標籤拆開,撰文上原本把標籤當分類用,但因為覺得多一點標籤可以有利SEO(?),所以就標了一堆,但回到前台後就變得凌亂。 想說是有什麼方式可以指定只呈現要的標籤即可?'
],
[
'https://facebook.com/groups/blogger.skill/permalink/1291575704783668/',
'上面的圖是我找到的模板,但YT嵌入碼貼上後,高度被限制到很窄,同樣的YT預設的嵌入碼,在下面的圖中觀賞上比較舒適,HTML裡面找了很多但還是無解,因此來請教高手求解方'
],
[
'https://facebook.com/groups/blogger.skill/permalink/1290236434917595/',
'製作網頁如果需要用到一些素材、插圖時,可以使用「免費素材搜尋引擎」,一站直接搜尋所有熱門圖庫:https://icon.wfublog.com/ 同時本篇也介紹這些免費素材圖庫,依照CC0、圖示、向量、透明圖等類別進行重點說明:https://www.wfublog.com/2023/06/free-icon-vector-image-stock.html'
],
[
'https://facebook.com/groups/blogger.skill/permalink/1258260454781860/',
'最近因為要在文章裡插入表格,研究了一下方法,想將測試的結果回饋給社團,就把過程整理成文章,若有誤還請各位高手指點,謝謝。'
],
...
]
更多 NodeJs 相關文章: