前言#
打工人になってから、掘金をブラウジングする時間があまりなく、週末にベッドでリラックスしながらホットリストをチェックすることが多いですが、その際に掘金のホットリストの履歴を見逃してしまうことがよくあります(ホットリストの内容は常に変化しています)。このような状況では、素晴らしい掘金の記事を見逃してしまうかもしれません。では、一行のコマンドで掘金の歴史的ホットリストを記録する方法はあるのでしょうか?ここでPuppeteerを紹介する必要があります。
簡単な紹介#
Puppeteerは、Google によって開発およびメンテナンスされている Node.js ライブラリで、高度な API を提供し、ヘッドレス Chrome(GUI のない Chrome ブラウザ)インスタンスを制御してウェブページの自動化操作を行います。ウェブページのスクリーンショット、PDF の生成、データのスクレイピング、自動フォーム入力およびインタラクションなど、さまざまなタスクを実行するために使用できます。
ここでヘッドレス 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 のクローリングを行い、プリレンダリングの効果を得ることができます(この点については、機会があればフロントエンドのファーストスクリーン最適化に関する記事を出したいと思います😽)。