🎭

Playwrightにふれる その(1) : スクレイピングとUI操作シミュレーション

に公開

Playwrightとは

Playwrightとは、Microsoftが開発したブラウザ操作を自動化するためのオープンソースのNode.jsライブラリです。Webアプリのテストやスクレイピングに使うことが可能です。
Chrome/Chromium、Firefox、Safari(WebKit)の3大ブラウザをクロスプラットフォームでサポートしており、実際のブラウザを起動して、人が操作するのと同じように「ページ遷移」「クリック」「入力」「スクリーンショット取得」などができます。

今回はPlaywrightの基本的な部分を理解をするために、簡単にPlaywrightを使ったスクレイピングを試してみたいと思います。

手当たり次第にWebサイトのスクレイピングを行うのはよろしくないので、今回はスクレイピング練習用に用意されているサイトQuotes to Scrapeを練習台とさせていただきました。

スクレイピング試行用のサンプルコード

PlaywrightはNode.js製です。
軽く試すには同じ言語が良いだろうということでNode.jsで書いてみました。
以下コマンドで手元にプロジェクトを用意します。

mkdir playwright-scrape-demo
cd playwright-scrape-demo
npm init -y
npm install -D playwright typescript ts-node @types/node
npx playwright install

Playwrightのライブラリ自体はJavaScriptコードだけなので、実際にブラウザを操作するための「ブラウザバイナリ」が別途必要となります。最後のnpx playwright installコマンドでテストやスクレイピングで使う各ブラウザの自動操作環境を用意します。

作業の前にtsconfig.jsonだけ簡単に作っておきます。

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "CommonJS",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

ここまでの作業によって以下のようなディレクトリ構成のファイル群が用意できました。

playwright-scrape-demo/
├── package.json
├── tsconfig.json
└── node_modules/

ではここにスクレイピングを実行するスクリプトを用意してみましょう。
playwright-scrape-demo直下にscrapeWithLocator.tsというファイルを作成します。

ファイルの中身は以下の通りです。

import { chromium, Browser, Page, Locator } from 'playwright';

(async (): Promise<void> => {
  const browser: Browser = await chromium.launch({ headless: true });
  const page: Page = await browser.newPage();
  await page.goto('https://quotes.toscrape.com/');

  const quoteBlockLocator: Locator = page.locator('.quote:nth-child(3)');
  const quoteText: string = await quoteBlockLocator.innerText();

  console.log(quoteText);

  await browser.close();
})();

各箇所について以下詳細を見ていきます。

launch()してnewPage()してgoto()する

playwrightの操作環境構築関数3拍子です。

ファイルの冒頭でplaywrightからchromiumをインポートしています。これはいわゆるChromeを操作するという機能を持ったライブラリです。
chromium.launch()という関数を実行することで、ブラウザを立ち上げることができます。これによりスクレイピングやテストなど試行するためのブラウザを用意することができます。
処理に時間がかかるので、awaitで待つ必要があります。
Playwrightにはchromium.launch()で立ち上げたブラウザを表現する型としてBrowserが用意されているので、これもインポートしています。

Playwrightではブラウザ操作をシミュレートする際、UIを表示するかしないかを選択することができます。今回は{ headless: true }ということで、バックグラウンドでのシミュレーションをする設定としています。

引き続く、const page: Page = await browser.newPage();では、Playwrightで立ち上げたブラウザで目的のWebサイトに行くための準備を行なっています。これによりPlaywrightのブラウザ上にWebコンテンツを表示するための”新しいページ”が作成されます。またこの”ページ”を表現する型としてPageがあります。

用意したページオブジェクトに対して、goto関数を呼び出し、引数に遷移先URLを指定することで、目的のWebサイトをPlaywright上で表示しています。
これでスクレイピングの準備ができました。

上記の例では記載していませんが、goto関数実行後に、await page.content();とすることで、当該ページの全html要素を取得することができます。

特定の要素を指定するlocator()

本サンプルコードでは特定の要素を取得するためにlocator関数を利用しています。
locator関数の引数にクラス名やID名などを指定することで任意の要素を取得することが可能です。
今回はQuotes to Scrapeのトップページ上にある.quoteクラスを持つ要素を取得してみます。

ここでは.quoteクラスを持つ要素のうち、3つ目の要素を取得するために以下のように指定しました。
const quoteBlockLocator: Locator = page.locator('.quote:nth-child(3)');

そして、取得したlocatorに対して.innerText()を実行することで、要素内部の文字列要素が取得できます。

本スクリプトの実行結果は以下の通りです。

> npx ts-node scrapeWithLocator.ts
“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”
by Albert Einstein (about)
Tags: inspirational life live miracle miracles

3つ目の.quote要素の文字列が出力されていますね。

.quoteクラスを持つHTML要素は複数あります。locatorで取得した要素が複数である場合、そのLocatorオブジェクトに.innerText()を実行しようとするとエラーになります。

locator.innerText: Error: strict mode violation: locator('.quote') resolved to 10 elements:

locator関数ではクラス名やID名以外にも様々な要素特定方法があります。
ここではさらに、「特定の文字列を指定する方法」と「XPathを指定する方法」について見てきたいと思います。

特定の文字列から要素を指定する

locator関数の引数を"text=取得したい文字列"とすることで指定した文字列を持つ要素を取得することができます。

const headerLocator: Locator = page.locator("text=Top Ten tags");
const headerText: string = await headerLocator.innerText();

文字列を指定した要素のinnerTextを取得するのはあまり面白味はありませんが、headerTextを出力するとTop Ten tagsが出力されます。

また文字列指定の場合は、完全位置ではなく、指定した文字列を含む要素を取得するので、"text=Top Ten"としても同じ結果になります。
ただしこの時、一致する要素が複数ある場合は先ほどと同様のエラーになるので、注意が必要です。

XPathから要素を指定する

Chromeのデベロッパーツールから特定のHTML要素をコピーしようとすると、「XPathをコピー」という選択肢があります。

このXPathをLocatorに指定することも可能です。XPathで要素を指定する際はlocatorの引数をxpath=<コピーしたXPath>とします。

 const xpathLocator: Locator = page.locator("xpath=/html/body/div/div[1]/div[1]/h1/a")
  const xpathElmText: string = await xpathLocator.innerText();

コピーしたXPathにはダブルクォートが含まれる場合があります。
ダブルクォートを含んだXPathを指定する際は、ダブルクォートの直前にバックスラッシュを置いてエスケープする必要があります。

ここまで、Playwrightを使ったスクレイピング手法について簡単に見てきました。
今回ふれたのは極々初歩的な部分のみとなりますが、無事Playwrightの世界に足を踏み入れることができました。
要素の指定方法については公式ドキュメントに記載がありますので、こちらが参考になるかと思います。
https://playwright.dev/docs/locators

これらを踏まえて次はPlaywrightを使ったUI操作をシミュレートする方法について見てみたいと思います。

UIイベントをシミュレートする

Playwrightの看板機能であるUIイベントのシミュレーションについて見ていきます。
Quotes to Scrapeにはログインページが用意されているので、このログインフォームを練習台としてPlaywrightを使った「入力フィールドへの文字列入力」と「ボタンクリック操作」を行なってみます。

大まかな手順としてはスクレイピングと同様です。
launch()してnewPage()してgoto()した後に、locatorで目的の要素を取得し、その要素に対してUIイベントを発生させます。

入力フィールドへの文字列入力とボタンクリック

以下にログインフォームの入力フィールドへの文字列操作をおこなうスクリプトを示します。

import { chromium, Browser, Page, Locator } from 'playwright';

(async (): Promise<void> => {
  const browser: Browser = await chromium.launch({ headless: false, slowMo: 500 });
  const page: Page = await browser.newPage();
  await page.goto('https://quotes.toscrape.com/login');

  await page.waitForTimeout(2000);

  const usernameInputLocator: Locator = page.locator('xpath=//*[@id=\"username\"]');
  await usernameInputLocator.fill("Shinji Kagawa")

  await page.waitForTimeout(2000);

  await browser.close();
})();

スクレイピング時とは異なり、実際のブラウザ上の動きを見たいので、launch関数のオプション、headlessにはfalseを指定しています。
また、デフォルトだと文字通り目にも止まらぬ速さで操作が完了してしまうため、あえて、slowMo: 500というオプションも追加しています。
また各所にawait page.waitForTimeout(2000);を挿入し、文字列入力前後に何もせず2秒待ってもらっています。

今回はXPathusernameの入力フィールドを取得し、そのLocatorに対して、typeという関数で引数に指定した文字列の入力を行なっています。

このスクリプトを実行すると、DockにChromeのリージョンフォーム版みたいなアイコンが現れます。

これは「Chrome Playwrightのすがた」です。(テキトーですみません)
アイコンが現れた後、すぐにブラウザが立ち上がり、以下のような操作が行われる様子を見ることができます。

引き続いてpasswordフィールドへの文字入力とLoginボタンのクリックをシミュレートしてみましょう。

import { chromium, Browser, Page, Locator } from 'playwright';

(async (): Promise<void> => {
  const browser: Browser = await chromium.launch({ headless: false, slowMo: 500 });
  const page: Page = await browser.newPage();
  await page.goto('https://quotes.toscrape.com/login');

  await page.waitForTimeout(2000);

  const usernameInputLocator: Locator = page.locator('xpath=//*[@id=\"username\"]');
  await usernameInputLocator.fill("Shinji Kagawa")

  const passwordInputLocator: Locator = page.locator('xpath=//*[@id=\"password\"]');
  await passwordInputLocator.fill("foobar")

  const loginButtonLocator: Locator = page.locator('xpath=/html/body/div/form/input[2]');
  await loginButtonLocator.click()

  await page.waitForTimeout(5000);

  await browser.close();
})();

await page.waitForTimeout(5000);は都合上挿入したものとなります。実際には不要です。
こちらのスクリプトの実行結果は以下のとおりです。

右上のLoginリンクが、操作後、Logoutに表示が変わっているので、無事ログインができたという様子がわかります。

他のUIイベントのAPIについてはこちらの公式ドキュメントをご確認ください。
https://playwright.dev/docs/api/class-elementhandle#element-handle-fill

長くなりましたが、今回の内容は以上となります。
今回の記事執筆を通して無事Playwrightの世界に一歩踏み入ることができた気がします。
ゆくゆくはPlaywrightをRailsアプリケーションのE2Eテストとして利用してみたいと考えているので、次の機会にその内容を記事としてまとめたいと思います。

誤っている点などがあればご指摘いただけるとありがたいです。
ここまでお読みいただきありがとうございました!

Discussion