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

# はじめに
あっというまに夏休みが終わるね(-.-)
なんにもやってない夏休みはつまらないので、Webからデータを作るスクレイピング入門みたいなのやります。
[日経競馬](https://www.netkeiba.com/)からデータを持ってきて、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

```Javascript
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

```Shell
npm install iconv --save
```
node-gypのrebuildで失敗したら、[**ここ**](https://qiita.com/pooshikin/items/b82f0f91b292c5c956cd)を参照。

### 実行

```Shell
node jockey_leading_get.js
```

### 結果

```Shell
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

```JavaScript
/**
 *  @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化をもくろんで。
* ヘッダー２行をスキップしてもいいかも、後続の「jockey_leading_load.js」ではヘッダー2行をスキップしてテーブルにロード。
* http.responseTextを'\n'でsplitして1行ずつ処理します。
* \<table ～\</table>を処理
* \<td nowrap>～\</tr><tr>を処理
* jockey_leading.htmlのテーブルを解析して、各要素をbody_htmltocsv関数で取出し配列(csv[])に格納。
* 出力ファイル「jockey_leading.csv」
* 出力する前に「jockey_leading.csv」は削除、ファイル有無は判定しない。無くても続行。
* cvsの書き出しは同期処理にしなければ、並びがバラバラになるので、必ずappendFileSyncで書出します。

### xmlhttprequest install

```Shell
npm install xmlhttprequest -save
```

### 実行

```Shell
node jockey_leading_tocsv.js
```

### 結果

```Shell
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

```JavaScript
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」を開く。
* イミディトウィンドウに以下のコマンド

```Shell
?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

```Shell
npm install node-adodb --save
```

### 実行

```Shell
node jockey_leading_table.js
```

### 結果

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

![2019-08-16-15-29-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239597/2913df70-6b87-9e14-b273-e5ec58072262.png)

## jockey_leading_load.js

```JavaScript
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したデータをロードします。

### 実行

```Shell
node jockey_leading_load.js
```

### 結果

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

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

#### jockey

![2019-08-16-15-48-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239597/ba33c07a-c97e-399c-eb88-d41503d7bfe7.png)

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