Node.jsで簡単♪スクレイピング


どうも、**プログラミングは料理に似ている**
白金御行＜プラチナ☆みゆき＞です。

ということで今回は料理レシピみたいなテンションでNode.jsのスクレイピング手順を記述していきます。
参考に本稿では[Cookpadさんのtech blog](https://techlife.cookpad.com)の記事をスクレイピングしてJSON形式で書き出すものを作っていきましょう。

また、本稿はN番煎じ記事なので新規性はなく、主に備忘用の記事になります。

## 材料

- 言語：Node.js(v12.19.0)
- HTTPクライアントライブラリ：node-fetch
- HTMLパーサライブラリ：jsdom
- ファイル操作ライブラリ：fs

## 作り方

まず結論からということでCode全文を以下に記載して、後に個々の説明に入ります。

```js

import fetch from 'node-fetch';
import jsdom from 'jsdom';
import fs from 'fs';

const { JSDOM } = jsdom;

// HTTPリクエストからDOM要素を取得
async function getArchive(url, year) {
    const res = await fetch(url+String(year));
    // resが返るまでwait
    const html = await res.text();
    // htmlが返るまでwait
    const dom = new JSDOM(html);
    const document = dom.window.document;
    const titles = document.querySelectorAll('.entry-title-link');
    const dates = document.querySelectorAll('time');

    for (let i=0; i < titles.length; i++) {
        let reDate = dates[i].textContent.replace(/\s/g, '');
        nodeArray.push({
            url: titles[i].href,
            title: titles[i].textContent,
            date: reDate,
        })
    };
};

// write to JSON file
const writeJson = function(data, filename) {
    console.log('write start');
    fs.appendFile('data/'+filename+'.json', JSON.stringify(data, null, '    '), (error) => {
        console.log('write end');
    });
};

// wait
const sleep = () => new Promise(resolve => {
    setTimeout(() => {
        resolve()
    }, 5000)
});

// run
let nodeArray = [];
let url = 'https://techlife.cookpad.com/archive/';
(async function(filename) {
    for (let year=2021; year>2007; year--) {
        await getArchive(url, year);
        await sleep();
        console.log(year);
    }
    writeJson(nodeArray, filename);
})('cookpad');

```

### moduleのimport

```js
import fetch from 'node-fetch';
import jsdom from 'jsdom';
import fs from 'fs';

const { JSDOM } = jsdom;
```

まず必要なmoduleをimport文でimportしていきます。次にJSDOMというHTMLパーサのコンストラクタを生成しておくことがポイントです。

### スクレイピング関数を用意

```js
// HTTPリクエストからDOM要素を取得
async function getArchive(url, year) {
    const res = await fetch(url+String(year));
    // resが返るまでwait
    const html = await res.text();
    // htmlが返るまでwait
    const dom = new JSDOM(html);
    const document = dom.window.document;
    // native JSと同じように処理
    const titles = document.querySelectorAll('.entry-title-link');
    const dates = document.querySelectorAll('time');

    for (let i=0; i < titles.length; i++) {
        let reDate = dates[i].textContent.replace(/\s/g, '');
        nodeArray.push({
            url: titles[i].href,
            title: titles[i].textContent,
            date: reDate,
        })
    };
};
```

getArchiveというHTML要素をスクレイピングする関数の定義部分ですが、async/await文によって**非同期処理**とすることがポイントです。JavaScriptでメモリを長時間使う処理は順次実行を無視して先々処理が進んでしまいます。
それを防止するためにWebサーバからデータをfetchする処理とHTML本文を抜き出す処理にはawait文を付けることによって後続の処理を待機させています。
後は先ほど作ったJSDOMコンストラクタでdomインスタンスを生成。
タイトルと記事URL、作成日時が欲しいのでそれらのnodeをquerySelectorAllで取得しています。

次に、for文中でnodeArray(あとで作る)に次々とObject形式のデータを放り込んでいってます。

### JSON形式で出力

```js
// write
const writeJson = function(data, filename) {
    console.log('write start');
    fs.appendFile('data/'+filename+'.json', JSON.stringify(data, null, '    '), (error) => {
        console.log('write end');
    });
};
```

書き込むデータ(data)と書き出すファイル名(filename)を引数に取る関数を作成しています。
fs.appendFileの第3引数には**コールバック関数が必須**という点がポイントです。

### 待機

```js
// wait
const sleep = () => new Promise(resolve => {
    setTimeout(() => {
        resolve()
    }, 5000)
});
```

連続でのリクエストはサーバに負荷をかけるので1リクエストにつき5秒待機する関数を定義しています。

### 実行

```js
// run
let nodeArray = [];
let url = 'https://techlife.cookpad.com/archive/';
(async function(filename) {
    for (let year=2021; year>2007; year--) {
        await getArchive(url, year);
        await sleep();
        console.log(year);
    }
    writeJson(nodeArray, filename);
})('cookpad');
```

最後に実行部です。非同期処理でスクレイピングと待機を望む年数分実行し、最後にJSON形式でファイルに書き出しています。

### 結果

```js
{
        "url": "https://techlife.cookpad.com/entry/2021/09/06/130000",
        "title": "Cookpad Summer Internship 2021 10 Day Techコースを開催しました！",
        "date": "2021-09-06"
    },
    {
        "url": "https://techlife.cookpad.com/entry/2021/08/24/175828",
        "title": "AWSフル活用！クッキングLiveアプリ「cookpadLive」を支える技術",
        "date": "2021-08-24"
    },
    {
        "url": "https://techlife.cookpad.com/entry/2021/08/19/100000",
        "title": "レガシーとなった TLS 1.0/1.1 廃止までの道のり",
        "date": "2021-08-19"
    },
```

結果の一部ですが、このようにデータが正常に取得、JSON形式で出力できていることがわかりますね！

## おわりに

私はいつもサーバサイドにはPythonを使っていますがNode.jsでも問題なくスクレイピングできることがわかりました。非同期処理の形式で書く必要があるのがNode.jsの特殊なところかと思いますので詰まった方は以下の参考をお読みください。

## 参考

- [Node.js でお手軽スクレイピング 2020 年夏](https://qiita.com/otchy/items/244c19c561ecb7211fa5)
- [async/await 入門（JavaScript）](https://qiita.com/soarflat/items/1a9613e023200bbebcb3)
- [Node.js でファイルを保存する方法
](https://qiita.com/phi/items/b5ade0c2ecc58935e046)