PuppeteerによるヘッドレスChromeの使い方 evaluate

Puppeteerとは

Chromeを操って各種チェックなどを行えるようにするもの。

例えば下記のようなスクリプトを作成してnode test.jsを実行すればChromeが指定した文字列を検索して、さらにスクリーンショットを保存してくれる。

// test.js
const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.type('#lst-ib', 'new date');
  await page.waitFor(1000);
  await page.click('.lsb');
  await page.waitForNavigation();
  await page.screenshot({path: 'screenshot.png'});
  browser.close();
});

Chromeが指定した文字列を検索して、さらにスクリーンショットを保存

Puppeteerの意味と読み方

Puppeteerは「人形使い」という意味。人形使いのようにChromeを操るためこのような名称になっている。

読み方はパペティア

Puppeteerをブラウザ上で使用する方法

Try PuppeteerというPuppeteerをブラウザ上で使用できるサイトがある。

簡単な確認ならこれでできるができることが限られるので仕事には使えない。

https://try-puppeteer.appspot.com/

Puppeteerの使い方

Puppeteerを使用するにはNode.jsが必要なので入れていなければ公式サイトからインストールしておく。

https://nodejs.org/ja/

インストールが完了したら下記のコマンドでPuppeteerをインストール。

$ mkdir puppeteer && cd puppeteer && npm i puppeteer

あとはtest.jsなどのJSファイルを作成してnode test.jsを実行すればPuppeteerが動作する。

ヘッドレスChromeを見える化

ヘッドレスChromeは画面上には表示されないが、launchにheadless: false, slowMo: 100のオプションを付けるとブラウザが起動して処理手順を確認できる。

headless: falseのときはpage.waitForNavigationは使用しないほうが良い。

const puppeteer = require('puppeteer');

puppeteer.launch({
    headless: false,
    slowMo: 100 // 遅延時間
}).then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.type('#lst-ib', 'new date');
  await page.waitFor(1000);
  await page.click('.lsb');
  await page.screenshot({path: 'screenshot.png'});
  browser.close();
});

iPhoneでの動作を確認する方法

Puppeteerはデバイスを指定してエミュレートできるため、例えばiPhone 5を指定してスクリーンショットを撮ることが可能。

デバイスをスマートフォンにすると表示されるサイトもスマートフォン用サイトになり、ページ内の要素が変わることがあるため注意が必要。

例えばGoogleの場合、入力フォームの部分はPCだと#lst-ibだがSPだと.gLFyfとなっている。

ちなみにフィーチャーフォン(ガラケー)サイトのチェックはPuppeteerではできない。

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const device = devices['iPhone 5'];
console.log(device);

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(device);
  await page.goto('https://www.google.co.jp/');
  await page.type('.gLFyf', 'new date');
  await page.waitFor(1000);
  await page.click('.Tg7LZd');
  await page.waitForNavigation();
  await page.screenshot({path: 'screenshot.png'});
  browser.close();
});

フルスクリーンや特定の要素もスクリーンショット

フルスクリーンや特定の要素もスクリーンショットで撮ることができる。

lazyloadを使用した画像もfullPage: trueを付けるだけで撮影可能。

await page.screenshot({path: 'screenshot.png', fullPage: true});

特定の要素だけのスクリーンショットを撮りたい場合はDOM要素のスクリーンショットを撮るための関数を作成して指定する。

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.type('#lst-ib', 'new date');
  await page.waitFor(1000);
  await page.click('.lsb');
  await page.waitForNavigation();
  async function screenshotDOMElement(selector, padding = 0) {
    const rect = await page.evaluate(selector => {
      const element = document.querySelector(selector);
      const {x, y, width, height} = element.getBoundingClientRect();
      return {left: x, top: y, width, height, id: element.id};
    }, selector);

    return await page.screenshot({
      path: 'screenshot.png',
      clip: {
        x: rect.left - padding,
        y: rect.top - padding,
        width: rect.width + padding * 2,
        height: rect.height + padding * 2
      }
    });
  }
  await screenshotDOMElement('#searchform');
  browser.close();
});

下記のように直接要素を指定してスクリーンショットは撮れない。

await page.$('#foo').screenshot({path: 'screenshot.png'});

さらに、pathがなければスクリーンショットは保存されない。

await page.$('#foo').screenshot();

スクリーンショット画像を1つに統合する

Puppeteerで保存した複数のスクリーンショットを1つに統合したいことがある。

Puppeteer自体には画像を統合する機能は存在しない。

そのため、画像を統合する際はImageMagickをインストールして下記のように記述して統合させる。

const puppeteer = require("puppeteer");
const im = require('imagemagick');

puppeteer.launch({
  headless: false,
  slowMo: 100
}).then(async browser => {
  const page = await browser.newPage();
  await page.goto("https://google.co.jp/");
  async function screenshotDOMElement(selector, name, padding = 0) {
    const rect = await page.evaluate(selector => {
      const element = document.querySelector(selector);
      const {x, y, width, height} = element.getBoundingClientRect();
      return {left: x, top: y, width, height, id: element.id};
    }, selector);
    return await page.screenshot({
      path: name + '.png',
      clip: {
        x: rect.left - padding,
        y: rect.top - padding,
        width: rect.width + padding * 2,
        height: rect.height + padding * 2
      }
    });
  }
  const arr = ['foo', 'bar', 'baz'];
  const convertArr = ['-append']; // 横結合は+append
  for (const n of arr) {
    await page.evaluate(n => {
      return document.getElementById('lst-ib').value = n;
    }, n);
    await screenshotDOMElement('.tsf-p', n);
    await convertArr.push(n + '.png');
  }
  await convertArr.push('result.png');
  await im.convert(convertArr);
  await browser.close();
});

ImageMagickを使用して統合した画像

clickのあとはwaitForNavigation

page.clickを使用したときにページが遷移する場合はpage.waitForNavigationを記述しておくと遷移完了まで待ってくれる。

スクリーンショットを撮りたいときは必須。この場合、指定時間後に表示されているか不明瞭なpage.waitForは使用しないほうが良い。

await page.click('button');
await page.waitForNavigation();

goto後にwaitForNavigationは意味がない

goto後にwaitForNavigationを指定しても意味がない。

この場合はgotoにwaitUntilのoptionを追加する。

// 間違った書き方 
await page.goto('https://example.com/');
await page.waitForNavigation({ waitUntil: 'domcontentloaded' });

// 正しい書き方 
await page.goto('https://example.com/', { waitUntil: 'domcontentloaded' });

waitUntil:'domcontentloaded'は使用頻度が高くPuppeteerの処理時間を向上できるため、定数に入れておくと良い。

また、waitUntil:'networkidle2'を使用すれば500ミリ秒間に2つ以上のネットワーク接続が存在しなければ処理を終了できるため、こちらのオプションも時間短縮になる。

gotoを使用する際は通常はHTMLだけ取得できている状態で終了しても良いのであればwaitUntil:'domcontentloaded'、画像やJSの読み込み後に終了するのであればwaitUntil:'networkidle2'を指定すると良いだろう。

const WUD = { waitUntil: 'domcontentloaded' };
const NK2 = { waitUntil: 'networkidle2' };
await page.goto('https://example.com/', WUD);
await page.goto('https://iwb.jp/', NK2);

clickのあとはwaitForSelectorも有効

clickのあとの要素がわかっているならwaitForSelectorを使用するとより確実に目的の要素を操作できる。

await page.click('button');
await page.waitForSelector('#foo .bar');

page.type使用時の注意点

page.typeは指定した要素に文字を入力するが、すでにvalueで文字が指定されている場合は語尾に追記してしまう。

例えば

に4を指定すると

になってしまう。

これを防ぐには下記のようにしてvalueを上書きする。

await page.$eval('#lst-ib', e => e.value = 'foo');

このようにinputのvalueを上書きする関数を作成しておくとコードの可読性が良くなる。

async function setInputVal(el, val) {
  page.evaluate((data) => {
    return document.querySelector(data.el).value = data.val;
  }, {el, val});
}
await setInputVal('#lst-ib', 'val');

Cookieを設定する

page.setCookieでCookieを設定可能。(削除はdeleteCookie)

page.cookies()でCookieを取得可能

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.setCookie({"name": "foo", "value": "bar"});
  const cookies = await page.cookies();
  await console.log('cookies.json', JSON.stringify(cookies));
  // await page.deleteCookie({"name": "foo"});
  browser.close();
});

page.localStorage()はないため、localStorageに保存したい場合は、page.evaluateを使用する。

実行結果をfsでテキストファイルで保存

Puppeteer自体にはテキストファイルで保存する機能はないため、例えばCookieを取得してJSON形式で保存したい場合は以下のようになる。

const puppeteer = require('puppeteer');
const fs = require('fs');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  const cookies = await page.cookies();
  fs.writeFileSync('cookies.json', JSON.stringify(cookies));
  browser.close();
});

page.evaluateとは

Puppeteerのコードを見ているとpage.evaluateというのをよく見かける。

page.evaluateはブラウザ内での実行結果を返す。

例えばlocalStrageを使用する場合はpage.evaluate内に処理を記述して返す。

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  const data = await page.evaluate(() => {
    let data;
    localStorage.setItem('foo', 100);
    data = localStorage.getItem('foo');
    return data;
  });
  console.log(data); // => 100
  browser.close();
});

headless: falseのときはpromptやalertも使用できるのでブラウザ上で一時停止して入力する文字列を指定したり、OKを押して次に進めるような使い方ができる。

puppeteer.launch({
    headless: false,
    slowMo: 100
}).then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  const t = await page.evaluate(() => prompt('検索する文字列を入力'));
  await page.type('#lst-ib', t);
  await page.click('.lsb');
  await page.waitFor(1000);
  await page.evaluate(() => alert('検索結果が表示されました'));
  browser.close();
});

Puppeteerはnodeで実行しているため、引数(process.argv[2])から検索用語を取得することもできる。

$ node sample.js foo
const puppeteer = require('puppeteer');
const searchWord = process.argv[2];
console.log('searchWord: ' + searchWord);

puppeteer.launch({
    headless: false,
    slowMo: 100
}).then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.type('#lst-ib', searchWord);
  await page.click('.lsb');
  await page.waitFor(5000);
  browser.close();
});

page.goBackで前のページに戻る

前のページに戻るにはpage.gotoではなくpage.goBackを使用したほうが移動が早い。

ただし、SafariはChromeと違ってpage.goBack時にJavaScriptが実行されないので注意が必要。

SafariはChromeと違い戻るボタン後のページでJavaScriptが実行不可

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://www.google.co.jp/');
  await page.type('#lst-ib', 'new date');
  await page.waitFor(1000);
  await page.click('.lsb');
  await page.waitForNavigation();
  await page.screenshot({path: 'screenshot.png'});
  await page.goBack();
  await page.screenshot({path: 'screenshot2.png'});
  browser.close();
});

特定の要素のテキストを取得

特定の要素のテキストを取得するにはpage.$evalで要素を指定してinnerTextで取得する。(HTMLの場合はinnerHTML)

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://iwb.jp/');
  const recentPost = await page.$eval('#recent-posts-2', e => e.innerText);
  await console.log(recentPost);
  browser.close();
});

特定のリンクの一覧を取得

Webサイト内の特定のリンクの一覧を取得する場合は下記の通りpage.$$evalを使用する。ちなみにpuppeteer.launch().thenで記述しない場合はこのようになる。

const puppeteer = require('puppeteer');
const url = 'https://iwb.jp/';
const target = '#recent-posts-2 > ul > li > a';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const links = await page.$$eval(target, links => {
    return links.map((link) => link.href);
  });
  console.log('最近の投稿リンク一覧');
  console.log(links.join('\n'));
  await browser.close();
})();

特定のリンクの一覧を取得

Webサイト内の特定のリンクの一覧を取得する場合は下記の通りpage.$$evalを使用する。ちなみにpuppeteer.launch().thenで記述しない場合はこのようになる。

const puppeteer = require('puppeteer');
const url = 'https://iwb.jp/';
const target = '#recent-posts-2 > ul > li > a';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(url);
  const links = await page.$$eval(target, links => {
    return links.map((link) => link.href);
  });
  console.log('最近の投稿リンク一覧');
  console.log(links.join('\n'));
  await browser.close();
})();

正しくはpage.evaluateの後ろに変数を記述して読み取れるようにしなければならない。

// 正しい記述例
const arr = [1, 2, 3];
for (const n of arr) {
  await page.evaluate(n => {
    return document.querySelector('input[name="foo"]').value = n;
  }, n);
  await page.waitFor(3000);
}

要素の有無の確認

$evalや$$eval使用時に要素が存在しないとエラーになってしまうため、何かしらの理由でページ内に要素が存在しないことがある場合は以下の条件分岐で要素の有無を確認する必要がある。

if (await page.$('.foo')) {
  await page.$$eval('.foo', c => c.value = 'bar');
}