前言#
打工人になってから、掘金をブラウジングする時間があまりなく、週末にベッドでリラックスしながらホットリストをチェックすることしかできません。しかし、この時に掘金のホットリストの履歴を見逃してしまうことがよくあります(ホットリストの内容は常に変化しています)。このような状況では、素晴らしい掘金の記事を見逃してしまうかもしれません。では、一行のコマンドで掘金の歴史的ホットリストを記録する方法はあるのでしょうか?ここでPuppeteerを紹介する必要があります。
簡単な紹介#
Puppeteerは、Google によって開発・維持されている Node.js ライブラリで、高度な API を提供し、Headless Chrome(ヘッドレス Chrome ブラウザ)インスタンスを制御してウェブページの自動化操作を行うことができます。ウェブページのスクリーンショット、PDF の生成、データのスクレイピング、自動化されたフォームの入力やインタラクションなど、さまざまなタスクを実行するために使用できます。
ここで Headless Chrome、つまりヘッドレスブラウザについて強調します(後のコードでこの概念が関わります)。ヘッドレスブラウザは、可視のユーザーインターフェースを持たないウェブブラウザです。バックグラウンドで実行され、ウェブ操作やブラウジング行動を実行しますが、グラフィカルインターフェースは表示されません。従来のウェブブラウザは通常、ユーザーが見ることのできるインターフェースを提供し、ユーザーはこれらのインターフェースを通じてインタラクションを行い、フロントエンドとバックエンドのロジックインタラクションを実現します。たとえば、URL を入力してリンクをジャンプしたり、ログインや登録時に送信ボタンをクリックしたりします。しかし、ヘッドレスブラウザはこれらの操作をバックグラウンドで自動化して実行でき、ポップアップが何度も表示されることはありません。これにより、開発者はスクリプトを作成してさまざまな操作を自動化することができます。たとえば:
- ウェブアプリケーションのテスト自動化
- ウェブページのスクリーンショットを撮る
- JavaScript ライブラリの自動テストを実行する
- ウェブサイトデータを収集する
- ウェブインタラクションを自動化する
次に、Puppeteer を迅速に使い始め、一行のコマンドで掘金のホットリストの記事情報を取得する方法を示します。
Puppeteer の設定#
ここでの設定は、実際には公式ドキュメントのクイックスタートに従うだけですので、簡単に説明します。
ここでは、Puppeteer
を直接インストールすることができます。
npm i puppeteer
ここで注意が必要なのは、puppeteer.config.cjs
ファイルを作成して Puppeteer を設定できることです:
const {join} = require('path');
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
// Puppeteerのキャッシュ場所を変更します。
cacheDirectory: join(__dirname, '.cache', 'puppeteer'),
};
この設定ファイルを作成した後にインストールコマンドを実行すると、.cache
フォルダーが追加されます。開いてみると、多くのバイナリファイルが保存されていることがわかります。これは起動速度を向上させる最適化策に関わります。.cache
ファイルは、最初にPuppeteer
を使用する際に、現在のオペレーティングシステムに適した Chrome ブラウザのバイナリファイルを自動的にダウンロードします。これにより、後続の起動時に Puppeteer が再度必要なファイルをダウンロードする必要がなくなり、起動速度が向上します。
Puppeteer の初歩#
この時点で、test.js
ファイルを作成し、以下の内容を入力します。逐行で説明します:
// Puppeteerライブラリをインポートし、その機能を使用できるようにします。ここではESMの構文を使用することもできます。
//import puppeteer from 'puppeteer';
const puppeteer = require('puppeteer');
(async () => {
// Chromeブラウザのインスタンスを起動します。現在、Puppeteerはデフォルトでヘッドレスブラウザモードで起動します。
const browser = await puppeteer.launch();//const browser = await puppeteer.launch({headless: true});
// 新しいページオブジェクトを作成します。
const page = await browser.newPage();
// 指定されたURLにナビゲートし、URLを入力してジャンプする操作をシミュレートします。
await page.goto('https://example.com');
// 現在のページのスクリーンショットを撮ります。注意:Chromeチームは、異なるデバイスで表示される内容を一貫させるために、デフォルトのブラウザウィンドウサイズを800x600に設定しています。
await page.screenshot({path: 'example.png'});
// Chromeを閉じます。
await browser.close();
})();
node .\test.js
コマンドを実行した後にこの画像が得られれば、成功したことを示します:
素晴らしい、これで Puppeteer の基本操作を習得しました。次は、前述の要件を実現します。
機能の実装#
まず、掘金のホットリストで、記事のタイトルとリンクがどこにあるのかを知っておく必要があります。具体的には、彼らの位置を理解するのではなく、Puppeteer
にその位置を知ってもらう必要があります。ホットリスト部分でコンソールを開き、セレクタ構文:Page.$$()
を使用します。このメソッドは、ブラウザ内でdocument.querySelectorAll
メソッドを実行できます。$$('a')
と入力すると、多くの a タグが得られますが、これは明らかに期待とは異なります。
この時、範囲を狭める必要があります。ホットリストにマウスを置き、右クリックして「検査」を選択します。
これで、コンソール上でこの部分の内容を迅速に特定できます。この時、セレクタの内容を変更します:$$('.hot-list>a')
。これでリンクの内容を取得できました。タイトルを取得したい場合も同様の原理で、少し処理を加えます:$$('.article-title').map(x=>x.innerText)
とすれば、掘金ホットリストのタイトルを得ることができます。
落とし穴#
この時点でコードを直接実行すると、非常に高い確率で[]
が得られます。ここで重要な点は、ウェブページの読み込み遅延です。
ここでデフォルトのヘッドレスブラウザモードを解除し、コードを次のように変更します:
const browser = await puppeteer.launch({ headless: false })
この時、再度コードを実行すると、ウェブページが完全に読み込まれないうちに操作が終了してしまうことがわかります。この時、waitUntil
を設定するか、スクリプトの遅延読み込みを行う必要があります。コードを次のように変更します:
await page.goto("https://juejin.cn/hot/articles", {
waitUntil: "domcontentloaded",
});
await page.waitForTimeout(2000);
しかし、ドキュメントを確認すると、page.waitForTimeout
は非推奨であり、Frame.waitForSelector
を使用することが推奨されています。これは、指定されたセレクタに一致する要素がフレーム内に現れるのを待ってからコードを実行します。直接的にコードを遅延実行するよりも効率的です。ここでは一時的にこのままにし、後で完全なコードを添付します。
機能の補完と最適化#
内容を正常に検出できたら、次に必要なのはそれをローカルに保存することです。この時、Node.js のファイルシステムモジュールを導入し、検出されたファイル内容を書き込みます:
import puppeteer from "puppeteer";
import fs from "fs";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://juejin.cn/hot/articles', {
waitUntil: "domcontentloaded"
});
await page.waitForTimeout(2000);
let hotList = await page.$$eval(".article-title[data-v-cfcb8fcc]", (title) => {
return title.map((x) => x.innerText);
});
console.log(hotList);
// 記事のタイトルをテキストファイルに保存します。
fs.writeFile('titles.txt', hotList.join('\n'), (err) => {
if (err) throw err;
console.log('記事のタイトルがtitles.txtファイルに保存されました');
});
await browser.close();
})();
これで記事のすべてのタイトルを取得できましたが、タイトルだけでは不十分です。週末に記事を読むために手動で入力する必要があると面倒です。この時、記事のタイトルとリンクを一緒に保存する必要があります。closest("a").href
を呼び出してリンクを取得します:
const articleList = await page.$$eval(
".article-title[data-v-cfcb8fcc] a",
(articles) => {
return articles.map((article) => ({
title: article.innerText,
link: article.href,
}));
}
);
console.log(articleList);
// 記事のタイトルとリンクをテキストファイルに保存します。
const formattedData = articleList.map(
(article) => `${article.title} - ${article.link}`
);
fs.writeFile("articles.txt", formattedData.join("\n"), (err) => {
if (err) throw err;
console.log("記事のタイトルとリンクがarticles.txtファイルに保存されました");
});
大成功!しかし、この時、翌日再度このスクリプトを実行すると、前日のファイルが上書きされてしまいます。これは問題です。そこで、日付で分類し、記事のホットリストを異なる日数で分類する必要があります。ここで、先に述べた待機機能と指定されたセレクタに一致する要素がフレーム内に現れる機能を追加します。最終的には次のようになります:
import puppeteer from "puppeteer";
import fs from "fs";
(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto("https://juejin.cn/hot/articles", {
waitUntil: "domcontentloaded",
});
// フォルダ名を処理します。
const currentDate = new Date().toLocaleDateString();
const fileName = `${currentDate.replace(/\//g, "-")}.txt`;
await page.waitForSelector(".article-title[data-v-cfcb8fcc]");
const articleList = await page.$$eval(
".article-title[data-v-cfcb8fcc]",
(articles) => {
return articles.map((article) => ({
title: article.innerText,
link: article.closest("a").href,
}));
}
);
console.log(articleList);
const formattedData = articleList.map(
(article) => `${article.title} - ${article.link}`
);
fs.writeFile(fileName, formattedData.join("\n"), (err) => {
if (err) throw err;
console.log(`記事のタイトルとリンクがファイルに保存されました: ${fileName}`);
});
await browser.close();
})();
コードを実行すると、次のような内容が得られます:
まとめ#
Puppeteer
は、Google チームによって開発・維持されている Node.js ライブラリで、さまざまな自動化操作を非常に便利に行うことができます。想像してみてください。これからは、単純な node コマンドを実行するだけで、現在のホットリストの記事と情報を保存できるのです。なんて素晴らしいことでしょう🐱
ただし、クローラーというこのソリューションは、彼の多くの機能の中で最も取るに足らない部分に過ぎません。公式が言うように、彼は自動化されたフォームの送信、UI テスト、サイトのタイムラインのキャプチャ、SPA のクローリングを行い、プリレンダリングの効果を得ることもできます(この点については、機会があればフロントエンドの初期表示最適化に関する記事を出したいと思います😽)。