PlaywrightでJavaScriptの動的サイトをスクレイピング

## 概要

年末年始は、Playwrightを使って、JavaScriptの動的サイトをスクレイピングをやっていました。

JavaScriptの動的サイトというのは、ページ表示後にJavaScriptが実行されてページが完成するサイトのことです。

例えば、以下のようなサイトがあった場合

![スクリーンショット 2024-01-10 18.07.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61162/e59ea8c1-97a0-bbd5-eead-5ce908cfdcf7.png)

HTMLが、このようなものであれば、ツールを使わなくても簡単にスクレイピングできます。
（HTMLを取ってきて文字列処理するだけ）

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Playwrightテストページ</title>
</head>
<body>
    <h1>ようこそPlaywrightテストページへ</h1>
    <h2>おすすめサイトのリンク</h2>
    <ul>
        <li><a href="https://www.nnn.ed.nico/">N予備校</a></li>
        <li><a href="https://zen-univ.jp/">ZEN大学</a></li>
    </ul>
</body>
</html>
```

しかし最近では、Next.js や Vue.js のような、SPA(シングルページアプリケーション)フレームワークの登場で、HTMLを取得しても、ページの情報が無いものも増えてきました。

先程のページのコンテンツを、別ファイルのJavaScriptで描画するようにしてみました。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Playwrightテストページ</title>
</head>
<body>
    <script src="index.js"></script>
</body>
</html>
```

```index.js
const h1 = document.createElement('h1');
h1.innerText = 'ようこそPlaywrightテストページへ';
document.body.appendChild(h1);
const h2 = document.createElement('h2');
h2.innerText = 'おすすめサイトのリンク';
document.body.appendChild(h2);
const ul = document.createElement('ul');
ul.innerHTML = `<li><a href="https://www.nnn.ed.nico/">N予備校</a></li>
    <li><a href="https://zen-univ.jp/">ZEN大学</a></li>`
document.body.appendChild(ul);
```

こういう構成だと、HTMLを取得しても何のページが分からず、スクレイピングできません。

## Selenium

昔ながらのツールにSeleniumがあります。
Seleniumは、実際にブラウザを起動して、表示されたものを基にスクレイピングできるので、JavaScriptの動的サイトも対応可能です。

私もよく実務でブラウザの表示テストツールとして使っていましたが、環境構築が大変で、なぜかよく失敗するという不安定なものでした（一時、精神的に参ったことがありました…）

なので、Seleniumを使うぐらいなら諦めるぐらいのレベルでございました。

## Playwright

Playwrightは、とにかく環境構築が簡単で、安定的に動作するのが良いです。
実際に環境構築をしつつ、先程のページの情報をスクレイピングしてみましょう。

まずは、[Playwrightのイントロページ](https://playwright.dev/docs/intro) を参考に、手元のプロジェクトにPlaywrightをインストールします。

```js
npm init playwright@latest
```

TypeScriptかJavaScriptか選べますが、今回はJavaScriptを選びました。
後はEnterでオッケーです。

## タイトルを取ってみる

ではタイトルを取ってみましょう。

とはいっても、こんなプログラムで簡単に取ることができます。

```js:exec.js
const playwright = require('playwright');

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

  await page.goto('https://labeneko.github.io/playwright_test/');
  const title = await page.locator('h1').innerText();
  console.log(title)
  await browser.close();
})();
```

```
$ node exec.js 
ようこそPlaywrightテストページへ
```

## 要素の表示を待って取る

今回は処理がシンプルすぎるため、特に問題なく取れましたが、サイトによってはJavaScriptの実行に時間がかかるものもあります。

```js:index.js
setTimeout(() => {
    const h1 = document.createElement('h1');
    h1.innerText = 'ようこそPlaywrightテストページへ';
    document.body.appendChild(h1);
    const h2 = document.createElement('h2');
    h2.innerText = 'おすすめサイトのリンク';
    document.body.appendChild(h2);
    const ul = document.createElement('ul');
    ul.innerHTML = `<li><a href="https://www.nnn.ed.nico/">N予備校</a></li>
        <li><a href="https://zen-univ.jp/">ZEN大学</a></li>`
    document.body.appendChild(ul);
}, 5000);
```

こんな感じで、ページの表示に5秒かかるようなサイトにしてみます。

この場合は、h1の表示を待ってから取るというプログラムを書くことで要素を取得できます。（Playwrightは優秀なので、書かなくても取れることがほとんどです）

```exec.js
const playwright = require('playwright');

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

  await page.goto('https://labeneko.github.io/playwright_test/');
  await page.waitForSelector('h1');
  const title = await page.locator('h1').innerText();
  console.log(title)
  await browser.close();
})();
```

## 要素のループ

おすすめサイトのリンクとページ名を取得するのはこんな感じ

```exec.js
const playwright = require('playwright');

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

  await page.goto('https://labeneko.github.io/playwright_test/');
  await page.waitForSelector('h1');
  const title = await page.locator('h1').innerText();
  console.log(title)

  const elements = await page.locator('ul li');

  for (let i = 0; i < await elements?.count(); i++) {
    const element = await elements.nth(i);
    const link = await element.locator('a').getAttribute('href');
    const text = await element.locator('a').innerText();
    console.log(link);
    console.log(text);
  }
  await browser.close();
})();
```

簡単に取れますね！(Seleniumに比べて)

```
takatsuno:playwright_test takahiro_tsuno$ node exec.js 
ようこそPlaywrightテストページへ
https://www.nnn.ed.nico/
N予備校
https://zen-univ.jp/
ZEN大学
```