ほんじゃーねっと

おっさんがやせたがったり食べたがったりする日常エッセイ

node.jsでWebスクレイピングして取得データを保存する

node.jsでデータ収集のためのWebスクレイピングを行う。

Webスクレイピングの流れというのはだいたい決まっていて、

  1. WebページにアクセスしてHTMLを取得する
  2. 取得したHTMLの中から必要なデータを抽出する
  3. 抽出したデータを保存する

の3段階となる。

通常、Webスクレイピングが必要となるのは

  • データ取得用のAPIが提供されていない
  • 必要なデータが1ページに収まらず多くのページにまたがっており、手作業でコピペしていくのが難しい

場合で、そのうちWebスクレイピングが可能なのは

  • 対象の各ページが同じフォーマットになっており、パターン化された処理で必要なデータを取得できる

場合となる。

例えば「ページングされた検索結果画面」とか

「同じフォーマットでHTMLが書かれたたくさんの商品詳細画面」

なんかがスクレイピングでデータ取得しやすい。

前提として、

Webスクレイピングは手作業 でWebページにアクセスしてコピペする、

という作業をプログラムが自動で行うようなもので、手動か自動かに関わらず

対象のデータを取得したり保存したりする権利なり許可なりが必要となる。

また、プログラムを使用する場合は

手動とちがってWebページに一度に大量のリクエストを行なうことが可能なので、

サーバに負荷をかけないように注意する必要がある。

node.jsでWebスクレイピングする場合、いくつか使えるライブラリがあるけど

「cheerio-httpcli」がつまづきにくくて良いと思う。

www.npmjs.com

puppeteerの方が人気ありそうだけど、自分のWindows環境ではうまく動かなかった。

取得したデータをデータベースに保存する場合、自分は「knex」を使ってる。

Knex.js - A SQL Query Builder for Javascript

あとは日付処理に便利な「moment」と

Moment.js | Home

リストやマップの処理に便利な「lodash」を必要に応じてインポートしておく。

lodash.com

だいたい下記のような形でスクリプトを始める:

// cheerio-httpcliでhttpsアクセスするための設定
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';

// 必要なライブラリをインポート
const httpClient = require('cheerio-httpcli');
const knexLib = require('knex');
const _ = require('lodash');
const moment = require('moment');

// knexで生成するDB情報保存用ファイルのインポート
const knexfile = require('./knexfile')['development'];

// リクエストごとに一定時間空けるためのsleep関数
async function sleep(waitMillSeconds) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve();
        }, waitMillSeconds);
    });
}

メイン処理は下記のような感じ。 例としてYahoo!天気の地震情報を数ページ分取得してみる。

typhoon.yahoo.co.jp

async function main() {
    // ベースURL。リクエストごとに変わらない部分
    const baseUrl = 'https://typhoon.yahoo.co.jp/weather/jp/earthquake/list/';

    // DBアクセス用のknexオブジェクトを生成
    const knex = knexLib({
        client: knexfile.client,
        connection: knexfile.connection
    });
    
    // リクエストごとに3秒待つための設定
    const waitMillSeconds = 3000;

    // 取得件数が500件を超えたら終了する設定
    const maxB = 500;

    // 1ページあたりの件数設定。
    const intervalB = 100;

    // 1ページ目から開始
    let currentB = 1;

    while (currentB < maxB) {
        console.log('b = ', currentB);

        // HTMLデータを取得
        const result = await httpClient.fetch(baseUrl, {sort: 1, key: 1, b: currentB});

        const $ = result.$;
        const logList = [];

        // HTML内の表の行データを取得して変換してリストに入れる
        $('tr', '#eqhist').filter(function () { return $(this).attr('bgcolor') === '#ffffff'; }).each(function () {
            const row = $(this);
            const cellList = $('td', row).map(function () { return $(this).text(); }).get();
            const rowId = $('a', $('td', row).first()).attr('href');
            const cellMap = _.zipObject(['happened', 'announced', 'place', 'magnitude', 'intensity'], cellList);
            cellMap.happened = moment(cellMap.happened, 'YYYY年M月D日 H時m分ごろ').toDate();
            cellMap.id = rowId;
            if (cellMap.magnitude !== '---') {
                logList.push(cellMap);
            }
        });

        // リストに入れたデータをデータベースに登録する
        for (const log of logList) {
            await knex('test_logs').insert(log);
        }

        currentB += intervalB;

        // 指定ミリ秒数待機
        await sleep(waitMillSeconds);
    }

    await knex.destroy();
}

main();

このスクリプトを流すと、取得したデータがデータベースに入るので、

あとはSQLで集計してみたりグラフ表示に使ってみたりすることができる。

上記スクリプトはinsertにしか対応してないので、2回実行すると落ちる。

ちゃんと作るなら既存データは無視する処理を入れたりして、

新しいデータが増えても新しいデータの分だけ取得するようにする。

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック

JS+Node.jsによるWebクローラー/ネットエージェント開発テクニック