こんにちは、にょろりんこの備忘録的技術ブログです。
今回は、自作クローラシリーズの中核ともいえる「URL収集ロジック」について紹介してみようと思います。
静的なHTMLページはもちろん、JavaScriptでリンクが後から描画される動的ページ(SPAなど)にも対応できるように設計しています。 具体的には、Puppeteer と Node.js を使って、指定したページを開いて自動でスクロールし、`<a>` タグをすべて抽出する処理です。
補足:SPAとはなにか?
SPA(Single Page Application)とは、「ページ遷移が発生しない」タイプのWebアプリケーションのことです。普通のWebサイトは、リンクをクリックするたびにページが切り替わり、毎回HTMLがサーバーから読み込まれますよね?でもSPAでは、最初に1つのHTMLだけを読み込み、あとはJavaScriptが必要なコンテンツを動的に描画していきます。つまり、見た目は切り替わっていても、ブラウザ上では「ページ遷移」していないのです。
この処理は、私が運営している匿名検索エンジン「SaePorns」で実際に稼働しているクローラの中に組み込まれており、次にクロールすべきURLの発見や、リンクの再帰的収集に使われています。
「リンクの抽出」と聞くと、`document.querySelectorAll('a')` で一発じゃない?と思うかもしれません。 確かに、静的なページならそれで問題ありません。
ですが最近のウェブサイト、特に動画共有系のサイトでは、JavaScriptで後からリンクが描画されることが多く、 ページを開いただけでは `<a>` タグがまだ存在しないことも珍しくありません。 さらに、スクロールするごとにリンクが読み込まれる「無限スクロール」構造も増えています。
つまり、普通に `page.goto()` してすぐ `<a>` を抽出しようとしても、何も取れない/一部しか取れないという事態になるわけです。

そこで登場するのが、今回紹介するモジュールです。 これは「スクロール」と「描画の完了待ち」を組み合わせた実戦向けリンク抽出器です。
このモジュールの責務は以下のとおりです:
- 対象URLに `goto()` でアクセス
- 一定間隔で `window.scrollBy()` を呼び出しながら自動スクロール
- `<a href>` が描画されるのを最大10秒待つ
- 描画されたリンクをすべて `.map(a => a.href)` で抽出して返す
つまり、動的描画でもリンク(URL)を取りこぼさないことを目的とした、堅牢なリンク(URL)抽出処理になっています。
これを使えば、たとえJSで描画されるSPA系のページでも、同一ドメイン内のリンクを片っ端から収集できます。実際に SaePorns のループ内では、この関数を呼び出して取得したURLを正規化し、DBに保存していく構成になっています。
それでは、実際のコードを見ていきます。
この関数では「ページの一番下まで自動的にスクロール」する処理を行っています。
async function autoScroll(page) {
await page.evaluate(async () => {
await new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight - window.innerHeight) {
clearInterval(timer);
resolve();
}
}, 100);
});
});
}
※半角(が2個続くと引用になってしまうはてなブログの仕様?のため(を全角にしてあります。コピペする際は半角に戻してお使いください。
最近のウェブサイトでは、ページの読み込みと同時にすべてのリンクが描画されるとは限りません。 特に、スクロールするたびに新しい要素が読み込まれる「無限スクロール型」のページでは、 ページ下部まで移動しないと `<a>` タグが出現しないこともあります。
そのため、この `autoScroll()` 関数は、以下のような流れでページを強制的にスクロールし、すべてのコンテンツが描画されるのを促す役割を担っています。
- 100ピクセルずつ下方向にスクロールしながら、
- `document.body.scrollHeight` に達するまで繰り返す
- 最下部まで到達したら `resolve()` して完了
このスクロール処理によって、JSによって後から描画されるリンク要素も確実に画面上に表示され、このあとに行う `<a>` タグの抽出で「取りこぼしがなくなる」わけです。
次に、`fetchElements()` 関数です。
この関数が本モジュールのメイン処理部になります。 やっていることはシンプルですが、実戦的で強力です。
async function fetchElements(page, url) {
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });// 無限スクロール対応
await autoScroll(page);// aタグが描画されるまで待機(最大10秒)
await page.waitForSelector('a', { timeout: 10000 });const links = await page.evaluate(() => {
const anchors = Array.from(document.querySelectorAll('a[href]'));
return anchors.map(a => a.href.trim()).filter(Boolean);
});return links;
} catch (err) {
console.error(`fetchElements エラー: ${err.message}`);
return ;
}
}
この関数では、まず page.goto() を使って対象URLにアクセスします。waitUntil: 'networkidle2' を指定しているのは、JavaScriptで後から描画される要素の読み込みが終わるのを待つためです。ページ内のリクエストが落ち着く(=ネットワークがアイドル状態になる)まで待ってから処理を進めることで、安定したスクレイピングが可能になります。
次に、先ほど紹介した autoScroll() を使って、ページの最下部まで自動でスクロールします。これにより、スクロール中に動的に読み込まれるリンクも含めて描画させることができます。
続いて、<a> タグが出現するのを最大10秒間待機し、描画されたリンクを document.querySelectorAll('a[href]') で全て取得。map(a => a.href.trim()).filter(Boolean) で余計な空白や空要素を除去したうえで、リンクの配列として返しています。
もし何らかのエラーが発生した場合は、ログを出して空配列 を返す仕様になっているため、呼び出し元での処理も止まることなく、安定して運用することができます。
この fetchElements() 関数によって、静的ページでもSPAでも、サイト構造を問わずリンクを網羅的に抽出できるわけです。
今回紹介した fetchElements() 関数は、私の自作クローラの中でももっとも実戦的なリンク収集装置です。単に <a> タグを拾うだけでなく、動的に描画されるリンクにも対応し、無限スクロールやSPA構造のページでも取りこぼしなく収集できる設計になっています。
裏側では以下の3つの要素が支えています:
- page.goto() における networkidle2 待機(JS描画完了を想定)
- autoScroll() によるスクロール実行(無限ロード対応)
- <a> タグの明示的な描画待ちと抽出処理
これらを組み合わせることで、普通のスクレイパーでは抜けがちなリンクを拾い上げられるようになります。
この処理はおそらく、GoogleやBingのような大手検索エンジンのクローラでも行われていると考えています。JSで描画されるコンテンツが増えている今、こうした「描画を待ってからリンクを取得する」戦略は、もはや検索エンジンとしての基礎体力のようなものです。
この関数は、SaePornsのクローラapp145-main.js内で繰り返し使われ、日々サイト内を巡回して次の解析対象URLを見つけ出す「目」として働いています。
それではよき開発ライフを。
※このコードが実際に使われている「追跡されないアダルト動画の検索エンジンSae-Porns」はこちら!よかったら見ていってください。(18歳未満の方はご利用できません。)