Edited at

日経競馬をスクレイピングして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です。


    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


    まとめ

    スクレイピングをnode.jsとJavaScriptで構築する入門をやってみました。

    いわずもがな、初学者向けです。不明点が多々あると思います。

    俺だったこうやる的なご意見もありそうです。

    とりあえず、目的は達成しています。

    AccessをMySQLに置き換えるとか、コードはALLフリーなので色々改造して、さらなるスクレイピングワールドへ