PlaywrightのChromiumブラウザなどの自動操作方法

Playwrightとは

Playwrightとは2020年1月31日にリリースされた、テスト自動化ツールです。

以前はPuppeteerというテスト自動化ツールが人気だったが、2024年現在はPlaywrightを使用する人のほうが多くなってきています。

ちなみにPlaywright (プレイライト)とは「脚本家」という意味です。

この記事では主にPlaywrightをインストールして、chromiumをimportして使用するやり方について説明しています。

Playwrightのインストール

Playwrightのインストールはディレクトリを作成して、cdで移動したあと「npm init playwright@latest」コマンドを実行するだけです。

実行後に色々聞かれますが、初めはすべてEnterを押してデフォルト設定にしてみてください。

$ mkdir my-test; cd my-test
$ npm init playwright@latest
✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'npx playwright install')? (Y/n) · true

インストール完了後、testsディレクトリにexample.spec.jsが入っているので、実行例をすぐに確認できます。

npx playwright test

実行結果のレポートを見るには「show-report」を実行します。

npx playwright show-report

この記事では以前書いたPuppeteerによるヘッドレスChromeの使い方 2021年度版の内容をPlaywrightで実行する方法について記載しています。

例えばGoogleトップページのスクリーンショットを保存したい場合、screenshot.mjsというファイル名で以下のコードを書いて保存して、node screenshot.mjsを実行すればスクリーンショットを保存できます。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()

  await page.goto('https://www.google.co.jp/')
  await page.screenshot({ path: 'screenshot.png' })
  await browser.close()
})()
node screenshot.mjs
Playwrightで保存したスクリーンショット

フルページのスクリーンショットの場合は以下のように「fullPage: true」を追加します。

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

特定の要素の範囲のみスクリーンショットの場合はlocatorを使用して要素を指定します。

await page.locator('form').screenshot({ path: 'screenshot_form.png' })
Playwrightでform要素部分を保存したスクリーンショット

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

Playwright自体にはスクリーンショット画像を保存する機能はありますが、統合する機能はないため、sharpライブラリをインストールして、以下のように統合します。

import { chromium } from 'playwright'
import sharp from 'sharp'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()
  const imgFileGoogle = 'screenshot_google.png'
  const imgFileYahoo = 'screenshot_yahoo.png'

  await page.goto('https://www.google.co.jp/')
  await page.screenshot({ path: imgFileGoogle })
  await page.goto('https://www.yahoo.co.jp/')
  await page.screenshot({ path: imgFileYahoo })

  const imageGoogle = sharp(imgFileGoogle)
  const imageYahoo = sharp(imgFileYahoo)
  const metadataGoogle = await imageGoogle.metadata()
  const mergedImageBuffer = await sharp({
    create: {
      width: metadataGoogle.width * 2,
      height: metadataGoogle.height,
      channels: 4,
      background: { r: 0, g: 0, b: 0, alpha: 0 },
    },
  })
    .composite([
      { input: await imageGoogle.toBuffer(), left: 0, top: 0 },
      { input: await imageYahoo.toBuffer(), left: metadataGoogle.width, top: 0 },
    ])
    .png()
    .toBuffer()

  await sharp(mergedImageBuffer).toFile('screenshot_combined.png')
  await browser.close()
})()

sharpライブラリの使い方については過去記事をご参照ください。

sharpでNode.jsによる画像変換処理を使用する方法

ブラウザを表示させて実行する

chromium.launchにヘッドレスモードを無効にする「headless: false」を追記すれば、Playwright実行時にブラウザを表示できます。

指定したページでどのように動いているか見たいときに有用です。

ブラウザを表示すると処理の完了に時間がかかるので、特に理由がなければブラウザはデフォルトの非表示状態で使用したほうが良いです。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch({ headless: false })
  const page = await browser.newPage()

  await page.goto('https://www.google.co.jp/')
  await page.screenshot({ path: 'screenshot.png' })
  await browser.close()
})()

実行速度をミリ秒で指定する

chromium.launchに { slowMo: 1000 } を設定すれば各アクションの間に指定されたミリ秒数の待機を挿入します。

「headless: false」を設定してブラウザを表示させているときは、どのように動作しているか見たいことが多いので、slowMoを併用することが多いです。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch({
    headless: false,
    slowMo: 1000, // 1秒
  })
  const page = await browser.newPage()

  await page.goto('https://www.google.co.jp/')
  await page.screenshot({ path: 'screenshot.png' })
  await browser.close()
})()

各ブラウザで実行する

PlaywrightではChromium (Chrome)だけでなく、FirefoxとWebkit (Safari)のブラウザも使用可能です。

例えば各ブラウザでスクリーンショットを保存したい場合は以下のようになります。

import { chromium, firefox, webkit } from 'playwright'

;(async () => {
  const browsers = [
    { name: 'Chromium', browser: chromium },
    { name: 'Firefox', browser: firefox },
    { name: 'WebKit', browser: webkit },
  ]

  await Promise.all(
    browsers.map(async ({ name, browser }) => {
      const launchBrowser = await browser.launch()
      const context = await launchBrowser.newContext()
      const page = await context.newPage()
      await page.goto('https://www.whatsmybrowser.org/')
      await page.screenshot({ path: `screenshot_${name}.png` })
      await launchBrowser.close()
    })
  )
})()

スクリーンショットを確認すると、Chrome, Firefox, Safariで認識されていることがわかります。

screenshot Chromium
screenshot Firefox
screenshot WebKit

スマートフォンブラウザで実行する

パソコン版ではなく、スマートフォン版のChroniumを実行したい場合はdevicesをimportして、browser.newContext()でdevicesを指定します。(以下はiPhone 11の例)

import { chromium, devices } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const context = await browser.newContext(devices['iPhone 11'])
  const page = await context.newPage()

  await page.goto('https://example.com/')
  await page.screenshot({ path: 'screenshot_iPhone_11.png' })
  await browser.close()
})()

保存したスクリーンショットは下図になります。

Playwright iPhone 11 screenshot

設定可能なデバイスの一覧はdeviceDescriptorsSource.jsonで確認できます。

domcontentloadedで処理速度を高速化

page.gotoの引数にURLだけを指定するとデフォルト設定のloadでイベントが発火します。

第2引数に { waitUntil: 'domcontentloaded' } を指定すると、DOMContentLoadedでイベントが発火するので、処理速度が高速化します。

スクリーンショットなど、画像読み込みなどの完了を待つ必要がなければ、指定したほうが良いです。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()
  const DCL = { waitUntil: 'domcontentloaded' }

  await page.goto('https://example.com/', DCL)
  console.log(await page.title())
  await browser.close()
})()

JavaScriptを無効で処理速度をさらに高速化

ページ読み込み時にJavaScriptを無効にすると処理がさらに高速化します。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const context = await browser.newContext({ javaScriptEnabled: false })
  const page = await context.newPage()
  const DCL = { waitUntil: 'domcontentloaded' }

  await page.goto('https://iwb.jp/', DCL)
  console.log(await page.title())
  await browser.close()
})()

指定したミリ秒待機(スリープ)

page.waitForTimeoutでミリ秒を指定すれば処理を指定した秒数だけ処理を待機できます。

await page.waitForTimeout(3000)

ページをリロードする

page.reloadでリロードできます。

アクセスする際にランダムに表示されるバナーなどの表示切替に使うと便利です。

await page.reload()

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

特定の要素のテキストを取得するには「page.$eval」を使用して要素を指定して以下のようにします。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()

  await page.goto('https://iwb.jp/')
  const siteTitle = await page.$eval('.site-title', (title) => {
    return title.textContent.trim()
  })
  console.log(siteTitle)
  await browser.close()
})()

metaタグの「name="description"」のようなページ内に存在するか不明の要素の場合は以下のようにして、存在しない場合はnullなどを返して実行中にエラーが出ないようにすると良いです。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()

  await page.goto('https://iwb.jp/')
  const description = await page.$eval('meta[name="description"]', (meta) => {
    return meta?.getAttribute('content') || null
  })
  console.log(description)
  await browser.close()
})()

複数の要素のテキストを取得する

最近の投稿リンク一覧のような複数の要素のテキストを取得したい場合は「page.$$eval」を使用して要素を指定して以下のようにします。

$が2つになっており、map()でまとめてテキストを返しています。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()
  const target = '#recent-posts-2 > ul > li > a'

  await page.goto('https://iwb.jp/')
  const links = await page.$$eval(target, (links) => {
    return links.map((link) => link.textContent)
  })
  console.log('最近の投稿リンク一覧')
  console.log(links.join('\n'))
  // WordPressの画像をAVIFに変換できるShortPixelプラグインの使い方
  // JavaScriptのディープコピーはstructuredClone()で可能
  // 生成AIでWebサイトを制作できるv0 by Vercelの使い方と注意点
  await browser.close()
})()

要素の有無の確認

要素の有無の確認する場合はawait page.$(要素)をif文で判定します。

if (await page.$('h1')) {
  console.log('ページ内にh1があります。')
} else {
  console.log('ページ内にh1がありません。')
}

if (await page.$('h7')) {
  console.log('ページ内にh7があります。')
} else {
  console.log('ページ内にh7がありません。')
}

特定の要素のHTMLを取得する

特定の要素のHTMLを取得するには$.eval()を使用してinnerHTMLで取得します。

外側のHTMLを含む場合はouterHTMLを使用してください。

import { chromium } from 'playwright'
;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()

  await page.goto('https://iwb.jp/')
  const postHTML = await page.$eval('#recent-posts-2', (el) => {
    return el.innerHTML
  })
  console.log(postHTML)
  await browser.close()
})()

フォームの入力とクリック

テキストフィールドの入力にはpage.fill、クリックはpage.clickを使用します。

クリック後のWebページのスクリーンショットを保存する場合はpage.waitForLoadState('load')を使用して読み込みが完了するのを待ちます。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch({ headless: false })
  const page = await browser.newPage()
  await page.goto('https://www.google.com/')
  await page.fill('[name="q"]', 'JavaScript')
  await page.click('[type="submit"]')
  await page.waitForLoadState('load')
  await page.screenshot({ path: 'screenshot.png' })
  await browser.close()
})()

フォームのvalue属性の値を取得

フォーム(要素)のvalue属性の値を取得するにはpage.$evalを使用する。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const page = await browser.newPage()
  await page.goto('https://www.google.com/')
  await page.fill('[name="q"]', 'JavaScript')
  const searchText = await page.$eval('[name="q"]', (el) => el.value)
  console.log(searchText) // JavaScript
  await browser.close()
})()

フォームのselectタグの選択

await page.selectOption('select[name="year"]', '2020')

inputタグのcheckboxやradioを選択

inputタグのcheckboxやradioを選択にうはpage.checkを使用します。

page.clickでも可能ですが、使い分けたほうが指定した要素がcheckbox or radioだということがわかりやすくなります。

checkboxでpage.clickを使用するとオンとオフが交互に入れ替わるので使用は避けたほうが良いです。

// checkboxをオン
await page.check('#checkbox')

// checkboxをオフ
await page.uncheck('#checkbox')

ブラウザの戻る・進むを実行する

ブラウザの戻る・進むを実行するには、page.goBack()、page.goForward()を使用する。

ブラウザのようにキャッシュを利用しないので、page.goto()のように { waitUntil: domcontentloaded } を指定しないと、デフォルトのloadイベントでの発火となるので、処理は遅い。

await page.goBack()
await page.goForward()

Promise.allによる並列処理

複数のURLにアクセスしてWebスクレイピングする場合は普通にforなどで順に処理するよりもPromise.allを使用して並列処理したほうが早くなります。

並列処理できる数には上限があるので、URLを指定しすぎないよう注意してください。

import { chromium } from 'playwright'

;(async () => {
  const browser = await chromium.launch()
  const urls = [
    'https://www.yahoo.co.jp/',
    'https://line.me/ja/',
    'https://mixi.jp/',
    'https://dena.com/jp/',
    'http://gree.jp/'
  ]
  const promiseList = []
  const titleList = []
  const DCL = { waitUntil: 'domcontentloaded' }

  for (const url of urls) {
    promiseList.push(
      (async () => {
        const page = await browser.newPage()
        const res = await page.goto(url, DCL)

        if (res.status() !== 200) return `${res.status()} ERROR`

        const result = await page.title()
        await page.close()
        return result
      })().catch((e) => console.error(e))
    )
  }

  await Promise.all(promiseList).then((vList) => {
    vList.forEach((title) => titleList.push(title))
  })

  console.log(titleList)
  await browser.close()
})()