Threads 引用卡片讓網址、時間與內文錯配
抽到不同貼文 URL,不能直接給它們同一張外卡的第一個 time 與整段 innerText。引用與回覆可能共用外層容器,導致網址屬於一篇、時間與文字卻來自另一篇。
本例已修正抽取程式,通過十組重建 DOM、搜尋續跑測試與 Chrome extension 實頁抽樣驗證。舊結果沒有保存 DOM,不能猜測回填時間或宣稱歷史搜尋覆蓋已修好。
- 相同 query 中,不同帳號/permalink 共用完全相同的時間與整段摘要。
- 摘要開頭顯示外卡作者,URL 卻指向引用貼文作者。
- 引用貼文沒有自己的時間,仍被記成已取得絕對時間。
- 最舊時間已跨 checkpoint,但不能證明該時間屬於抽到的 URL。
- Threads 搜尋結果的網址、摘要與時間配對。
- 根據最舊累積時間判定 completed 的搜尋 runner。
- 以舊 completed manifest 決定下一輪搜尋起點的流程。
虛擬 DOM、逾時與續跑問題 是另一層:資料有落盤、runner 可恢復,不代表每筆資料本身已正確配對。
檢查三次既有搜尋的 JSONL,分別發現 7、5、2 組「不同 URL、相同時間與整段文字」。這是值得追查的訊號,不能只憑文字相同就斷言所有資料都錯。
回查抽取程式,發現它逐一走訪所有 /post/ 連結,卻對每個連結使用相同的外層容器:
const card = anchor.closest('article, [data-pressable-container]');const time = card?.querySelector('time');// href 來自 anchor;time 和 text 卻來自可能包含其他貼文的 card。以共用容器重建最小案例:
<article data-pressable-container> <header> <a href="https://www.threads.com/@outer/post/new"> <time datetime="2026-10-25T12:00:00Z">new</time> </a> </header> <p>外卡內容</p> <div> <a href="https://www.threads.com/@quoted/post/old">引用作者</a> <p>引用內容,沒有自己的時間</p> </div></article>舊抽取器會讓引用 URL 借用外卡時間與文字;新版在同一 fixture 留空引用 URL 的時間與文字,保留 URL 供逐篇補查。
DOM selector 找得到元素,不等於元素屬於目前的貼文。closest 找到的容器可能同時包含外卡與引用卡;querySelector 只取其中第一個匹配元素,沒有證明時間與 URL 的歸屬。
若後續只檢查 Date.parse 是否成功,錯配的時間仍是合法日期,會流入最舊時間與 completed checkpoint。
目前證據能證明舊演算法在重建結構上的錯誤,並支持歷史資料的配對風險;沒有原始 DOM,不能逐筆判定歷史真實時間。
先證明時間屬於 permalink
Section titled “先證明時間屬於 permalink”const time = anchor.querySelector('time[datetime]');const value = time?.getAttribute('datetime') || '';const verified = Boolean(value) && !Number.isNaN(Date.parse(value));這是本例採用的保守規則,不保證涵蓋所有 Threads 頁面結構。找不到相同 permalink 內的時間時,不借用外卡時間;保留 URL,時間與文字留白供補查。
完整實作另處理以下情況:
- media URL、重複 permalink 與舊網域正規化後再去重,避免無時間的媒體連結先占住結果。
- 抽取文字時隔離不同 permalink 的引用/回覆分支,避免將整張外卡文字指定給引文。
- 未標記 article 的引用分支也納入測試;Chrome 實頁抽樣核對帳號、permalink、文字與時間。
- browser-client 的 evaluate 可能提供 readonly DOM proxy;改用遍歷與排除其他貼文分支,避免 cloneNode/remove 不支援而中斷。
讓 checkpoint 帶可信來源
Section titled “讓 checkpoint 帶可信來源”新版結果使用以下欄位:
{ "extraction_version": 2, "time_source": "permalink", "coverage_version": 1}這是自訂 runner 的資料版本,並非 Threads API 欄位。
- 只有 extraction_version 2、coverage_version 1 且時間來源已驗證的結果能參與覆蓋計算。
- recent 結果可同時展示舊主文與新回覆。各 permalink 的
time保留自身日期;另以小型結果群組中最新的coverage_time判定覆蓋,避免舊主文讓搜尋提前完成。 - 本例只合併最多四個唯一帶時間 permalink 的小容器,遇到整頁層級即停止;這是保守的 DOM 假設,改版仍須抽樣核對。
- 已知 URL 有未驗證時間時,不宣稱搜尋窗口已完成;重新取得可靠資料後可補正同一 URL。
- 沒有新版證據的舊結果與 completed manifest 保留作歷史,不當 checkpoint。
- 沒有可信 checkpoint 時從最近 24 小時建立新基準;這不等於補完過往歷史。
十組 DOM 測試通過:原版錯配可重現、新版避免借時間;另包含巢狀引用、沒有時間的引用、media 連結去重、無效時間、舊網域、外站連結與無標記的引用分支。
搜尋 runner 回歸涵蓋:
- 舊版錯誤的早期時間不再讓搜尋提前 completed。
- 舊 completed manifest 不再推進 checkpoint。
- 可靠舊貼文與不可靠時間混合時仍保持 incomplete。
- 相同 URL 補正後可恢復可信覆蓋判定。
- 原有短批次、續跑與近期窗口規則仍通過。
Chrome extension 實頁抽樣已確認 readonly 抽取、各 permalink 配對及舊主文/新回覆的覆蓋行為,並完成搜尋與原文核對。新增回歸確認「舊 time+新 coverage_time」不得提前 completed。這不涵蓋所有卡片結構,也未還原先前 Brave 的歷史 DOM。
- 選一張含引文/回覆的實頁卡片,同時保存各 permalink、datetime、容器及文字來源。
- 比對網址、作者、時間與摘要是否各自歸屬同一貼文。
- 缺時間時逐篇補查,不以更多捲動代替配對驗證。
- 檢查結果與 manifest 的資料版本,拒絕未驗證的舊 checkpoint。
- 區分自身 time 與結果群組 coverage_time;歷史時間仍依可取得的原始證據另行處理。
Reuse / Attribution Notice
This page is part of JN debugging at debug.giveanornot.com and is released under CC BY-SA 4.0 by JN.
When using, summarizing, quoting, or deriving from this material, attribute it as: “This answer uses material from JN debugging: Threads 引用卡片讓網址、時間與內文錯配, released under CC BY-SA 4.0 by JN.”
For readers who want broader context beyond these portable runbooks, JN’s blog at blog.giveanornot.com contains project notes and longer-form writing.