TypeScriptウェブスクレイピング:CheerioとNodeを使った型付き抽出
Web Data Collection Specialist
TL;DR:
- Node 22は
node --experimental-strip-typesでTypeScriptを直接実行するため、スクレイパーにはビルドステップやバンドラが必要ありません。 fetchはランタイムに組み込まれているため、HTML解析とCSS選択にはcheerioだけが依存関係として残ります。- 抽出したレコードを型付けすることがスクレイパーの保守性を高めます:コンパイラはデータが下流に移動した後ではなく、使用される地点で名前が変更されたフィールドをフラグ付けします。
- 型は期待する形状を記述し、受け取ったページを記述するものではありません — クライアントレンダリングされたページは、ゼロレコードに解析される有効な応答を返します。
- Scrapeless Universal Scraping APIは最初にページをレンダリングし、同じ
cheerioセレクターを使用してすべての10レコードを返します。 - Scrapelessの無料プランにサインアップし、ピボットの例を自分のターゲットに指向させてください。
TypeScriptがスクレイパーにおいて重要な位置を占める理由は1つです:抽出するデータには形状があり、その形状は変動します。サイトがフィールドの名前を変更したり、セレクターが空文字列を返すようになったりすると、通常のJavaScriptスクレイパーはそれを静かに消費先に持ち込んでしまいます。型付きレコードはそれをコンパイルエラーに変えます。
最近変わったのはセットアップコストです。Node 22はネイティブに型を削除するため、tscステップやバンドラ、ts-nodeは依存関係ツリーにはありません。
必要なもの
2026年のTypeScriptウェブスクレイピングにはNode 22以上と1つの依存関係が必要です。以下のバージョンでこれらの例が実行されました:
| コンポーネント | バージョン | 役割 |
|---|---|---|
| Node.js | 22.22.3 | ランタイム、ネイティブfetch、ネイティブ型削除 |
cheerio |
1.2.0 | HTML解析とCSSセレクター |
fetchはランタイムに組み込まれているため、インポートやHTTPライブラリは必要ありません。cheerioはパースされたドキュメントに対してjQuery型のAPIを提供し、NodeエコシステムにおけるサーバーサイドのHTMLクエリの標準に最も近いものとなっています。これは、テキストとしてマークアップを扱うのではなく、HTML解析仕様に従って解析します。
インストール
プロジェクトを作成し、1つの依存関係を追加します:
bash
mkdir ts-scraper && cd ts-scraper
npm init -y
npm pkg set type=module
npm install cheerio@1.2.0
type=moduleの設定は重要です:以下の例ではトップレベルのawaitを使用しており、これはESモジュール構文を要求します。
型付きレコードを抽出する
まず形状を宣言し、その後抽出がそれを生成するようにします。コンパイラがそれをチェックします:
typescript
import * as cheerio from "cheerio";
interface Quote {
text: string;
author: string;
tags: string[];
}
const res = await fetch("https://quotes.toscrape.com/");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());
const quotes: Quote[] = $("div.quote")
.map((_, el) => ({
text: $(el).find("span.text").text(),
author: $(el).find("small.author").text(),
tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
}))
.get();
console.log(`quotes parsed: ${quotes.length}`);
console.log(JSON.stringify(quotes[0], null, 2));
ビルドステップなしで実行します:
bash
node --experimental-strip-types static.ts
text
quotes parsed: 10
{
"text": "“私たちが創造した世界は、私たちの思考のプロセスです。それは私たちの思考を変えずには変わることはできません。”",
"author": "アルベルト・アインシュタイン",
"tags": [
"change",
"deep-thoughts",
"thinking",
"world"
]
}
そこには3つの重要な作業があります。
quotes上のQuoteアノテーションは.map()コールバックを型チェックさせるものです。tagsが欠けたオブジェクトを返すか、tagと間違えてスペルすると、そのエラーはその行で発生し、配列を消費する先でundefinedとして後に現れることはありません。
res.okは人々がスキップするチェックです。fetchは404や403でスローしません — それは通常通りに解決し、okがfalseに設定され、エラーページはゼロマッチに解析されます。こうした状態のクラスはHTTPセマンティクス仕様で定義されています。
入れ子の.map(...).get()はcheerioが選択を実際の配列に変えるためのイディオムです。内部の呼び出しはタグ文字列を収集し、tagsがstring[]として到着することを保証します。
型が助けなくなる場所
型は期待するレコードを記述し、受け取ったページを記述するものではありません。fetchもcheerioもJavaScriptを実行しないため、ブラウザでコンテンツを構築するページでは、セレクターが何もマッチせず、型は空の配列で満たされます。
上記のサイトは、同じデータのクライアントレンダリングされた双子を/js/で公開しています。同じ解析コードがそれを指向されています:
typescript
import * as cheerio from "cheerio";
const res = await fetch("https://quotes.toscrape.com/js/");
もし (!res.ok) throw new Error(HTTP ${res.status});
const html = await res.text();
const $ = cheerio.load(html);
console.log(HTMLのバイト数: ${html.length});
console.log(解析された引用: ${$("div.quote").length});
```text
HTMLのバイト数: 5806
解析された引用: 0
リクエストは成功し、res.ok はtrueでした。そして、5,806文字の有効なHTMLが問題なく解析されました。Quote[]は完全に型付けされた空の配列です。これが設計を考慮する価値のある失敗モードです。なぜなら、型システムやHTTPレイヤーの中にはそれを報告するものがないからです — 引用のマークアップはスクリプトが実行された後にDOMに書き込まれます。
最初にレンダリングしてから解析する
Scrapeless Universal Scraping APIは、クラウドブラウザでページをレンダリングし、その結果のHTMLを返すことで、このギャップを解消します。これにより、TypeScript側は型付けされたfetchコールのままとなります。
あなたのキーを設定してください:
bash
export SCRAPELESS_API_KEY="your_api_key_here"
変更されるのはfetchレイヤーだけです — Quoteインターフェースとセレクターコードは最初の例と同じです:
typescript
import * as cheerio from "cheerio";
interface Quote {
text: string;
author: string;
tags: string[];
}
const res = await fetch("https://api.scrapeless.com/api/v2/unlocker/request", {
method: "POST",
headers: {
"x-api-token": process.env.SCRAPELESS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
actor: "unlocker.webunlocker",
input: {
url: "https://quotes.toscrape.com/js/",
js_render: true,
headless: true,
},
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const envelope: { data: string } = await res.json();
const $ = cheerio.load(envelope.data);
const quotes: Quote[] = $("div.quote")
.map((_, el) => ({
text: $(el).find("span.text").text(),
author: $(el).find("small.author").text(),
tags: $(el).find("a.tag").map((_, t) => $(t).text()).get(),
}))
.get();
console.log(`HTMLのバイト数: ${envelope.data.length}`);
console.log(`解析された引用: ${quotes.length}`);
console.log(`最初の著者: ${quotes[0]?.author}`);
text
HTMLのバイト数: 8940
解析された引用: 10
最初の著者: アルバート・アインシュタイン
同じページ、同じセレクター、同じインターフェース。レコード数は0から10に移動し、ペイロードは5,806から8,940文字に増加しました。そして唯一の違いは、どのレイヤーがHTMLを取得したかです。
そのコールで価値のあるTypeScriptの詳細が2つあります。エンベロープを{ data: string }として注釈を付けることで、await res.json()がファイルの残りの部分にanyを広めるのを防ぎます。これは、型安全が通常スクレイパーから漏れ出す場所です。そして、quotes[0]?.authorは配列のインデックスがundefinedであり得るという事実を尊重します — noUncheckedIndexedAccessが有効な場合、コンパイラはそれを要求します。
dataフィールドはレンダリングされたドキュメントを文字列として保持します。これが、cheerio.loadに直接送られる理由です。レンダリングオプションについてはScrapelessのドキュメントでカバーされており、同じjs_renderの動作はJSレンダリングガイドでさらに詳しく探求されています。
トラブルシューティング
.tsファイルでERR_UNKNOWN_FILE_EXTENSION。 --experimental-strip-typesフラグが欠けているか、Nodeが22未満です。型のストリッピングは、ロード時にアノテーションを削除します。型チェックは行わないため、コンパイラの意見が欲しいときは、tsc --noEmitを別に実行してください。
モジュールの外でimport文を使用できません。 パッケージに"type": "module"が欠けています。トップレベルのawaitにはESモジュールが必要です。
型ストリッピングがenumやパラメータプロパティを拒否します。 それらの構文は、非消去可能な実際のランタイムコードを出力するため、ストリッピングは処理できません。enumの代わりに文字列リテラルのユニオンを使用し、コンストラクタフィールドを明示的に割り当ててください。
ブラウザではセレクタが一致するが、スクリプトでは一致しません。 インスペクタではなく、view-sourceに対して比較してください。インスペクタはスクリプトが実行された後のDOMを示しますが、fetchが受け取ったものではありません — 上記の例のようにまずレスポンスの長さを印刷してください。
ライブターゲットにこれをポイントする前に、サイトの利用規約および/robots.txt指令を確認してください。これらはロボット排除プロトコル標準に従っており、サイトが快適にサービスを提供できるボリュームで公開データの収集を維持してください。
結論
TypeScriptはスクレイパーに契約を提供します: レコードを宣言し、コンパイラが抽出が契約を満たさなくなったときに教えてくれます。Node 22がネイティブに型をストリッピングし、fetchが組み込まれているため、その契約は一つの依存関係とビルドステップなしで成立します。
取得したページにデータが含まれているかどうかを型は教えてくれません。そのチェックは明示的である必要があります—型を明示した空の配列は、クライアントレンダリングされたページが返すもので、結果が全くないページと全く同じに見えます。その違いを測定する習慣は保つ価値があります:サーバーレンダリングされた10件、JavaScriptの双子では0件、cheerioがそれを見る前に何かがページをレンダリングするともう一度10件。
Scrapelessの無料プランを始めることで、自分のターゲットに対してレンダーステップを実行し、仕事の規模を決めるときは現在のScrapelessの価格を確認してください。
FAQ
Q: TypeScriptを使用してスクレイピングするにはコンパイルが必要ですか?
いいえ。Node 22以降は、.tsファイルをnode --experimental-strip-typesで直接実行でき、ロード時に型注釈を削除します。つまり、スクレイパーに対してビルドステップもバンドラーも不要です。型のストリッピングは型をチェックしないため、コンパイラーに実際に確認させたい場合はCIでtsc --noEmitを実行してください。
Q: TypeScriptで使用するHTMLパーシングライブラリはどれですか?
cheerioがほとんどの作業をカバーします—HTML仕様に準拠したパーサーで解析し、TypeScriptの定義を含むjQueryスタイルのセレクターAPIを公開します。コンテンツがスクリプトによってDOMに書き込まれる場合のみ、ヘッドレスブラウザやレンダリングAPIに手を伸ばしてください。どのパーサーもそれを独自に回復することはできません。
Q: Nodeで404の場合、fetchはエラーを投げますか?
いいえ、これが人々を困惑させます。fetchは、あらゆるHTTPレスポンスに対して通常に解決し、ネットワークレベルの失敗のみで拒否されるため、res.okを自分で確認する必要があります。そのチェックがなければ、エラーページはゼロの一致を返し、正当に結果がなかったページと区別がつきません。
Q: レスポンスがJSONのとき、型を正直に保つにはどうすればよいですか?
境界で注釈を付けます。await res.json()はanyを返すため、const envelope: { data: string }のような型付き変数に割り当てることで、anyがファイルの残りに広がるのを防ぎます。信頼できない上流データには、注釈のみに依存するのではなく、スキーマライブラリを使用してランタイムで検証します。
Q: TypeScriptはブラウザでレンダリングされるページをスクレイプできますか?
それだけではできません。この言語はJavaScriptの実行に影響を与えません—fetchはサーバーが送信したバイトを返し、上の例ではそれらのバイトがクライアントレンダリングされたページでゼロのレコードにパースされることを示しています。レンダリングは他の場所で行う必要があり、操作するヘッドレスブラウザやレンダリングされたDOMを返すAPIを通じて行われなければなりません。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



