Help us understand the problem. What is going on with this article?

日経競馬をスクレイピングしてDBを構築しよう

はじめに

あっというまに夏休みが終わるね(-.-)
なんにもやってない夏休みはつまらないので、Webからデータを作るスクレイピング入門みたいなのやります。
日経競馬からデータを持ってきて、Microsoft Accessへデータをロードするまでの簡単なサンプルを説明します。スクレイピングやったことない人のために。ガンガン応用してください。
盛夏

前提

  • OS : Windows7以上
  • PoweShellのターミナルで実行
  • VSCodeでコード編集
  • node.js環境構築済み
  • Microsoft Accessは2010以上

アプリ構成

アプリ 機能
1 jockey_leading_get.js リーディング騎手データのスクレイピング
2 jockey_leading_tocsv.js リーディング騎手データのCSVファイル変換
3 jockey_leading_table.js リーディング騎手データのAccessテーブル作成
4 jockey_leading_load.js リーディング騎手データのcsvのテーブルへロード

jockey_leading_get.js

var request = require('request');
var Iconv = require('iconv').Iconv;
var fs = require('fs');
const file = ".\\jockey_leading.html";

var options = {
    url: 'https://db.netkeiba.com/?pid=jockey_leading',
    encoding: null,
    method: 'GET'
}

/**
 *  AppendFile
 * @param {String} path 
 * @param {String} data 
 */
function AppendFile(path, data) {
    fs.appendFile(path, data, function (err) {
      if (err) {
          throw err;
      }
    })
}

request(options, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        var iconv = new Iconv('EUC-JP', 'UTF-8//TRANSLIT//IGNORE');
        body = iconv.convert(body).toString();
        AppendFile(file,body);
    }
})

説明

  • リーディング騎手データを「jockey_leading.html」として保存。
  • 日経競馬のページがEUC-JPのため、npm iconvをインストールしてUTF-8にコード変換。

iconv install

npm install iconv --save

node-gypのrebuildで失敗したら、ここを参照。

実行

node jockey_leading_get.js

結果

PS C:\Users\~> ls jockey_leading.html


    ディレクトリ: C:\Users\~


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2019/08/15     15:22      97440 jockey_leading.html

jockey_leading_tocsv.js

/**
 *  @param {String} html
 */
function htmltocsv(html){
    const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
    const http = new XMLHttpRequest();
    const file = ".\\jockey_leading.csv";
    UnlinkFile(file);
    http.open("GET", html, true);
    http.responseType = "document";
    http.send(null);
    var csv = [];
    http.onreadystatechange = function() {
      if(http.readyState == 4 && http.status==200) {
            var data = http.responseText;
            var lines = data.split( '\n' );
            var sw = 0;
            var body = 0;
            var output = '';
            // header
            const head1 = '順位,騎手名,所属,生年月日,1着,2着,3着,着外,重賞,,特別,,平場,,芝,,ダート,,勝率,連対率,複勝率,収得賞金(万円),代表馬,順位変動' + '\n';
            const head2 = ',,,,,,,,出走,勝利,出走,勝利,出走,勝利,出走,勝利,出走,勝利,,,,,' + '\n';
            csv.push(head1);
            csv.push(head2);
            for ( var i = 0; i < lines.length; i++ ) {
                // 空行は無視する
                if ( lines[i] == '' ) {
                    continue;
                }
                // <table 抽出
                if ( sw == 0 & lines[i].substr(0,6) == '<table' ) { sw = -1; }
                if ( sw == -1 ) {
                    // body start keyword
                    if (lines[i].substr(0,11) == '<td nowrap>') { body = -1; }
                    if ( body == -1 ) {
                        if ( lines[i] == '</tr><tr>' ) {
                            csv.push(output.substr(0, output.length - 1) + '\n');
                            var output = '';
                        } else {
                            output = output + '"' + body_htmltocsv(lines[i]) + '",';
                        }
                    }
                }
                if ( sw == -1 & lines[i].substr(0,8) == '</table>' ) { 
                    csv.push(output.substr(0, output.length - 20) + '\n');
                    sw = 0;
                }
            }
       }
       for ( var i = 0; i < csv.length; i++) { AppendFileSync(file, csv[i]); }
    }
}

/**
 *  AppendFileSync
 * @param {Sting} path 
 * @param {Sting} data 
 */
function AppendFileSync(path, data) {
    const fs = require("fs");
    fs.appendFileSync(path, data, function (err) {
      if (err) {
          throw err;
      }
    })
}

/**
 * @param {String} path 
 */
function UnlinkFile(path) {
    const fs = require("fs");
    fs.unlink(path, function(err) {
        if (err) {
            console.log(err);
        }
    })
}

/**
 * 
 * @param {String} data 
 * 
 */
function body_htmltocsv(data) {

    var pos = data.indexOf("</a>");
    // </a>がない
    if ( pos == -1 ) {
        var data = data.replace('<td nowrap>', '');
        var data = data.replace('</td>', '');
    } else {
        var pos = data.indexOf('<td nowrap>');
        // 先頭が<td nowrap>
        if ( pos == 0 ) {
            var pos = data.indexOf('>',12);
            var pos2 = data.indexOf('<',13);
            var data = data.substr( pos + 1, pos2 - pos -1 );
        } else {
            var pos = data.indexOf('<td class="txt_l" nowrap>');
            // 先頭が<td class="txt_l" nowrap>
            if ( pos == 0 ) {
                var pos = data.indexOf('[東]');
                var pos2 = data.indexOf('[西]');
                // [東][西]を含む
                if ( pos != -1 || pos2 != -1 ) {
                    var pos = data.indexOf('>',29);
                    var pos2 = data.indexOf('<',30);
                    if ( pos != -1 ) {
                        var data = '[東]' + data.substr( pos + 1, pos2 - pos -1 );
                    } else {
                        var data = '[西]' + data.substr( pos + 1, pos2 - pos -1 );
                    }
                } else {
                    var pos = data.indexOf('>',25);
                    var pos2 = data.indexOf('<',26);
                    var data = data.substr( pos + 1, pos2 - pos -1 );
                }
            }
        }
    }
    return data;
}

htmltocsv('file:' + '.\\jockey_leading.html');

説明

  • npm xmlhttprequestをインストール。直接htmlをfsで読み込んでもbody_htmltocsv関数は動作可能ですが、あえて将来httpからダイレクトにcsv化をもくろんで。
  • ヘッダー2行をスキップしてもいいかも、後続の「jockey_leading_load.js」ではヘッダー2行をスキップしてテーブルにロード。
  • http.responseTextを'\n'でsplitして1行ずつ処理します。
  • <table ~</table>を処理
  • <td nowrap>~</tr>を処理
  • jockey_leading.htmlのテーブルを解析して、各要素をbody_htmltocsv関数で取出し配列(csv[])に格納。
  • 出力ファイル「jockey_leading.csv」
  • 出力する前に「jockey_leading.csv」は削除、ファイル有無は判定しない。無くても続行。
  • cvsの書き出しは同期処理にしなければ、並びがバラバラになるので、必ずappendFileSyncで書出します。
  • xmlhttprequest install

    npm install xmlhttprequest -save
    

    実行

    node jockey_leading_tocsv.js
    

    結果

    PS C:\Users\~> ls jockey_leading.csv
    
    
        ディレクトリ: C:\Users\~
    
    
    Mode                LastWriteTime     Length Name
    ----                -------------     ------ ----
    -a---        2019/08/15     15:23       9331 jockey_leading.csv
    

    jockey_leading_table.js

    const check_jockey = "SELECT Name FROM MSysObjects WHERE Name = 'jockey';"
    const create_table_jockey = "CREATE TABLE jockey (" +
    "順位 INT NOT NULL," +
    "騎手名 char(20) NOT NULL," + 
    "所属 char(26)," +
    "生年月日 DATE," +
    "1着 INT," +
    "2着 INT," +
    "3着 INT," +
    "着外 INT," +
    "重賞出走 INT," +
    "重賞勝利 INT," +
    "特別出走 INT," +
    "特別勝利 INT," +
    "平場出走 INT," +
    "平場勝利 INT," +
    "芝出走 INT," +
    "芝勝利 INT," +
    "ダート出走 INT," +
    "ダート勝利 INT," +
    "勝率 DOUBLE," +
    "連対率 DOUBLE," +
    "複勝率 DOUBLE," +
    "収得賞金 DOUBLE," +
    "代表馬 char(20)," +
    "順位変動 char(4)" +
    ");"
    const create_index_jockey = "CREATE INDEX idxID ON jockey (順位 ASC);"
    const ADODB = require('node-adodb');
    const connection = ADODB.open('Provider=Microsoft.ACE.OLEDB.12.0;Data Source=jra2019.accdb;');
    
    // Table Exist Check
    connection
      .query(check_jockey)
      .then(data => {
        // Create Table
        if (data.length == 0) {
          connection
          .execute(create_table_jockey)
          .then(data => {
            // Create Index
            connection
            .execute(create_index_jockey)
            .then(data => {
            })
            .catch(error => {
              console.log(error);
            });
          })
          .catch(error => {
            console.log(error);
          });
        }
      })
      .catch(error => {
        console.log(error);
      });
    

    説明

    • ADODBでDB操作するので、node-adodbをインストールします。
    • Microsoft Access のテーブル格納は必ずしもロード順にならないので、インデックスを設定します。
    • AccessのMSysObjectsを読取、「jockey」テーブルの存在チェックを行います。
    • 実行するまえに、「jra2019.accdb」を作成します。

    MSysObjectsへ読みとり権限を付与する

    • Microsoft Accessで「jra2019.accdb」を作成。
    • 「jra2019.accdb」を開く。
    • Ctl+gで「Microsoft Visual Basic for Application」を開く。
    • イミディトウィンドウに以下のコマンド
    ?currentuser()
    Admin
    CurrentProject.Connection.Execute "GRANT SELECT ON MSysObjects TO Admin;"
    
    • currentuserでaccdbのユーザを確認して、MSsysObjectsにアクセス権限を付与します。
    • これを行わないと、外部からMSsysObjectsを読み取れません。
    • Microsoft Accessが旧mdbの場合 "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=xxxxx.mdb;"

    node-adodb install

    npm install node-adodb --save
    

    実行

    node jockey_leading_table.js
    

    結果

    下記の通り、テーブルが作成されればOKです。

    2019-08-16-15-29-00.png

    jockey_leading_load.js

    async function loadcsv(table, data) {
        const ADODB = require('node-adodb');
        const connection = ADODB.open('Provider=Microsoft.ACE.OLEDB.12.0;Data Source=jra2019.accdb;');
        try {
            await connection.execute('INSERT INTO ' + table + ' VALUES(' + data + ');');
        } catch (error) {
            console.error(error);
        }
    }
    
    function jockeyload() {
        const fs = require('fs');
        const file = ".\\jockey_leading.csv";
        var data = new String();
        data = fs.readFileSync(file, 'utf8');
        var lines = data.split( '\n' );
        for ( var i = 0; i < lines.length - 1; i++ ) {
            if (i < 2) {
                continue;
            }
            loadcsv('jockey', lines[i]);
        }
    }
    
    jockeyload();
    

    説明

    • node-adodbでCSVをテーブルにインサートします。
    • loadcsvは同期処理(async~await)にします。これをやらないと、csvがバラバラに格納されます。
    • jockey_leading.csvを同期(readFileSync)で読取、'\n'でsplitします。
    • splitしたデータをロードします。

    実行

    node jockey_leading_load.js
    

    結果

    下記のエラーコードは無視してください。

    process:
       { code: -2147467259,
         message: 'マシン \'xxxxxxx\' のユーザー \'Admin\' がデータベースを開けない状態、またはロックできない状態にしています。' },
      exitCode: 0 }
    

    jockey

    2019-08-16-15-48-00.png

    まとめ

    スクレイピングをnode.jsとJavaScriptで構築する入門をやってみました。
    いわずもがな、初学者向けです。不明点が多々あると思います。
    俺だったこうやる的なご意見もありそうです。
    とりあえず、目的は達成しています。
    AccessをMySQLに置き換えるとか、コードはALLフリーなので色々改造して、さらなるスクレイピングワールドへ

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした