はじめに
Webページから「記事の本文だけ」を取り出したいと思ったことはありませんか?
RSSリーダーを自作したい、記事を要約するAIパイプラインを組みたい、ブックマークサービスにプレビューを表示したい……。そんなとき、いざHTMLを取得してみると、ナビゲーション、サイドバー、広告、フッターなどのノイズだらけで、肝心の本文を切り出すのは意外と骨が折れます。サイトごとにDOM構造は違うので、CSSセレクタを頑張って書いてもすぐに破綻してしまいます。
そこで活躍するのが Article-Extractor(@extractus/article-extractor)です。URLを渡すだけで、記事の本文・タイトル・著者・アイキャッチ画像・読了時間までまとめて抽出してくれる、まさに「本文抽出の専門家」のようなライブラリです。
Article-Extractorとは
Article-Extractorは、extractusプロジェクトが開発しているNode.js向けの記事抽出ライブラリです。URLまたはHTML文字列を入力すると、ページの構造を解析して本文とメタデータをJSONとして返してくれます。MITライセンスで公開されており、Node.js 20以上で動作します(本記事執筆時点の最新バージョンは8.1.0です)。
主な特徴
- URLを渡すだけのシンプルなAPI -
extract(url)の1行で、本文・タイトル・著者・画像などを一括取得できます - メタデータも同時に抽出 - OGP画像、favicon、公開日、読了時間(ttr)まで自動で解析してくれます
- HTMLのサニタイズ内蔵 - 抽出したコンテンツは
sanitize-htmlベースで清浄化されるため、そのまま表示に使いやすい安全なHTMLが得られます - カスタマイズ可能 - サイトごとの変換ルール(transformations)やfetchオプションを追加して、抽出精度を細かくチューニングできます
インストール
お使いのパッケージマネージャーでインストールできます。
# npm
npm i @extractus/article-extractor
# yarn
yarn add @extractus/article-extractor
# pnpm
pnpm install @extractus/article-extractor
# bun
bun add @extractus/article-extractor
ESMパッケージなので、import構文で読み込みます。
基本的な使い方
最もシンプルな例から見ていきましょう。URLを渡すだけです。
import { extract } from '@extractus/article-extractor'
const article = await extract('https://example.com/some-article')
console.log(article)
抽出に成功すると、次のような構造のオブジェクトが返ってきます。
{
url: '記事のURL',
title: '記事タイトル',
description: '記事の概要文',
image: 'アイキャッチ画像のURL',
author: '著者名',
favicon: 'faviconのURL',
content: '<p>本文のHTML...</p>',
published: '公開日時',
type: 'article',
source: 'サイト名',
links: ['正規URLなどのリンク一覧'],
ttr: 180 // 読了時間(秒)
}
タイトル、本文、画像、さらには読了時間の推定値まで、たったこれだけのコードで手に入ります。自前でOGPタグをパースしたり、本文領域を推測するロジックを書いたりする必要はもうありません。
HTML文字列から抽出する
すでにHTMLを取得済みの場合や、独自のHTTPクライアントを使いたい場合は、extractFromHtmlが使えます。
import { extractFromHtml } from '@extractus/article-extractor'
const res = await fetch('https://example.com/some-article')
const html = await res.text()
// 第2引数のURLは相対リンクの解決などに使われます
const article = await extractFromHtml(html, 'https://example.com/some-article')
console.log(article.title)
オプションで抽出をチューニングする
extract関数は第2引数にparserOptions、第3引数にfetchOptionsを受け取ります。
import { extract } from '@extractus/article-extractor'
const article = await extract(
'https://example.com/some-article',
{
// 読了時間の計算に使う「1分あたりの単語数」(デフォルト: 300)
wordsPerMinute: 250,
// description の最大文字数(デフォルト: 210)
descriptionTruncateLen: 160,
// 本文とみなす最小文字数(デフォルト: 200)
contentLengthThreshold: 120,
},
{
// fetch に渡すオプション
headers: {
'user-agent': 'my-reader-app/1.0',
},
// AbortSignal でタイムアウトも設定できます
signal: AbortSignal.timeout(10000),
}
)
AbortSignal.timeout()と組み合わせれば、応答の遅いサイトで処理が止まってしまう事故も防げます。
サイト固有の変換ルールを追加する
特定のサイトで抽出結果が今ひとつなときは、addTransformationsでサイト専用の前処理・後処理を定義できます。
import { addTransformations, extract } from '@extractus/article-extractor'
addTransformations([
{
patterns: [/example\.com\/*/],
// 抽出前にDOMを整形する
pre: (document) => {
// 不要な要素をあらかじめ取り除く
document.querySelectorAll('.ad-block, .related-posts').forEach((el) => {
el.parentNode.removeChild(el)
})
return document
},
// 抽出後のDOMを加工する
post: (document) => {
// 遅延読み込み画像の data-src を src に差し替える
document.querySelectorAll('img[data-src]').forEach((el) => {
el.setAttribute('src', el.getAttribute('data-src'))
})
return document
},
},
])
const article = await extract('https://example.com/some-article')
URLパターンごとにルールを登録できるので、よく取得するサイトだけ精度を上げる、といった運用が可能です。
実践的なユースケース
「あとで読む」サービスのプレビュー生成
ブックマークしたURLからタイトル・画像・読了時間を取り出して、カード表示用のデータを作る例です。
import { extract } from '@extractus/article-extractor'
async function createBookmarkPreview(url) {
try {
const article = await extract(url, {}, {
signal: AbortSignal.timeout(8000),
})
if (!article) return null
return {
title: article.title,
description: article.description,
thumbnail: article.image,
favicon: article.favicon,
readingMinutes: Math.ceil((article.ttr ?? 0) / 60),
}
} catch (err) {
console.error(`抽出に失敗しました: ${url}`, err.message)
return null
}
}
const preview = await createBookmarkPreview('https://example.com/some-article')
console.log(preview)
// => { title: '...', readingMinutes: 3, ... }
抽出できないページではnullが返ったり例外が投げられたりするため、実運用では上記のようにフォールバックを用意しておくのがおすすめです。
複数記事の一括取得
RSSフィードから集めたURL群をまとめて処理する例です。Promise.allSettledを使えば、一部の失敗が全体を止めません。
import { extract } from '@extractus/article-extractor'
const urls = [
'https://example.com/article-1',
'https://example.com/article-2',
'https://example.com/article-3',
]
const results = await Promise.allSettled(
urls.map((url) => extract(url, {}, { signal: AbortSignal.timeout(8000) }))
)
const articles = results
.filter((r) => r.status === 'fulfilled' && r.value)
.map((r) => r.value)
console.log(`${articles.length} / ${urls.length} 件の記事を抽出しました`)
抽出したcontentはサニタイズ済みのHTMLなので、そのままMarkdown変換ライブラリに渡して要約パイプラインの入力にしたり、全文検索エンジンに投入したりと、後段の処理につなげやすいのも嬉しいポイントです。
なお、Webページの取得にあたっては、対象サイトの利用規約やrobots.txtを尊重し、アクセス頻度に配慮することを忘れないようにしましょう。
まとめ
Article-Extractorを使うと、これまでサイトごとに書き分けていた本文抽出ロジックが、わずか数行のコードに置き換わります。
extract(url)だけで本文・メタデータ・読了時間まで取得できますextractFromHtmlで独自のHTTP処理とも組み合わせられますparserOptions/fetchOptions/addTransformationsで細かなチューニングが可能です- 抽出結果はサニタイズ済みHTMLなので、表示にも後段処理にも使いやすいです
RSSリーダー、ブックマークサービス、記事要約パイプラインなど、「記事の中身」を扱うアプリケーションを作るなら、まず最初に試してほしいライブラリです。npm i @extractus/article-extractorから、ぜひ手元で動かしてみてください。
