Puppeteerを使って、画像切り出しをやってみた

## Puppeteerで何をした？
某移行案件で、古く使われてるサイトもあり、デザインモック、psdデータなど残ってなかった。ある日、<b>「結構長いページですけど、午前中に画像もらえますか？」</b>と言われた。

ついに来た。無茶振り。

ざっと見て、100枚ほどの画像を2時間でやれと。ふむ。
puppeteerのおかげで一気に書き出しができたので、誰かの助けになれたらーと思っての記事です。

（開発者ありがとう T__T)


## Puppeteerとは？
### puppeteerの意味
Puppeteerの意味は人形使い。パペットを扱う人が、puppeteerだそうです。かっこいいですね、ブラウザーをパペットとし、コントロールしちゃう発想。

### Puppeteerができることは

Puppeteerは、DevToolsプロトコルまたはWebDriver BiDiを介してChromeやFirefoxを制御するための高レベルAPIを提供するJavaScriptライブラリです。(ChatGPT訳)

:::note info
Puppeteerでは、マウス、タッチイベント、キーボード入力を通じてページ上の要素とやり取りすることができます。通常、まずCSSセレクタを使用してDOM要素をクエリし、次に選択した要素に対してアクションを実行します(ChatGPT訳)
:::

簡単にいうと、CSSセレクタから切り出しして、画像やテキスト情報など取得することができる。実際、いろんなデバッグ作業もこれで実現できるらしいですが、今回はCSSセレクトの切り出しのみやりました! 


## 実装

https://pptr.dev/

↑pupppeteerの公式サイトにQuick Startがありますので、基本サンプルコードで、できます。そして、通信は必要なので、今回はnode.jsで作ってみました。


```
const puppeteer = require('puppeteer');
const path = require('path');

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

  await page.setJavaScriptEnabled(true);
  await page.setViewport({
  
  //サイズを決める、画質と画像サイズに影響ある部分です。
    width: 1980,
    height: 800,
    deviceScaleFactor: 2,
  });

  await page.setRequestInterception(true);
  page.on('request', request => {
    if (request.url().includes('cookie')) {
      request.abort();
    } else {
      request.continue();
    }
  });

  const targetURL = 'https://サイトのアドレスを入れてください';
  await page.goto(targetURL, { waitUntil: 'domcontentloaded' });

//ポイント①
//CSSを読み出しできてから待ってもらう
  await page.waitForSelector('CSSのセレクトを指定', { visible: true });

  await page.evaluate(() => {
    const style = document.createElement('style');

//ポイント②
// 意外にも重要なフォント指定

    style.textContent = `
      * {
        font-family: 'DNP Shuei Mincho Pr6', serif !important;
      }
    `;
    document.head.appendChild(style);
  });


//ポイント③
//追従するヘッダー、フッターを書き出しさせないため
  await page.evaluate(() => {
    const header = document.querySelector('#header');
    const footer = document.querySelector('#footer');
    if (header) header.style.display = 'none';
    if (footer) footer.style.display = 'none';
  });

  await page.evaluate(async () => {
    const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    let previousHeight = 0;

    while (true) {
      const currentHeight = document.body.scrollHeight;
      window.scrollTo(0, currentHeight);
      await delay(500);
      if (currentHeight === previousHeight) break;
      previousHeight = currentHeight;
    }
  });
```

↑ポイント①-③は苦労しました!!!
① 指定した要素が確実にDOM上で操作可能な状態になるのを保証します。
② 全然違うフォントや、タイトル崩れなど起きたので、フォント指定は意外にも重要でした。
③ 一番大変だったところ、サイトの都合上、追従ヘッダー、フッター(時々ボタン)などを、ここでミュートさせる。

ここまで来たら、あとはハッピーです!


``` 
  // スクリーンショットを撮る親要素のセレクタ
  const parentSelector = '指定する親要素';
  const fileName = '画像名を設定.jpg';

  const parentElement = await page.$(parentSelector);
  if (parentElement) {
    await parentElement.screenshot({
      path: path.resolve(__dirname, fileName),
      quality: 100,
    });
    console.log(`${fileName} にスクリーンショットを保存しました`);
  } else {
    console.log(`${parentSelector} が見つかりませんでした`);
  }

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

↑これで、画像が綺麗に切り取れました。
上記の部分を改造して、同じタグのものを順番に切り出すことも可能だったので、自分が担当したサイトが、こういったCSSの使い回しが多かったので、、、

このようにして、書き出しの部分をちょっと変更して、複数枚を一気に書き出しました!!
指定階層のものを、for文で回して、順番に書き出してもらい、100枚なんて、10分で終わった(やった!!)

```
const selectors = [
    { parentSelector: '#id1', childSelector: '.css1', baseFileName: '基本ファイル名1' }
  ];

  for (const item of selectors) {
    const parentElement = await page.$(item.parentSelector);
    if (parentElement) {
      const childElements = await parentElement.$$(item.childSelector); // 複数要素を取得
      if (childElements.length > 0) {
        for (let i = 0; i < childElements.length; i++) {
          const childElement = childElements[i];
          const fileName = `${item.baseFileName}_${i + 1}.jpg`;
          await childElement.screenshot({
            path: path.resolve(__dirname, fileName),
            quality: 100,
          });
          console.log(`${fileName} にスクリーンショットを保存しました`);
        }
      } else {
        console.log(`${item.childSelector} が ${item.parentSelector} の中に見つかりませんでした`);
      }
    } else {
      console.log(`${item.parentSelector} が見つかりませんでした`);
    }
  }
  ```

最後に `node ファイル名` で書き出しして、終わり!!!


  ## やってみて良かったこと & 気づき
①
すごく早くできたかつ、指定要素のみ書き出したので、ヒューマンエラーのない工程ができた。

②
こうやって早く出来上がったのが、コーダーさんがすごく綺麗に階層を整理してくれたおかげです。たまに、クソCSS書く自分もいますが、人のコード読んで、身が引き締まります。

③
なんか問題ある時に、こうやってライブラリーの力を借りてできたこと、本当に、開発者に感謝しかない。特にバタバタしていた期間に、puppeteerに出会えたことに感謝〜

## 参考サイト

1.  [Puppeteer公式サイト](https://pptr.dev/)
2.  [Puppeteerの魅力は何か～ヘッドレスChromeを自由自在に操る](https://gihyo.jp/article/2022/09/rapid-learning-puppeteer-01)
3.  [【データ収集に！】Puppeteerの使い方を徹底解説！スクレイピングの初心者にもおすすめ【Node.js】](https://zenn.dev/masakasuno1/articles/ee1ab796e58ddb)