你是否曾想過將 Threads 上重要的討論串備份下來,特別是當原作者透過多則回覆來補充說明時?手動截圖或複製貼上既費時又零散。在這篇教學中,我們將延續先前安裝好的 Puppeteer 工具,打造一個自動化流程,讓你只要輸入一則 Threads 貼文網址,就能完整備份主貼文及作者的所有回覆
我們的目標非常明確:建立一個自動化工作,當你提供一個 Threads 貼文網址時,它能抓取頁面資料,並整理成一個結構化的 JSON 檔案。
新增一個流程「Create Workflow」
初始節點選擇「Trigger manually」
下個節點選擇「Edit Fields」,欄位設定為「url」,內容可以隨便填寫想抓的 Threads 網址
下個節點選擇「Puppeteer」的「Run Custom Script」
程式碼填寫如下
// --- [ 0. 輔助函式定義 ] ---
/**
* 在巢狀的物件或陣列中遞迴尋找指定鍵的所有值
* @param {object} obj - 要搜尋的物件或陣列
* @param {string} key - 要尋找的鍵
* @returns {unknown[]} - 找到的值的陣列,其元素的具體類型不確定
*/
function nestedLookup(obj, key) {
let results = [];
if (typeof obj !== "object" || obj === null) {
return results;
}
if (Array.isArray(obj)) {
for (const item of obj) {
results = results.concat(nestedLookup(item, key));
}
} else {
for (const k in obj) {
if (k === key) {
results.push(obj[k]);
}
if (typeof obj[k] === "object") {
results = results.concat(nestedLookup(obj[k], key));
}
}
}
return results;
}
/**
* 解析 Threads 貼文的 JSON 資料集,提取重要欄位
* @param {object} data - 單一貼文的資料物件
* @returns {object} - 格式化後的貼文物件
*/
function parseThread(data) {
const result = {
text: data.post?.caption?.text || null,
published_on: data.post?.taken_at || null,
id: data.post?.id || null,
pk: data.post?.pk || null,
code: data.post?.code || null,
username: data.post?.user?.username || null,
user_pic: data.post?.user?.profile_pic_url || null,
user_verified: data.post?.user?.is_verified || false,
user_pk: data.post?.user?.pk || null,
user_id: data.post?.user?.id || null,
has_audio: data.post?.has_audio || false,
reply_count: data.post?.direct_reply_count || 0,
like_count: data.post?.like_count || 0,
images: (data.post?.carousel_media || [])
.map((media) => media.image_versions2?.candidates?.[1]?.url)
.filter(Boolean),
image_count: data.post?.carousel_media_count || 0,
videos: (data.post?.video_versions || [])
.map((video) => video.url)
.filter(Boolean),
};
if (result.username && result.code) {
result.url = `https://www.threads.net/@${result.username}/post/${result.code}`;
} else {
result.url = null;
}
return result;
}
// --- [ 1. 主執行邏輯 ] ---
const inputData = $input.item.json;
const postUrl = inputData.url;
// 輸入驗證
if (!postUrl || !postUrl.startsWith("https://www.threads.")) {
console.error(
"錯誤:未提供有效的 Threads 網址。應以 https://www.threads. 開頭。"
);
throw new Error("無效的 Threads 網址,流程中止");
}
console.log(`[開始抓取] 目標網址: ${postUrl}`);
try {
const postCodeFromUrl = postUrl.split("/post/")[1]?.split("/")[0];
if (!postCodeFromUrl) {
throw new Error("無法從 URL 中解析出貼文代碼。");
}
console.log(`[目標鎖定] 貼文代碼: ${postCodeFromUrl}`);
// --- [ 2. 導航並等待頁面載入 ] ---
await $page.goto(postUrl, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
console.log("[頁面導航] 成功。");
await $page.waitForSelector("[data-pressable-container=true]", {
timeout: 20000,
});
console.log("[內容等待] 偵測到應用程式容器,開始提取資料");
// --- [ 3. 提取並解析內嵌 JSON 資料 ] ---
const hiddenDatasets = await $page.$$eval(
'script[type="application/json"][data-sjs]',
(scripts) => scripts.map((s) => s.textContent)
);
console.log(
`[資料提取] 找到 ${hiddenDatasets.length} 個 JSON 資料區塊,開始過濾...`
);
for (const hiddenDataset of hiddenDatasets) {
if (!hiddenDataset.includes(`"code":"${postCodeFromUrl}"`)) {
continue;
}
console.log("[資料過濾] 找到包含目標貼文代碼的資料區塊!");
const data = JSON.parse(hiddenDataset);
const threadItems = nestedLookup(data, "thread_items");
if (!Array.isArray(threadItems) || threadItems.length === 0) {
continue;
}
const allThreads = threadItems.flat().map((t) => parseThread(t));
const mainThread = allThreads.find((t) => t.code === postCodeFromUrl);
if (mainThread) {
const authorUsername = mainThread.username;
console.log(
`[作者過濾] 主貼文作者為: ${authorUsername}。將只保留此作者的回覆。`
);
const replies = allThreads.filter((t) => {
// 回覆必須滿足兩個條件:
// 1. 它不是主貼文本身。
// 2. 它的作者與主貼文的作者相同。
return t.code !== postCodeFromUrl && t.username === authorUsername;
});
console.log(
`[解析成功] 精準定位到主貼文 (${mainThread.code}),找到 ${replies.length} 則來自原作者的回覆。`
);
const result = {
thread: mainThread,
replies: replies, // 這是已被過濾後的回覆列表
};
// --- [ 4. 格式化並回傳結果 ] ---
return [
{
json: {
...inputData,
...result,
},
},
];
}
}
throw new Error(
"無法在頁面中找到目標貼文的資料。可能是貼文不存在,或頁面結構已變更"
);
} catch (error) {
console.error(`[抓取失敗] 在處理 ${postUrl} 時發生錯誤:`, error.message);
throw new Error(`抓取失敗: ${error.message}`);
}
為了讓主程式碼更簡潔,我們先定義兩個輔助工具函式。
nestedLookup(obj, key)
: 這個函式會在一個複雜的、巢狀的物件或陣列中,遞迴地找出所有符合指定 key
的值。由於 Threads 頁面原始資料結構複雜,這個函式能幫助我們輕鬆地撈出所需的資料區塊。
parseThread(data)
: 這個函式是我們的資料清洗器。它接收單一貼文的原始資料物件,從中提取我們感興趣的欄位(如:文字、作者、發布時間、圖片/影片連結等),並將它們整理成一個乾淨、格式化的物件。
這是爬蟲的主要執行流程,從接收輸入到回傳結果。
取得並驗證網址:
程式會先從輸入節點取得 url
。
接著,它會驗證這個網址是否以 https://www.threads.
開頭,確保輸入的資料是有效的。如果網址無效,流程將會中止並拋出錯誤。
導航至目標頁面:
使用 await $page.goto()
指令,讓 Puppeteer 控制的瀏覽器前往我們提供的 Threads 網址。
await $page.waitForSelector()
會等待頁面特定元素載入完成,確保我們在頁面準備好之後才開始抓取資料。
提取 JSON 資料:
這是最關鍵的一步。現代的網頁常常將頁面初始資料儲存在 <script type="application/json">
標籤中。相較於直接爬取畫面上的 HTML 元素,解析這些 JSON 資料更有效率且資料更完整。
$page.$$eval()
指令會找到所有符合條件的 script
標籤,並將其內容提取出來。
解析與過濾:
定位貼文: 腳本會從網址中解析出貼文的唯一代碼 (postCode
),然後走訪所有抓取到的 JSON 資料區塊,直到找到包含該代碼的區塊。
提取資料: 找到目標後,使用前面定義的 nestedLookup
函式來撈出所有貼文項目 (thread_items
)。
篩選作者回覆:
首先,腳本會找到我們的主貼文,並記錄下作者的 username
。
接著,它會過濾所有的貼文,只保留那些 username
與主貼文作者相同,且不是主貼文本身的回覆。
這個過濾步驟,確保我們只備份到「原作者」的補充說明,排除了其他人的留言。
回傳結果:
最後,腳本將整理好的主貼文 (thread
) 和作者回覆串 (replies
) 組合成一個物件。
這個物件會被回傳到下一個節點,完成我們的備份任務
最後會輸出類似這樣的結構資料
{
"url": "https://www.threads.net/...",
"thread": {
"text": "這是主貼文的內容...",
"published_on": 1718712000,
"username": "user",
"images": ["url1.jpg", "url2.jpg"],
"videos": []
},
"replies": [
{
"text": "這是作者的第 1 則回覆...",
"username": "user"
},
{
"text": "這是作者的第 2 則補充說明...",
"username": "user"
}
]
}
透過這個自動化流程,你現在有了一個強大的工具,可以輕鬆地將任何 Threads 貼文及其作者的完整回覆,保存為一份結構化的 JSON 資料,方便後續的查閱或應用