Puppeteerの利用方法メモ


書いてあること

  • Puppeteerの利用検証を行った際のメモ

環境

  • Windows 10 Pro 1909
  • Node.js v12.16.1
  • Npm 6.14.3
  • puppeteer 2.1.1

作成したプロジェクト

↓に置いてあります。
puppeteer-project

Puppeteerとは

Puppeteer

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

環境構築

Node.jsをインストール

↓からインストーラを取得してインストール
Node.js

bash
# バージョン確認
$ node --version
v12.16.1
$ npm --version
6.14.3

# npmを最新化
$ npm install -g npm

プロジェクト作成

bash
# プロジェクトのディレクトリを作成・移動
$ mkdir puppeteer-project && cd puppeteer-project

# Git初期化
$ git init

# .gitignoreファイルを下記の通り作成

# プロジェクトを作成(対話形式はスキップ)
$ npm init -y

# package.jsonを下記の通り修正

# src、outディレクトリを作成
$ mkdir src out
.gitignore
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# parcel-bundler cache (https://parceljs.org/)
.cache

# next.js build output
.next

# nuxt.js build output
.nuxt

# Nuxt generate
dist

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless

# IDE / Editor
.idea

# Service worker
sw.*

# macOS
.DS_Store

# Vim swap files
*.swp
package.json
{
  "name": "puppeteer-project",
  "version": "1.0.0",
  "description": "Puppeteer Project",
  "author": "yoshi0518"
}

Puppeteerインストール

bash
$ npm install --save puppeteer

ESLint、Prettier設定

必要なパッケージをインストール

bash
$ npm install --save-dev prettier eslint eslint-plugin-prettier eslint-config-prettier babel-eslint

.eslintrc.jsを作成

.eslintrc.js
module.exports = {
  root: true,
  parser: 'babel-eslint',
  env: {
    browser: true,
    node: true,
    es6: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:prettier/recommended',
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'generator-star-spacing': 'off',
    'prettier/prettier': 'error'
  },
};

.eslintignoreを作成

.eslintignore
/node_modules/
/*.js
/package.json
/package-lock.json

.prettierrc.jsを作成

.prettierrc.js
module.exports = {
  trailingComma: 'es5',
  printWidth: 140,
  tabWidth: 2,
  singleQuote: true,
  semi: true,
};

.prettierignoreを作成

.prettierignore
/node_modules/
/*.js
/package.json
/package-lock.json

package.jsonにスクリプトを追加

package.json
{
  "name": "puppeteer-project",
  "version": "1.0.0",
  "description": "Puppeteer Project",
  "author": "yoshi0518",
+ "scripts": {
+   "lint": "eslint --ext .js --ignore-path .gitignore ./src",
+   "lint:fix": "eslint --ext .js --ignore-path .gitignore ./src --fix"
+ },
  "dependencies": {

動作確認

sample.jsを作成

src/sample.js
const puppeteer = require('puppeteer');
const path = require('path');

const URL = 'https://www.yahoo.co.jp/';
const PATH = './out';
const DEBUG = 0; // デバッグログなし
// const DEBUG = 1; // デバッグログあり

(async () => {
  console.log('[Info] ■■■ Puppeteer動作確認 Start ■■■');

  if (DEBUG) console.log('[Debug] Chromium起動');
  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50,
  });

  if (DEBUG) console.log('[Debug] 新しいタブを開く');
  const page = await browser.newPage();

  if (DEBUG) console.log('[Debug] ビューポート/デバイスを指定');
  await page.setViewport({
    width: 1200,
    height: 800,
  });

  if (DEBUG) console.log('[Debug] Yahooへ移動');
  await page.goto(URL);

  if (DEBUG) console.log('[Debug] スクリーンショットを保存');
  await page.screenshot({
    path: path.join(PATH, 'sample.png'),
    fullPage: true,
  });
  console.log(`[Info] ファイル保存:sample.png`);

  if (DEBUG) console.log('[Debug] Chromium終了');
  await browser.close();

  console.log('[Info] ■■■ Puppeteer動作確認 End ■■■');
})();

実行

下記コマンドを実行し、outディレクトリにsample.pngが作成されることを確認。

bash
$ node ./src/sample.js

動作検証

2020/4/2にPuppeteer動作検証を行った際のソースです。
各サイトに負荷がかかりますので、試す場合は各自の責任で最小限ご利用ください。

事前準備

必要なパッケージをインストール

bash
$ npm install --save dotenv csv-stringify iconv-lite request util child_process

Tesseract OCRをインストール

認証画像からキーワードを取得する際に利用するTesseract OCR(オープンソースOCRエンジン)をインストール

Mac

terminal
$ brew update
$ brew install tesseract

Windows

Tesseract OCRをWindowsにインストールする方法

共通処理を作成

common.js
const fs = require('fs');
const stringify = require('csv-stringify');
const iconv = require('iconv-lite');
const request = require('request');
const { promisify } = require('util');

// 指定したディレクトリ内のファイルを全て削除
exports.deleteFiles = (path) => {
  fs.readdir(path, (error, files) => {
    if (error) throw error;

    for (const file of files) {
      fs.unlink(`${path}/${file}`, (error) => {
        if (error) throw error;

        console.log(`[Info] ファイル削除:${file}`);
      });
    }
  });
};

// パラメータの配列からCSVファイルを作成
exports.createCsv = async (data, filePath, charSet) => {
  stringify(data, (error, csvData) => {
    if (error) throw error;
    const writableStream = fs.createWriteStream(filePath);
    writableStream.write(iconv.encode(csvData, charSet));
  });
};

// 指定した画像をダウンロード
exports.downloadImage = async (src, filePath) => {
  const res = await promisify(request)({
    method: 'GET',
    uri: src,
    encoding: null,
  });
  if (res.statusCode === 200) {
    await promisify(fs.writeFile)(filePath, res.body, 'binary');
  } else {
    throw new Error(res.statusCode + ' Image download error');
  }
};

YAHOO

sample_yahoo.js
const puppeteer = require('puppeteer');
const path = require('path');
const common = require('./common.js');

const URL = 'https://www.yahoo.co.jp/';
const PATH = './out/sample_yahoo';
const DEBUG = 0; // デバッグログなし
// const DEBUG = 1; // デバッグログあり

// 各ニュースページを開き、スクリーンショットを保存
const getChildPage = async (browser, href) => {
  console.log(`[Info] ニュースページのURL:${href}`);

  if (DEBUG) console.log('[Debug] 新しいタブを開き、各ニュースページに移動');
  const childPage = await browser.newPage();
  await childPage.goto(href);

  if (DEBUG) console.log('[Debug] 各ニュースのタイトルを取得');
  const title = await childPage.evaluate(() => document.querySelector('.pickupMain_articleTitle').innerHTML);
  console.log(`[Info] タイトル:${title}`);

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await childPage.waitFor(1500);

  if (DEBUG) console.log('[Debug] 「続きを読む」リンクをクリック');
  await Promise.all([childPage.waitForNavigation({ waitUntil: 'load' }), childPage.click('.pickupMain_detailLink > a')]);

  if (DEBUG) console.log('[Debug] スクリーンショットを保存');
  await childPage.screenshot({
    path: path.join(PATH, `${title}.png`),
    fullPage: true,
  });
  console.log(`[Info] ファイル保存:${title}.png`);

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await childPage.waitFor(1500);

  if (DEBUG) console.log('[Debug] 各ニュースページを閉じる');
  await childPage.close();
};

(async () => {
  console.log('[Info] ■■■ Yahooニュースを取得 Start ■■■');

  if (DEBUG) console.log('[Debug] 前回実行時のファイルを削除');
  await common.deleteFiles(PATH);

  if (DEBUG) console.log('[Debug] Chromium起動');
  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50,
  });

  if (DEBUG) console.log('[Debug] 新しいタブを開く');
  const page = await browser.newPage();

  if (DEBUG) console.log('[Debug] ビューポート/デバイスを指定');
  await page.setViewport({
    width: 1200,
    height: 800,
  });

  if (DEBUG) console.log('[Debug] Yahooへ移動');
  await page.goto(URL, { waitUntil: 'domcontentloaded' });

  if (DEBUG) console.log('[Debug] ニュースタブからhrefを取得');
  const hrefs = await page.evaluate(() =>
    Array.from(document.querySelectorAll('#tabpanelTopics1 > div > div > ul > li > article > a')).map((a) => a.href)
  );

  if (DEBUG) console.log('[Debug] 取得したhrefだけ繰り返し');
  for (const href of hrefs) {
    if (DEBUG) console.log('[Debug] 各ニュースページを開き、スクリーンショットを保存');
    await getChildPage(browser, href);
  }

  if (DEBUG) console.log('[Debug] Chromium終了');
  await browser.close();

  console.log('[Info] ■■■ Yahooニュースを取得 End ■■■');
})();

Google

sample_google.js
const puppeteer = require('puppeteer');
const path = require('path');
const common = require('./common.js');

const URL = 'https://www.google.com/?hl=ja';
const PATH = './out/sample_google';
const DEBUG = 0; // デバッグログなし
// const DEBUG = 1; // デバッグログあり

(async () => {
  console.log('[Info] ■■■ Google検索結果を取得 Start ■■■');

  if (DEBUG) console.log('[Debug] 前回実行時のファイルを削除');
  await common.deleteFiles(PATH);

  if (DEBUG) console.log('[Debug] Chromium起動');
  const browser = await puppeteer.launch({
    // headless: false,
    slowMo: 50,
  });

  if (DEBUG) console.log('[Debug] 新しいタブを開く');
  const page = await browser.newPage();

  if (DEBUG) console.log('[Debug] ビューポート/デバイスを指定');
  await page.setViewport({
    width: 1200,
    height: 800,
  });

  if (DEBUG) console.log('[Debug] Googleへ移動');
  await page.goto(URL, { waitUntil: 'domcontentloaded' });

  if (DEBUG) console.log('[Debug] 検索キーワードを入力');
  await page.type('input[name="q"]', 'puppeteer');

  if (DEBUG) console.log('[Debug] タイトルイメージにフォーカスを移動');
  await page.focus('#hplogo');

  if (DEBUG) console.log('[Debug] 検索ボタンをクリック');
  await Promise.all([page.waitForNavigation({ waitUntil: 'load' }), page.click('#tsf > div > div > div > center > input[name="btnK"]')]);

  if (DEBUG) console.log('[Debug] 検索結果からhrefを取得');
  const results = await page.$$('.g > .rc > .r > a');

  if (DEBUG) console.log('[Debug] 取得したhrefだけ繰り返し');
  let data = [{ text: 'text', href: 'href' }];
  let cnt = 0;
  for (const result of results) {
    const text = await (await result.getProperty('text')).jsonValue();
    const href = await (await result.getProperty('href')).jsonValue();

    if (DEBUG) console.log('[Debug] text、hrefを変数へ格納');
    data.push({
      text: text,
      href: href,
    });

    console.log(`[Info] 検索結果ページのURL:${href}`);
    console.log(`[Info] タイトル:${text}`);

    if (DEBUG) console.log('[Debug] 3件までPDF出力');
    if (cnt < 3) {
      if (DEBUG) console.log('[Debug] 新しいタブを開き、検索結果ページに移動');
      const childPage = await browser.newPage();
      await childPage.goto(href);

      if (DEBUG) console.log('[Debug] PDFを保存');
      await childPage.pdf({
        path: path.join(PATH, `${text}.pdf`),
      });
      console.log(`[Info] ファイル保存:${text}.pdf`);

      if (DEBUG) console.log('[Debug] 1.5秒待機');
      await childPage.waitFor(1500);

      if (DEBUG) console.log('[Debug] 検索結果ページを閉じる');
      await childPage.close();

      cnt++;
    }
  }

  if (DEBUG) console.log('[Debug] Chromium終了');
  await browser.close();

  if (DEBUG) console.log('[Debug] 検索結果をCSV出力');
  await common.createCsv(data, path.join(PATH, 'data.csv'), 'UTF-8');

  console.log('[Info] ■■■ Google検索結果を取得 End ■■■');
})();

価格.com

kakaku.js
const puppeteer = require('puppeteer');
const path = require('path');
const common = require('./common.js');

const URL = 'https://kakaku.com/pc/pda/itemlist.aspx?pdf_se=2'; // 製品一覧(iPad)
const PATH = './out/sample_kakaku';
const DEBUG = 0; // デバッグログなし
// const DEBUG = 1; // デバッグログあり

// 各製品ページを開き、製品情報を取得
const getItemInfo = async (browser, href) => {
  console.log(`[Info] 製品ページのURL:${href}`);

  if (DEBUG) console.log('[Debug] 新しいタブを開き、製品ページに移動');
  const childPage = await browser.newPage();
  await childPage.goto(href, { waitUntil: 'domcontentloaded' });

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await childPage.waitFor(1500);

  if (DEBUG) console.log('[Debug] 製品名を取得');
  const itemTitle = await childPage.evaluate(() => document.querySelector('#titleBox h2').innerText);
  console.log(`[Info] 製品名:${itemTitle}`);

  if (DEBUG) console.log('[Debug] 製品の情報を取得①');
  // 画像
  const itemImgSrc = await childPage.evaluate(() => document.querySelector('#imgBox > a > img').src);
  // 最安価格
  const itemPrice = await childPage.evaluate(() => document.querySelector('#priceBox div.priceWrap span.priceTxt').innerText);
  // 売れ筋ランキング
  const itemRanking = (await childPage.evaluate(() => document.querySelector('#ovBtnBox li.ranking span.num').innerText)) + '';
  // 満足度・レビュー
  const itemReview =
    (await childPage.evaluate(() => document.querySelector('#ovBtnBox li.review span.num').innerText)) +
    '点(' +
    (await childPage.evaluate(() => document.querySelector('#ovBtnBox li.review span.sup').innerText)) +
    ')';
  // クチコミ
  const itemBbs = (await childPage.evaluate(() => document.querySelector('#ovBtnBox li.bbs span.num').innerText)) + '';

  if (DEBUG) console.log('[Debug] 「スペック情報」リンクをクリック');
  await Promise.all([childPage.waitForNavigation({ waitUntil: 'load' }), childPage.click('#tab li:nth-child(4) a')]);

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await childPage.waitFor(1500);

  if (DEBUG) console.log('[Debug] 製品の情報を取得②');
  // OS種類
  const itemOs = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(2) td:nth-child(2)').innerText
  );
  // ネットワーク接続タイプ
  const itemNetwork = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(2) td:nth-child(4)').innerText
  );
  // CPU
  const itemCpu = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(4) td:nth-child(2)').innerText
  );
  // コア数
  const itemCore = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(4) td:nth-child(4)').innerText
  );
  // 画面サイズ
  const itemDisplaySize = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(14) td:nth-child(2)').innerText
  );
  // パネル種類
  const itemDisplayType = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(14) td:nth-child(4)').innerText
  );
  // 画面解像度
  const itemDisplayResolution = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(15) td:nth-child(2)').innerText
  );
  // 重量
  const itemWeight = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(30) td:nth-child(2)').innerText
  );
  // サイズ
  const itemSize = await childPage.evaluate(
    () => document.querySelector('#mainLeft > table > tbody > tr:nth-child(30) td:nth-child(4)').innerText
  );

  if (DEBUG) console.log('[Debug] 製品ページを閉じる');
  await childPage.close();

  if (DEBUG) console.log('[Debug] 製品画像をダウンロード');
  const filePath = path.join(PATH, itemTitle + itemImgSrc.split('.').pop());
  await common.downloadImage(itemImgSrc, filePath);

  return [
    itemTitle,
    itemImgSrc,
    itemPrice,
    itemRanking,
    itemReview,
    itemBbs,
    itemOs,
    itemNetwork,
    itemCpu,
    itemCore,
    itemDisplaySize,
    itemDisplayType,
    itemDisplayResolution,
    itemWeight,
    itemSize,
  ];
};

(async () => {
  console.log('[Info] ■■■ 価格.com売れ筋製品を取得 Start ■■■');

  if (DEBUG) console.log('[Debug] 前回実行時のファイルを削除');
  await common.deleteFiles(PATH);

  if (DEBUG) console.log('[Debug] Chromium起動');
  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50,
  });

  if (DEBUG) console.log('[Debug] 新しいタブを開く');
  const page = await browser.newPage();

  if (DEBUG) console.log('[Debug] 指定した製品一覧ページへ移動');
  await page.goto(URL, { waitUntil: 'domcontentloaded' });

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await page.waitFor(1500);

  if (DEBUG) console.log('[Debug] 売れ筋1~10位までのhrefを取得');
  let hrefs = await page.evaluate(() => Array.from(document.querySelectorAll('td.ckitemLink > a')).map((a) => a.href));
  hrefs.splice(10);

  if (DEBUG) console.log('[Debug] 1.5秒待機');
  await page.waitFor(1500);

  if (DEBUG) console.log('[Debug] 取得したhrefだけ繰り返し');
  const items = [];
  items.push([
    '製品名',
    '画像',
    '最安価格',
    '売れ筋ランキング',
    '満足度・レビュー',
    'クチコミ',
    'OS種類',
    'ネットワーク接続タイプ',
    'CPU',
    'コア数',
    '画面サイズ',
    'パネル種類',
    '画面解像度',
    '重量',
    'サイズ',
  ]);
  for (const href of hrefs) {
    if (DEBUG) console.log('[Debug] 各製品ページを開き、製品情報を取得');
    items.push(await getItemInfo(browser, href));
  }

  if (DEBUG) console.log('[Debug] 製品情報を表示');
  console.log(items);

  if (DEBUG) console.log('[Debug] Chromium終了');
  await browser.close();

  if (DEBUG) console.log('[Debug] 検索結果をCSV出力');
  await common.createCsv(items, path.join(PATH, 'data.csv'), 'UTF-8');

  console.log('[Info] ■■■ 価格.com売れ筋製品を取得 End ■■■');
})();

主なAPI仕様

Puppeteerクラス

launch:ブラウザを開く

headless:デフォルトはブラウザ非表示。falseでブラウザを表示
devtoolstrueで開発者ツールを開く
slowMo:全体的にゆっくり操作をさせる。50〜100程度で人間の操作に近い速度感と思われる
executablePath:Chromiumではなく、インストール済のChromeを使う場合にパスを設定
defaultViewport:デフォルトの画面サイズ等を変更

executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',

devices:デバイスのプリセットを利用

Browserクラス

close:ブラウザを閉じる

newPage:新しいタブを開く

Pageクラス

setViewport:ビューポートを指定

width:幅
height:高さ
deviceScaleFactor:スケール

setUserAgent:ユーザーエージェントを指定

goto:ページを移動

url:URL
timeout:タイムアウト時間
waitUntil:移動成功の判断基準。デフォルトはページ表示完了

waitFor:指定した時間、またはセレクターが表示されるまで、または関数が終了するまで待機

waitForSelector:セレクターが表示されるまで待機

waitForNavigation:ページ移動後の待機

waitUntil:移動成功の判断基準

title:ページタイトルを取得

await page.title()

$$:セレクターに該当する要素すべてを取得

ElementHandleが返される
https://github.com/puppeteer/puppeteer/blob/v2.1.0/docs/api.md#pageselector-1

evaluate:関数を実行して結果を返す

screenshot:スクリーンショットを保存

path:スクリーンショットの保存先
type:拡張子を指定。デフォルトはpng
quality:拡張子がjpegの場合の画像品質を0〜100で指定
fullPage:スクロール可能な場合、true、で全体を取得。デフォルトはfalse

pdf:PDFファイルを保存

path:PDFファイルの保存先
scale:スケール。デフォルトは1
landscape:用紙の向き
pageRanges:印刷するページの範囲。デフォルトは全ページ

type:指定した要素に文字入力

click:指定した要素をクリック

ElementHandleクラス

getProperty:指定したプロパティの値を返す

その他

利用検証時や、実務での開発で利用した内容

セレクターで要素の取得確認

開発者ツールのConsoleタブで下記コマンドを入力することで、要素が取得できているかを確認可能

console
document.querySelectorAll('確認したいセレクター');

ダウンロードパスを指定

ダウンロードパスは絶対パスで指定。
フォルダがない場合は自動作成される。

sample
const downloadPath = 'C:\\test\\puppeteer';
await page._client.send(
  'Page.setDownloadBehavior',
  { behavior: 'allow', downloadPath: downloadPath }
);

ファイルアップロード(input type="file")

sample
// input要素を取得し、アップロードするファイルを指定
const filePath = 'sample.csv';
const inputFileHandle = await page.$('input[name="f/filename"]');
await inputFileHandle.uploadFile(filePath);

// インポートボタンをクリック
await Promise.all([
  page.waitForNavigation({ waitUntil: 'load' }),
  page.click('input[value="インポート"]')
]);

画像認証のキーワード取得

sample
// 認証画像のスクリーンショットを保存
const auth_img = await page.$('img[src="●●●●●"]');
await auth_img.screenshot({ path: path.join(PATH, 'auth_img.png') });

// 認証画像からキーワードを取得
execSync(`tesseract ${PATH}/auth_img.png ${PATH}/auth_img`);
const keyword = fs.readFileSync(`${PATH}/auth_img.txt`, 'utf-8').split('\n')[0];

参考

Puppeteer API v2.1.0
puppeteerでファイルダウンロードのダウンロードパスを設定する
VS CodeにPrettier・ESLint・Stylelintを導入してファイル保存時にコードを自動整形させる方法
eslint & prettier を整備したので設定についてまとめた