node.js + puppeteer + axios でポケモンカードの画像（漆黒のガイストのカードリスト）をスクレイピングしてみる

## はじめに
sveltekitでホームページつくりました。良かったら見てください。
[安全にインターネットを楽しもう](http://safetyinternet001.ddns.net/)
[Twitter](https://twitter.com/safetyinternetz)

## やりたいこと
・公式のカードページから画像をスクレイピングしたい
・最新弾である[[漆黒のガイストのカードリスト]](https://www.pokemon-card.com/card-search/index.php?mode=statuslist&pg=732)の画像を全部保存したい

## 必要なもの
・node.js
・puppetter
・axios

## 手順
まずは`npm init -y`でさくっと環境をつくる。

```bash
npm init -y
npm i puppeteer
npm i axios
touch index.js
```
で、今後 node index.jsで起動していきます。

## まずはページを表示するだけ
```JavaScript
const puppeteer = require("puppeteer");

(async () => {
  //↓実際にブラウザで動きを見たければfalseに
  const browser = await puppeteer.launch({headless: true}); 
  const page = await browser.newPage();
  await page.goto("https://www.pokemon-card.com/card-search/index.php?mode=statuslist&pg=732");
  await page.screenshot({ path: 'screenshot.png' })
})();
```

これでフォルダにscreenshot.pngというファイルができて、スクリーンショットが撮れてたらOKです。

## img要素をまとめて配列に入れる
次に、img要素を拾ってくる処理をつくります。

```JavaScript
const puppeteer = require("puppeteer");
(async () => {
  //↓実際にブラウザで動きを見たければfalseに
  const browser = await puppeteer.launch({headless: true}); 
  const page = await browser.newPage();
  await page.goto("https://www.pokemon-card.com/card-search/index.php?mode=statuslist&pg=732");
  //使わないよ　await page.screenshot({ path: 'screenshot.png' });

  //imgタグのセレクター。cssとかidで指定もできるよ。
  const listSelector = await "img";
  const datas = await page.$$eval(listSelector, (list) => {
    return list.map((data) => data.src);
  });
  await console.log(datas)
})();
```
すると、こんな配列がconsoleに出たかと思います。

```node.js
[
  'https://www.pokemon-card.com/assets/images/logo.svg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039432_P_REDEIBA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039433_P_REDEIAN.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039434_P_SEREBIIV.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039435_P_SEREBIIVMAX.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039436_P_TANEBO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039437_P_SARUNORI.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039438_P_BACHINKI.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039439_P_GORIRANDA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039440_P_KODAKKU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039441_P_GORUDAKKU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039442_P_NYURA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039443_P_MANYURA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039444_P_POWARUNAMAMIZUNOSUGATA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039445_P_RABUKASU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039446_P_OTAMARO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039447_P_KAPUREHIRE.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039448_P_UOCHIRUDONV.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039449_P_MERIPU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039450_P_MOKOKO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039451_P_DENRYUU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039452_P_SHIMAMA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039453_P_ZEBURAIKA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039454_P_EMONGA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039455_P_ZERAORAV.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039456_P_GOSU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039457_P_GOSUTO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039458_P_GENGA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039459_P_YAMIRAMI.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039460_P_KURESERIA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039461_P_GOBITTO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039462_P_GORUGU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039463_P_MAIKA.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039464_P_KARAMANERO.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039465_P_ABURI.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039466_P_ABURIBON.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039467_P_KOKUBABADOREKKUSUV.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039468_P_KOKUBABADOREKKUSUVMAX.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039469_P_GAMAGARU.jpg',
  'https://www.pokemon-card.com/assets/images/card_images/large/S6K/039470_P_GAMAGEROGE.jpg',
  'https://www.pokemon-card.com/assets/images/ajax-loader.gif'
]
```

あとはこの配列にmapで逐次アクセスし、画像をつくるだけです。
それではその部分の関数もつくりましょう。

## URLの入った配列に順番にアクセスし、画像を作成する関数。
```JavaScript
const axios = require("axios");
const fs = require("fs");

/* urlSplit関数 
https://www.pokemon-card.com/assets/images/card_images/large/S6K/039432_P_REDEIBA.jpg
こういうアドレスがあったとき、[S6K]039432_P_REDEIBA.jpgというファイル名にしたいので、URLを分割して配列に入れておく。
 */
const urlSplit = (str) => {
  const array = str.split("/");
  const returnArray = [array.slice(-2)[0], array.slice(-1)[0]];
  return returnArray;
};

const imageSave = async (url) => {
  //Urlを上の関数で分割しておく。
  const imgName = await urlSplit(url);

  //axiosで取りにいく。
  const res = await axios.get(url, { responseType: "arraybuffer" });

  //取れた画像をfsで書き込む。./img/[S06K]039432_P_REDEIBA.jpgと保存される。
  fs.writeFileSync(
    `./img/[${imgName[0]}]${imgName[1]}`,
    new Buffer.from(res.data),
    "binary"
  );
};
```

ちなみに`logo.svg`とか`ajax-loader.gif`みたいなファイルもできちゃいます。
嫌ならif文とかで良い感じに除外してください。
僕は面倒なので手動で消してます。


## ページ遷移
このままだと1ページ目しか取れないので、ページを移動して2ページ目、3ページ目と取ります。
ちゃんと関数に分割したほうが良いのですが、今回は面倒なので同じ作業を3回書きます。

```JavaScript
  const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
  await page.click(".next");
  await sleep(5000);
```
こんな感じの処理を入れたら、次のページに移動してちょっと待ってくれます。
あとは同じように保存の処理を入れるだけです。

## 完成物
```JavaScript
const puppeteer = require("puppeteer");

const sleep = (msec) => new Promise((resolve) => setTimeout(resolve, msec));
(async () => {
  const browser = await puppeteer.launch({ headless: true });
  const page = await browser.newPage();
  await page.goto(
    "https://www.pokemon-card.com/card-search/index.php?mode=statuslist&pg=732"
  );
  const listSelector = await "img";
  const datas3 = await page.$$eval(listSelector, (list) => {
    return list.map((data) => data.src);
  });
  await console.log(datas3);
  await datas3.map((item) => imageSave(item));

  await page.click(".next");
  await sleep(5000);
  const datas = await page.$$eval(listSelector, (list) => {
    return list.map((data) => data.src);
  });
  await console.log(datas);
  await datas.map((item) => imageSave(item));

  await page.click(".next");
  await sleep(5000);
  const datas2 = await page.$$eval(listSelector, (list) => {
    return list.map((data) => data.src);
  });
  await console.log(datas2);
  await datas2.map((item) => imageSave(item));
  await browser.close();
})();

const axios = require("axios");
const fs = require("fs");
const imageSave = async (url) => {
  const imgName = await urlSplit(url);
  const res = await axios.get(url, { responseType: "arraybuffer" });
  fs.writeFileSync(
    `./img/[${imgName[0]}]${imgName[1]}`,
    new Buffer.from(res.data),
    "binary"
  );
};

const urlSplit = (str) => {
  const array = str.split("/");
  const returnArray = [array.slice(-2)[0], array.slice(-1)[0]];
  return returnArray;
};
```

スーパー愚直な画像スクレイピングでした。
もっともっとコードは綺麗にできます。
同期的に処理して、同時に異なるアドレスにアクセスして高速化とかもできると思います。
今回は動きをわかりやすくしたいので、async awaitで手続き型っぽい動きになってます。
とりあえず目的が達成できれば良いだけなら、これを`node index.js`で画像がダウンロードできます。

と、いうことで画像が保存できましたとさ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1308357/96d241ba-5e9e-dbc0-b670-4a4619718202.png)

## 終わりに
sveltekitでホームページつくりました。良かったら見てください。
[安全にインターネットを楽しもう](http://safetyinternet001.ddns.net/)
[Twitter](https://twitter.com/safetyinternetz)