Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
49
Help us understand the problem. What is going on with this article?
@vicugna-pacos

【Node.js】puppeteer基本情報&逆引き

puppeteerとは

Chrome または Chromium を操れるライブラリ。スクレイピングや自動テストに使える。Node.jsから使用できる。

公式サイト
https://pptr.dev/

使用バージョン:1.19.0

インストール

任意のプロジェクトフォルダで下記コマンドを実行。

npm i puppeteer

既定では、puppeteerをインストールすると、最新バージョンのChromiumも一緒にダウンロードされる。ダウンロードさせたくない場合は、環境変数で設定を変更するか、puppeteer-coreの使用を検討すると良い。

puppeteer-coreを使う

v1.7.0から追加されたパッケージ。Chromiumのダウンロードを行わない。代わりに既にPCにあるChrome/Chromiumなどを使用する。
大体の場合はpuppeteerの方を使えば良いが、スクリプトを動かす環境内にChomeがあり、そちらを使えば良いやと思う場合などは、puppeteer-coreを使う方が容量を取らなくて良さそう。

ブラウザの起動と終了

// headless : ブラウザを画面に表示するかどうかのフラグ。既定値はtrueで非表示となる。
// executablePath : 同梱されているChromiumを使用しない場合のみ、インストール済みのChromeやChromiumのパスを指定する。puppeteer-core使用時は必須指定。
const LAUNCH_OPTION = {
     headless : false
    ,executablePath : 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
};

const browser = await puppeteer.launch(LAUNCH_OPTION);
try {
    const page = await browser.newPage();   // 新しいタブを開く

    // ここにスクレイピングを実装

} finally {
    await browser.close();
}

指定したURLへ移動

ブラウザを起動したら、まずはスクレイピングしたいWebサイトへ移動することが多いと思う。

// 定義
await page.goto(url[, options]);

// サンプル
await page.goto("https://www.yahoo.co.jp/", {"waitUntil":"domcontentloaded"});

パラメータ:

  • url <string> 移動先のURL。
  • options <Object> オプション。
    • timeout <number> 待機時間の最大値(ミリ秒)。既定値は30秒。0を指定するとタイムアウト無しになる。既定値は page.setDefaultNavigationTimeout(timeout) または page.setDefaultTimeout(timeout) で変更できる。
    • waitUntil どの時点をもって移動完了とみなすかの設定値。既定値は load。配列にして複数の文字列を渡した場合は、すべてのイベントが完了してから移動完了とみなす。
      • load - ページの読込みが、画像などを含めて完了。
      • domcontentloaded - HTMLの読込みが終わった。
      • networkidle0 - ネットワーク接続が0件の状態が500ミリ秒続いたとき。
      • networkidle2 - ネットワーク接続が2件以下の状態が500ミリ秒続いたとき。
    • referer <string> リファラのヘッダー値。

要素を取得する

Webページを表示できたら、操作したいテキストボックスやボタンを指定するが、その各要素はCSSセレクタまたはXPathで指定できる。

// CSSセレクタによる要素の取得(最初の1件のみ)
const element1 = await page.$("#main-container");

// CSSセレクタによる要素の取得(一致するものすべて)
const elements1 = await page.$$(".section");

// XPathによる要素の取得(一致するものすべて)
const elements2 = await page.$x("/html/body/h1");

page.$()を使用しなくとも、クリックや文字入力は専用メソッドがあるが、CSSセレクタによる要素指定が基本となっている。XPathで要素を指定したい場合は、page.$x()を使う必要がある。

// CSSセレクタを使ってクリック
await page.click("#confirm-button");

// XPathを使ってクリック
const elements = await page.$x("/html/body/input[@type='button']");
if (elements.length > 0) {
    await elements[0].click();
}

要素が存在するか調べる

要素が見つからない場合、page.$()の戻り値がnullになるので、それで存在チェックができる。

指定したテキストを含む要素を取得する

// ページ全体から「あああ」というテキストを持つdivタグを探す
const elements1 = await page.$x('//div[contains(text(), "あああ")]');

// 指定のタグの中から「いいい」というテキストを持つaタグを探す
// ※XPathの先頭に「.」が必要。無いとページ全体を探索する。
const parent = await page.$("div#container");
const elements2 = await parent.$x('.//a[contains(text(), "いいい")]');

クリックする

// 定義
await page.click(selector[, options]);
// サンプル
await page.click("#submit-button");

パラメータ:

  • selector <string> 対象となる要素のセレクタ
  • options <Object>
    • button <"left"|"right"|"middle"> 既定値:left
    • clickCount <number> 既定値:1
    • delay <number> mousedownmouseupの間隔をミリ秒で指定する。既定値:0

クリックでナビゲーションが発生する場合、page.waitForNavigation()を使ってナビゲーションが終わるまで待機する必要がある。

クリック後のページ移動を待つ

クリックなどでナビゲーションが発生する場合、クリックした後ナビゲーションが終わるのを待つ必要がある。

クリック後のナビゲーションを待機するサンプル
const [response] = await Promise.all([
  page.waitForNavigation(waitOptions),
  page.click(selector, clickOptions),
]);

ページ移動を伴うナビゲーションであればwaitUntildomcontentloadedなどを指定すればよいが、非同期のナビゲーションであれば、networkidle0などを指定する。

// 定義
await page.waitForNavigation([option]);
  • options <Object>
    • timeout <number> 最大待機時間をミリ秒で指定する。0を指定するとタイムアウトなしになる。既定値:30秒。既定値はpage.setDefaultNavigationTimeout(timeout)またはpage.setDefaultTimeout(timeout)で変更可能。
    • waitUntil <string|Array<string>> どれくらい待つか指定する。既定値:load。複数指定した場合は、指定したすべてのイベントが終わるまで待つ。指定できる値はpage.goto()と同じ。
  • 戻り値: <Promise<?Response>>

Ctrlキーを押しながらリンクをクリックする

単純にリンクを新しいタブで開きたいなら、browser.newPage()してから指定のURLへ移動するのが最もシンプルだと思うが、Ctrlキーを押しながらclick()を実行することでも、リンクを新しいタブで開くことができる。ただしこの場合、新しいタブに自動的にフォーカスが移らないので注意。

リンクを新しいタブで開くサンプル
// Ctrlキーを押す(ControlRightでもOK)
await page.keyboard.down("ControlLeft");
// リンクを押す
await page.click("a");
// Ctrlキーを話す
await page.keyboard.up("ControlLeft");

// 開いたタブを最前面にする
const pages = await browser.pages();
const newPage = pages[pages.length-1];
await newPage.bringToFront();

文字を入力する

page.type(selector, text[, options])

  • selector <string> 対象となる要素のセレクタ
  • text <string> 入力する文字列
  • options <Object>
    • delay <number> キー入力間の待機時間をミリ秒で指定する。既定値:0。

テキストを取得する

const ele = await page.$(".scrape");
const text = await page.evaluate(elm => elm.textContent, ele);

Pageオブジェクトにテキスト取得のメソッドはないため、evaluateメソッドを代わりに使用する。
evaluateメソッドは、引数で指定した関数をページ内で実行する。第1引数に実行する関数、第2引数に関数へ渡す引数を指定する。この引数に、page.$()などで取得したElementHandleを渡すことが可能。

指定秒を待つ

page.waitForTimeout(milliseconds)

引数にミリ秒を指定する。

要素が表示されるまで待つ

page.waitForSelector(selector, {visible:true});

Chromeで動的に生成される要素を調べる

一時停止(F8)を押すか、要素で右クリック→「検証」を選ぶ。

javascript - マウスカーソルを乗せている間だけ表示される要素を、開発者ツールで調べるには - スタック・オーバーフロー

ダイアログに対応する

Webサイトが表示するダイアログに対して処理が必要な場合のサンプル。

ダイアログが表示されたらOKを押す
page.on("dialog", async (dialog)=>{
    console.log("dialog message : " + dialog.message());
    await dialog.accept();
});

一度だけ処理させたいなら、page.once()を使う方が良いかもしれない。使用方法はpage.on()と同じ。

Dialogクラスの説明

ログインが必要なページへのアクセス

以下の方法がある:

  • IDとパスワードを入力してログインする動作をスクリプトに実装する。
    • 良いところ:単純で分かりやすい。
    • 悪いところ:Googleには弾かれる。
  • ChromeからCookieをエクスポートし、それをpuppeteerに読み込ませて使用する。
    • 良いところ:Googleにもログインできる。
    • 悪いところ:Cookieをエクスポートするのが手間。Cookieの有効期限も考慮しなければならない。

アカウント+パスワードでGoogleにログインするスクリプトを作成し実行したところ、ページが以下のようになってログインできなかった。

puppeteerの操作を自動テストツールと判定して、ログインを阻止していると思われる。こういう場合、Cookieを使う方法だとログインせずともページがログイン後の状態となって上手く動作する場合がある。

Cookieを使用してログインする

Webサイトへログインした後のCookieを保存しておき、それをpuppeteerに読み込ませることで、自動化中もログイン後と同じ状態にする。

EditThisCookieのインストール

Chromeから簡単にCookieをエクスポートするためのツールをインストールする。EditThisCookieはChrome拡張機能として提供されている。

Cookieのエクスポート

Chromeで目的のWebサイトへログインした後、EditThisCookieのアイコンをクリックして、エクスポートのアイコンをクリックする。

Cookieの内容がJSON形式でクリップボードへエクスポートされるので、テキストファイルへ貼り付けて保存する。

puppeteerでCookieを読み込む

保存したCookieのファイルを、puppeteerで読み込む。目的のサイトへ移動する前にCookieを読み込んでおくこと。また、サンプルには実装していないが、Cookieの有効期限が切れた場合も考慮しておく必要がある。

const puppeteer = require("puppeteer-core");
const config = require("config");
const fs = require("fs");

async function useCookieSample(page) {

    let content = fs.readFileSync("cookie.json");
    let cookie = JSON.parse(content);

    await page.setCookie(...cookie);

    await page.goto("https://gmailのURL", {
        "waitUntil" : "domcontentloaded"
    });
}

参考:javascript - login into gmail fails for unknown reason - Stack Overflow

自動ツールであることを隠す

Webサイトによっては、puppeteerでアクセスするとツールであることを判定して、通常とは異なるページを返す。Webサイト側がチェックをかけているのだが、その方法の一つとして、JavaScriptを使って簡単に判定できる。
navigator.webdriverというプロパティで、puppeteerでChromeを動かしているときもこの値がtrueになっている。
参考:Navigator.webdriver - Web API | MDN

puppeteer側でこのプロパティをなくすことが可能。

navigator.driverを削除するサンプル
const page = await browser.newPage();
await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'webdriver', ()=>{});
    delete navigator.__proto__.webdriver;
});
await page.goto("https://www.yahoo.co.jp/");

参考:navigator.webdriver=trueだとロボットだとバレる。その回避法はあるか?puppeteerなら出来そう | 三昧生活 CFAutoG 自動化

ただ、自分で試行錯誤するより、有志のライブラリの力を借りる方が便利だと思う。

berstend/puppeteer-extra

49
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
vicugna-pacos
SIerにいながら業務効率化などに関わっています。最近は自サイトに技術メモを書くようにしています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
49
Help us understand the problem. What is going on with this article?