Mozilla's Readability 是 Firefox Reader View 背後抽取器的獨立 JavaScript 移植版。它以 Apache-2.0 套件 @mozilla/readability 的形式發布,會直接從即時 DOM 中挑選文章內容。因此在 Node 環境下,它需要像 jsdom 這樣的 DOM 實作。它不會抓取網頁、執行頁面 JavaScript,也不做結構化欄位抽取。
本次測試使用的是 npm latest 的 0.6.0 版本,發布於 2025 年 3 月 3 日;我在 2026 年 7 月 27 日檢查時,該倉庫已有 11,361 顆星(最後更新是 2026 年 7 月 9 日,因此 main 分支明顯領先已發布套件)。我是在 Node v22.22.3、macOS arm64、搭配 jsdom 29.1.1 進行測試,使用 22 個專門建立的標註 HTML fixtures;本文的所有數據都來自這套環境。實際上手後,它是這個類別裡門檻最低的工具:安裝只要兩分鐘、沒有二進位檔、沒有瀏覽器快取、每次重跑輸出都一模一樣。它有趣的地方不在於怎麼操作,而在於它的失敗模式幾乎都能從原始碼裡那幾個常數預測出來,而且其中一個常數比文件寫得更有決定性。
在 22 個受控的合成 fixtures 中,Readability 成功找回了全部 74 個已標註文章區塊。這個封閉結果並不代表它不會漏掉正文:公開的真實頁面基準測試顯示其召回率為 0.982,而本文列舉的已知問題型態在這裡都沒有重現。最明顯的 fixture 失敗,是源碼層級的 link-density 門檻在 0.25 下放行了多餘的兄弟節點內容。其表面嚴重程度,會隨測試環境而改變。
readability.js 到底是什麼,以及它不是什麼
Readability 本質上是對 DOM 進行規則式評分的流程。它會走訪候選元素、替每個元素計算內容分數、把分數往祖先節點傳遞,挑出分數最高的子樹,接著再做清理,把看起來像頁面邊欄或版面元素的內容剔除。這就是全部的文章抽取策略——沒有模型、沒有訓練資料、沒有針對單一網站的規則。也因為這樣,它能在從未見過的頁面上運作;而它的失敗又能從原始碼預先推得出來,這正是它最有意思的地方。
它有三件事不是,而這三件事也最常讓人誤會:
- 不是抓取器。 它吃的是
document,不是 URL。抓取、重試、反爬、Header 都得你自己處理。 - 不是渲染器。 不會執行 JavaScript。你傳進去的 DOM 內容是什麼,它就只看到什麼。
- 不是結構化抽取器。 你拿到的是
title、byline、excerpt、content(HTML)、textContent、length、siteName。沒有 schema、沒有型別化欄位,也沒有{name, price}。
四個常數,扛起了大部分工作

直接看 node_modules 裡的 Readability.js,你能比看文件更快掌握它的行為。這個函式庫的大部分表現,其實都由四個機制主導:
- 每段內容的分數公式:
1 + (commaCount + 1) + min(floor(len / 100), 3)。少於 25 個字元的段落完全不算。分數會往祖先節點傳遞並逐層折減:父節點拿滿分、祖父節點拿一半、更深一層的祖先則按level · 3計算。 DEFAULT_CHAR_THRESHOLD = 500—— 這是「成功解析」所需的最短文章長度。低於這個值時,系統會用更少清理步驟重新抓取一次。unlikelyCandidates正則式 —— 會匹配 class 或 id 中含有comment、footer、menu、related、sidebar、social、sponsor等字串的節點。命中的節點會在評分前先被移除。grabArticle裡的兄弟節點追加門檻 —— 主候選節點選出後,會評估它的兄弟節點是否也要納入。若兄弟節點本身分數達標,或nodeLength > 80 && linkDensity < 0.25,或nodeLength < 80 && nodeLength > 0 && linkDensity === 0 && 其中包含句點,就會一起收進來。
Link density 的計算方式是 Σ(linkText.length · coef) / textLength;其中裸 # 連結的 coef = 0.3,其他情況則為 1。本文測到的兄弟節點外洩,就是這道門檻在作怪;但它並不是整個抽取算法的全部。
安裝方式,以及簡報裡沒說的依賴
npm install @mozilla/readability jsdom,然後就能跑。兩分鐘、沒有二進位檔、沒有安裝後下載、也不需要瀏覽器常駐快取。就安裝成本而言,這東西幾乎完美。
但所謂「零依賴」講的是算法,不是執行環境。Readability 需要操作一個即時的 document,在 Node 下代表你得自己提供 DOM 實作——這裡我用的是 jsdom 29.1.1。jsdom 並不輕,對多數流程來說,它才是迴圈裡最主要的成本,而不是抽取本身。這部分要預留預算。
還有一個我重跑一次才踩到的坑:Readability.parse() 會直接改動它收到的 DOM。 同一個 jsdom document 如果 parse 兩次,第二次看到的會是已經被第一次拆過的文件。我的測試框架每次 parse 都會建立全新的 jsdom。如果你為了省時間而重用同一份 document,之後要提 bug 的大概就是這個。
我是怎麼測的
我沒有拿它去打線上新聞站。真實網站只能給你一個分數,卻無法告訴你為什麼;而對 heuristic 來說,「為什麼」才是價值核心。於是我改為生成 22 個 HTML fixtures,共含 91 個標註區塊(74 個文章區塊、17 個 boilerplate),而且每個區塊裡的每個詞都會加上該區塊專屬的 sentinel 字串。各區塊詞彙彼此不重疊,因此任何被抽出的 token 都能精準對應到唯一區塊;所謂「找回」或「外洩」就成了精確的集合判定,而不是模糊比對。
抽取步驟與評分步驟刻意分開。Node runner 只輸出原始抽取文字、isProbablyReaderable 布林值,以及實測的 link density。所有 precision 與 recall 都在事後由另一支腳本,對照標註資料計算。整個框架裡沒有任何 metric 常數是手動寫死的,這也是我唯一信得過自己數據的方式。
接著我把完全相同的 bytes送去 trafilatura 2.1.0 做同場對照。每個 fixture 都跑了三次;22 個案例每次都回傳 byte-identical 的文字。
你可以逐步檢視每個階段,而不是只看總表。tests/build_fixtures.mjs 會產生標註好的 HTML 與 ground truth;tests/run_readability.mjs 會記錄抽取結果與 predictor 輸出;tests/metrics.py 則在事後對這些紀錄打分。原始 Readability 輸出、計算後的指標、以及相同輸入對照結果,都保留在 artifacts/raw/。這種分層很重要:如果結果可疑,你能分辨到底是 parser 回傳了意外文字、標註集有問題,還是評分程式誤判。這套資料能驗證本文的主張,但它仍只是 harness 驗證,不代表你的實際部署頁面會有相同的失敗分布。
範圍限制是真實而且關鍵的:這些都是合成控制頁面,不是真實世界語料。 真實頁面的權威數據來自公開的 article-extraction-benchmark,它在大約 181 個真實頁面上,對 readability_js 0.6.0——也就是本文測試的同一版本——評出 word-F1 0.947 ± 0.005(precision 0.914 ± 0.008,recall 0.982 ± 0.003)。這個數據是引用,不是我重跑的。
本文使用的是 current-release benchmark rows;已被取代的歷史 rows 已排除。 這些受控 fixtures 追加的是逐區塊拆解,能指出哪些內容形狀會觸發哪些規則,而不是取代公開的真實頁面語料。
在合成 fixtures 裡,召回率是完美的
74 / 74。Readability 在全部 22 個 fixtures 裡,沒有漏掉任何一個已標註文章區塊——而在那 11 個同時混有 article 與 boilerplate 的乾淨合成頁面裡,micro-averaged token recall 也達到 1.000。沒有任何文章句子消失。
不過這個結果要附上兩個但書:
這些都是乾淨的、單欄的合成頁。真實文章常常層級更深、正文中段會插入廣告,甚至會因為評分產生雜訊而漏掉開頭段落——這類漏失已經在追蹤器裡有紀錄(issue #437、#901,以及在 #922 裡的「表格前內容被漏掉」案例)。我的 fixtures 沒有觸發這些問題,所以我不是在說它們已修好,而是在說我的測試沒碰到它們。到了真實頁面,這個版本的 benchmark recall 是 0.982,不是 1.000。
即便如此,這個結果的方向依然很有價值。Readability 的問題不是把你的文章整篇丟掉,而是它還帶了多少不該一起帶走的東西。
precision 數字,以及為什麼它需要三個標籤

一個數字最好引用,也最難辯護。在那 11 個混合 fixtures 裡,Readability 保留了 17 個 boilerplate 區塊中的 5 個,外洩率為 0.294。
但這不是真實世界的外洩率。三種不同的測法,衡量的是三件不同的事,而且只有其中一種能代表一般頁面:
| 數字在衡量什麼 | 結果 |
|---|---|
| 刻意加權的 fixture 集——11 個混合頁中有 6 個是專門設計來對抗 sibling gate 的 | 保留 17 個 boilerplate 區塊中的 5 個(0.294) |
唯一一個較接近真實的頁面——以 <article> 為主體,外圍包著 nav、ad banner、sidebar、comments、footer,再加一個中性 class 的 promo | 6 個 chrome 區塊中清掉 5 個;留下 1 個 |
| 約 181 個真實頁面,公開 benchmark(不是我的測試) | precision 0.914,recall 0.982,word-F1 0.947 —— readability_js 0.6.0 |
第一列請把它當壓力測試,不是預測。Readability 在實際世界裡並不會漏掉 29% 的 boilerplate。 在那個較接近真實的頁面上,所有帶有 unlikelyCandidates 正則會命中的 class 的內容——nav-menu、ad-banner、sidebar、comments、site-footer——都被乾淨移除,五個全清掉。唯一留下來的是我刻意設計、能躲過這個正則的那一塊。
0.25 門檻:boilerplate 清除從這裡停止
兄弟節點追加規則在原始碼裡有寫明。據我所知,沒有人實測過它到底在哪裡翻轉。所以我做了一條梯度:在 <article> 外面放一個 class 中性的 <p class="teaser-block">,內文是一篇明顯會勝出的四段式文章,而唯一變動的只有 promo 的長度與 link density。這些密度值是用 Readability 自己的公式實測出來的,不是憑空假設:
| Promo 區塊 | 內文字數 | 是否超過 80 字元 | 實測 link density | 結果 |
|---|---|---|---|---|
| 完全沒有連結 | 126 | 是 | 0.000 | 保留 |
| 一個短連結 | 126 | 是 | 0.143 | 保留 |
| 一個較長連結 | 126 | 是 | 0.278 | 移除 |
| 一半文字都做成連結 | 126 | 是 | 0.476 | 移除 |
| 單句、以句點結尾 | 60 | 否 | 0.000 | 保留 |
| 同樣文字,但沒有句點 | 59 | 否 | 0.000 | 移除 |
源碼條件使用的是 0.25 門檻;我量到的樣本剛好夾住它,0.143 會保留、0.278 會移除。另一條分支則會保留一個 60 字元、以句點結尾的句子,但移除同樣內容、沒有句點的 59 字元版本。文章召回在兩條分支裡都維持 4/4,因此這組樣本主要是在隔離 precision 的影響。
放到測試框架外來看,這個門檻的意思其實是:長、低連結密度、語氣中性的文章旁文字,也算文章。 這會把很多其實不是文章的東西一起納入——例如以段落形式寫的「延伸閱讀」、電子報推廣、編輯註記,或是行銷團隊寫成完整句子、只為追蹤而把連結拿掉的 sponsored teaser。
在 RAG 索引裡,這種低連結密度的 promo 段落可能會變成可被抽取的 chunk,進而讓檢索或生成模型把它當成正文。原始碼規則使這種失敗模式變得合理;但這次評測並沒有做端到端檢索或模型引用的評估。
如果你要做站點層級的硬過濾,可以先在抽取前過濾已知來源 DOM 容器、保留來源節點的祖先關係以便序列化前比對,或在事後套用經過嚴格驗證的文字模式過濾。只看回傳的 HTML,未必還能保留某個節點原本是否位於主要容器之外。兄弟節點門檻無法透過公開選項調整。
這些 fixtures 顛覆的三個假設
這些 fixtures 推翻了三個常見假設:charThreshold 會拒絕短文章、語意標籤是必要的、以及短非散文內容會被丟掉。下面的證據才是重點;不需要先做 preregistration 才能成立。
charThreshold = 500 不是懸崖
坊間常以為少於 500 字元的文章會回傳 null。其實不是。我把正文長度從 120 字元掃到 1500 字元,並分別測了 charThreshold 200、500 與 1000:
| 正文長度 | 各 threshold 都成功解析 | 抽取長度 |
|---|---|---|
| 120 | 是 | 161 |
| 300 | 是 | 342 |
| 460 | 是 | 509 |
| 520 | 是 | 569 |
| 800 | 是 | 841 |
| 1500 | 是 | 1555 |
完全平坦。三種 threshold 設定下,所有正文長度的抽取結果都一樣。這個門檻不是在控制回傳值,而是在決定要不要把 cleanup flags 拿掉再重跑一次抓取;如果頁面本來就乾淨,沒有東西可清,結果自然一樣。真正的 null 邊界是「完全沒有可抽取文字」。
而這裡真正會出問題的,反而比 false null 更糟。我餵它一個接近空白的頁面——只有一個 nav bar 和四個字的簡介。它照樣成功回傳,而且回傳的「文章」包含 nav。也就是說,當頁面根本沒有真正文章時,Readability 會把 boilerplate 當文章交給你。如果你在大規模爬取時,把非 null 結果直接當成「這頁有內容」,那個假設是錯的。
語意標籤不是關鍵
我原本預期,拿掉結構輔助後召回會下降。結果同一段文章,換兩種外殼:一個是 <main><article><h1> 加上語意清楚的 class 名稱;另一個則是 <div class="x1">,段落也只是裸 <div>。結果是:兩種情況文章區塊都找回 4/4,boilerplate 都是 0 外洩。 當文章明顯就是頁面上最密集的文字塊時,靠字數和逗號數的評分就能把它找出來,不需要語意標籤幫忙。說「Readability 需要 <article> 標籤」其實是坊間傳言。
這個說法真正誠實的限制是:我的頁面只有一個明顯內容塊。語意標籤真正可能發揮作用的地方,是兩個競爭中的密集子樹;這種 tie-break 我沒有測。
非散文內容會完整保留
「少於 25 個字元的段落不計分」這條規則,原本讓我以為表格和圖說會有損失。結果又錯了——那條規則只影響候選節點評分,不影響保留。一旦某個容器勝出,裡面的內容全都會一起帶走:
| 文章內的內容類型 | Readability | trafilatura |
|---|---|---|
| 敘述段落(×2) | 保留 | 保留 |
| 資料表格儲存格(×2) | 保留 | 保留 |
<pre> 程式碼區塊 | 保留 | 保留 |
少於 25 字元的單行 <p>(×2) | 保留 | 保留 |
<figcaption> | 保留 | 移除 |
| 總計 | 8/8 | 7/8 |
這是 heavier-handed cleaner 會輸的一個維度。如果你的頁面是文件、教學,或任何包含程式碼區塊與圖說的內容,Readability 這種「整個勝出子樹一起保留」的行為反而是優點。
isProbablyReaderable 會說不,但 parse() 會說可以

README 建議在正式 parse 前,先呼叫 isProbablyReaderable(doc) 當作便宜的預檢。我實測時,這個門檻拒絕了三種頁面形狀,但 parse() 都照樣處理成功:
| 頁面形狀 | Predictor 判定 | parse() | 需調哪個參數才可能改善 |
|---|---|---|---|
內容只存在於 <li> 元素中 | false | 成功 | 沒有——在 minScore 1–80 與 minContentLength 40–200 的所有組合都仍然是 false |
| 10 個段落,每個都少於 140 字元 | false | 成功 | minContentLength ≤ 100(minScore 無效) |
| 1 個 408 字元的段落 | false | 成功 | minScore ≤ 10(分數約為 16.4) |
| 正常文章(對照組) | true | 成功 | — |
這三種失敗有三種不同原因,而且只有兩種能調。<li> 這種情況是結構性的:predictor 只會評分 p、pre、article 節點(以及 div > br 的父節點),所以如果頁面內容全在 list item 裡,它根本打不到目標、分數是零,而且怎麼調 threshold 都救不回來——這種頁面形狀在 issue #662 裡早就有人報告。那種很多短段落的頁面,則是因為 minContentLength 先把每段都跳過了,導致十個看起來很夠長的段落最後總分還是零;把這個值調低就能解,調 minScore 沒用。單段落那個案例則是算術問題:分數是 sqrt(408 − 140) ≈ 16.4,低於預設 minScore 20 —— 一個孤立段落要自己達標,得有 140 + 20² = 540 字元。
README 的確有提醒 predictor 可能產生 false negatives。我要補上的實務建議是:不要把它當唯一門檻。 如果某頁很重要,就 parse 它,然後看結果長度。相較於你已經為 jsdom 付出的成本,parse 本身其實不算貴。
相同 bytes,兩個抽取器
把同一組 fixtures 丟給 trafilatura 2.1.0,讀法比把兩組不同測試環境量出來的數字硬擺在一起更乾淨,因為輸入本身是逐 byte 相同的:
| 指標(11 個混合 fixtures) | @mozilla/readability | trafilatura |
|---|---|---|
| 文章區塊召回率 | 1.000 | 1.000 |
| 保留的 boilerplate 區塊 | 5/17 (0.294) | 1/17 (0.059) |
| Token F1(micro) | 0.948 | 0.969 |
| 非散文內容召回率 | 8/8 | 7/8 |
| 極短文章(120 字元)token F1 | 0.800 | 0.571 |
這些 fixtures 裡沒有哪個工具是全面勝出的。trafilatura 留下的兄弟節點更少,而 Readability 則保留了更多短內容與非散文內容。兩者的絕對 token precision 都會因為未標註的標題文字而被拉低,所以 block-level 的外洩數量才是更直接的訊號。公開的真實頁面 benchmark 恰好也把它們的 word-F1 排得差不多,但語料與指標都不同,這不構成跨測試環境驗證。
簡單談 robustness:我還跑了一個刻意弄髒的版本,與標準頁面內容相同,但加入未關閉的 <p>、錯誤巢狀的 <b>/<i>,以及一個多出來的 </div>;結果 recall 仍是 3/3、零外洩,和語法正確版本一致。這部分功勞要歸給 jsdom 的 HTML5 tree builder,它會在 Readability 看到內容之前先把錯誤修好。沒有任何 fixture 讓 parser 當掉。
優點與缺點
優點
- 文章召回這項能力很強:22 個合成 fixtures 裡找回 74/74 個已標註區塊,混合組的 token recall 為 1.000。
- 正則類別的頁面家具清除可靠——真實頁面上的 nav、ad banner、sidebar、comments、footer 都被清掉了(6 個裡清了 5 個)。
- 非散文內容完整保留:表格、
<pre>程式碼、圖說、以及少於 25 字元的行都保住了(8/8),而 trafilatura 會丟掉圖說。 - 不依賴語意標記——中性化的
<div>文章,分數和<article>/<main>版本完全一致。 - 不會誤殺短文章:乾淨內容可一路抽到 120 字元,而且在
charThreshold200/500/1000 下結果都相同。 - 完全可重現:22 個 fixtures 三次執行都回傳完全相同的文字。
- 安裝只要兩分鐘、Apache-2.0 授權,而且 npm 上的版本就是我實測的版本(0.6.0),沒有過時問題。
缺點
- 兄弟節點追加門檻可以被利用:長、低連結密度、中性 class 的 promo 文本,和正文幾乎無法區分,會在
linkDensity < 0.25時一起被帶走。 - 在內容稀少的頁面上,它會把 boilerplate 當成文章回傳,而不是
null—— 那個近乎空白的 fixture,回來時連 nav bar 都被算進正文了。 isProbablyReaderable會對三種不同頁面形狀產生 false negatives,其中一種怎麼調都沒救。- 執行時需要完整 DOM——「零依賴」這種說法會遮住 jsdom 的成本,而那才是迴圈中的大頭。
parse()會改動輸入 document,所以每個頁面都得重建 DOM。- 不抓取、不渲染 JavaScript、也不輸出結構化資料。它只是流程中的一段,不是整條流程。
- 追蹤器裡提到的真實頁面漏失(開頭段落與表格前內容)在我的 fixtures 裡都沒重現,所以我不能告訴你它們到底是罕見,還是只是我的頁面沒碰到。
誰適合用,誰該跳過
如果你手上已經有 HTML,想在純 JavaScript、Node 服務裡把文章抽出來,而且不想多帶一個 Python 依賴,那就可以考慮 Readability。(我沒有正式收集時間分布,所以除了「jsdom 建構才是迴圈裡的大頭,不是抽取本身」之外,不做任何速度宣稱。)Reader mode 功能、離線文章歸檔、電子報、clean view 按鈕、瀏覽器擴充套件、帶程式碼區塊與圖說的文件流程——這就是它的主場,而召回率數字也證明這是條不錯的路。它的行為還能直接從原始碼讀懂;當你得向同事解釋為什麼某一塊內容被保留下來時,這比你想像中更重要。
如果你的評分標準是 boilerplate 清除精準度,那就別用它;尤其是你把輸出餵進 LLM 索引,任何漏進去的 promo 段落最後都可能變成可檢索 chunk。若頁面內容是 client-side 才渲染,也不適合,因為它只讀你給它的 DOM,並不執行 JavaScript。若你要的是 {title, price, sku} 而不是敘述文字,也不適合——任何設定都無法把內容抽取器變成 schema 驅動抽取器。而如果你處理的頁面會問「這頁到底有沒有文章」,那就不要把非 null 當成答案。
替代方案,以及 Thunderbit 生態系的位置
這裡不是在貶低 Mozilla 維護的免費 Apache-2.0 函式庫——Readability 是基礎設施,已經在 Firefox 裡跑了很多年;對 reader-mode 抽取來說,它就是參考實作。若你想看更完整的領域比較,我在 open-source scrapers roundup 有持續整理,也在 the best web scraping tools 做了更廣的調查。
若想看同一批 fixtures 在六個抽取器上的比較,請參考 six-library extraction comparison。
作者註記:Thunderbit 是我們提供的受管方案,主打 URL 輸入、渲染與抽取。它沒有參與這次 fixtures 測試,因此不代表有可比對的品質聲明。真正的分野在於:你已經有 DOM,想本地做文章抽取;還是你需要抓取、渲染與結構化輸出,並把它們當服務來運作。自架雖然省下供應商使用費,但仍要承擔基礎設施與維護成本。
老實說,這是個取捨:Readability 免費、透明,而且完全由你掌控——你可以直接看到決定輸出的那條門檻,這是受管 API 做不到的。受管方案要付費,機制也比較封閉,但它幫你處理了抓取、渲染、結構化這三段你原本得自己拼起來的流程。如果你想看這條光譜 AI 輔助的另一端,我在別處也寫過 用 AI 抓取任何網站 與 AI 爬蟲。到底選哪個,就看你實際想自己掌握哪些階段。
結論
如果你已經有 DOM、在跑 JavaScript/Node,而且寧可偶爾多帶一些兄弟節點,也不想過度刪除,Readability 是個值得選的工具。在這套 fixtures 裡,它找回了全部 74 個已標註文章區塊,也保留了表格、程式碼與圖說。不過這個結果仍受限於合成的單欄頁;公開真實頁面的 recall 是 0.982,已知的開頭與表格鄰接漏失沒有重現,而且內容稀少的頁面可能會把 boilerplate 當文章回傳。
但要正確理解它的弱點。它在這些 fixtures 裡最大的失敗面向是 precision,而且問題就出在原始碼裡一條明確、可追蹤的規則:兄弟節點若超過 80 字元且 link density 低於 0.25,就會被追加進文章,不管它是不是正文。我實際看著它在相同文字下,0.143 與 0.278 之間翻轉。真實頁面 benchmark 顯示 precision 0.914、recall 0.982。如果你把抽出來的文字餵給之後會引用它的模型,請同時檢查保留下來的 boilerplate 與被漏掉的正文,而不要假設任一種錯誤都不存在。
試試 Thunderbit 進行網頁資料抽取 Get Started Free
常見問題
Mozilla Readability 會移除所有 boilerplate 嗎?
不會,而且數字會依你怎麼量而差很多。在公開真實頁面 benchmark 上,readability_js 0.6.0 的 precision 是 0.914——也就是說,它回傳的內容裡大約有 8.6% 不是正文。在我那個較接近真實的測試頁上,它清掉了 6 個 chrome 區塊中的 5 個(nav、ad、sidebar、comments、footer 全沒了),只留下那個 class 中性的 promo 段落。在我刻意加權、專門設計來擊破 heuristic 的 fixture 集裡,它保留了 17 個區塊中的 5 個——那個數字是壓力測試,不是真實世界比率。
在 Node 裡使用 readability.js 需要 jsdom 嗎?
需要,或者其他 DOM 實作。Readability 雖然是純 JavaScript,但它操作的是即時 document,所以在 Node 下你得自己提供 DOM——我的環境裡是 jsdom 29.1.1。所謂「沒有依賴」指的是算法本身,不是執行環境。另外也要注意,parse() 會改動它收到的 document,所以每個頁面都應該重新建立一個 DOM,不要重用同一份。
charThreshold 選項到底在做什麼?
不是多數人以為的那樣。它不會讓短文章回傳 null——我一路抽到 120 字元都仍能成功,而且在 charThreshold 200、500、1000 下,抽取長度都一樣。這個門檻控制的是 parser 要不要在拿掉清理標記後重新跑一次抓取;如果頁面本來就乾淨,沒有東西可清,輸出自然相同。真正的 null 只會出現在完全沒有可抽取文字的頁面,而就算只有 nav 的頁面,回來的結果也仍然不是 null,只是把 nav 當成文章了。
我應該在 parse() 前先呼叫 isProbablyReaderable 嗎?
把它當提示,不要當門閘。它在三種頁面形狀上回傳了 false,但 parse() 仍然成功:內容只在 <li> 元素裡、10 個都少於 140 字元的段落、以及一個 408 字元的單段落。<li> 那種情況靠調參救不了,因為 predictor 只評分 p、pre 與 article 節點;很多短段落那種情況要把 minContentLength 調低;單段落那種情況則要把 minScore 調低,因為一個孤立段落必須長到 540 字元才能通過預設門檻。只要頁面重要,就直接 parse,然後檢查結果。
文章抽取要選 Readability 還是 trafilatura?
在相同的 fixture bytes 下,trafilatura 留下的 boilerplate 比較少(1/17 區塊 vs 5/17),而 Readability 找回了更多短內容,也保留了 trafilatura 會丟掉的 <figcaption>。選擇要看你對錯誤的容忍度與執行環境。公開 benchmark 是另一組脈絡,不是這次 fixture 結果的驗證。


