[GAS] netkeibaのレース結果ページをスクレイピングして情報を取得する方法 - 導入編

    GAS(Google Apps Script)を用いて、netkeibaのレース結果ページをスクレイピングし、情報を取得してみる。

    注意事項: 馬券の購入は自己責任でお願いいたします。また、免責事項も必ずお読みください。


    1. はじめに


    競馬の予想を行う際に、過去のレース結果に基づいた傾向を知りたいことがある。傾向を知るためには過去の大量のレース結果のデータが必要になるが、手動で取得するのには手間がかかる。スクレイピングを行うことで自動で大量の結果を取得することができる。

    今回は単純化のためこちらのページから1レース分だけ情報を取得することにする。
    対象のページは2021年の東京優駿(日本ダービー)。

    2. UrlFetchAppを用いてページの内容を取得


    早速、このページの内容を取得する。今回はUrlFetchAppを用いてHTML情報を取得する。取得した情報はとりあえずコンソールに出力してみる。
    コードは下記。
    function netkeibaGetResult() {
      const kUrl = "https://db.netkeiba.com/race/202105021211/"
      const kOptions = {method:"GET"}
      const kResponse = UrlFetchApp.fetch(kUrl ,kOptions)
      const kContent = kResponse.getContentText("EUC-JP")  
    
      console.log(kContent)
    }
    
    実行すると下記のようになる。中身が多すぎて全部は出力されないようだ。

    185006.png

    全て出力されていなくても、中身がkContentに全て格納されていれば特に問題はない。

    注意事項: 今回のケースではあてはまりませんが、同一サイトの複数のページ(URL)から情報を取得する際は、サーバーの負荷軽減などのために、必ず1秒以上間隔をあけるようにしましょう。

    3. 取得した内容から必要な情報を抽出


    次は取得した中身から必要な情報を抽出する。どのような情報を抽出するかは目的に応じて変わってくるが、今回は単勝の払戻金を抽出することにする。まずは、ページのソース(つまり2. で取得したkContent)を確認し、どこに払戻金の情報があるかを確認する。ページのソースはブラウザで簡単に表示できる(Chromeであれば右クリック > 「ページのソースを表示」)。
    「単勝」で検索すると、下の2件が引っかかる。

    185601.png

    185923.png

    このレースの単勝払戻金である「1,170」が2番目の単勝のすぐ近くにある。これを抽出することを考える。さまざまな方法が考えられると思うが、今回は地道に正規表現を使って処理していく。周囲のテキストは下記のような構成になっている。

    <tr>
    <th class="tan">単勝</th>
    <td>10</td>
    <td class="txt_r">1,170</td>
    <td class="txt_r">4</td>
    </tr>

    class="tan"はソースの中でここにしか出現しないので、これを手掛かりにし、適当なキーワードで「1,170」を囲むことを考える。</td>にすると前の「10」で切れてしまうので、</tr>にする。指定した文字列で囲まれた文字列を取得する方法は、こちらで紹介した正規表現を利用する手法を使う。コードは下記のようになる。
    // regにマッチする文字列をstrから探して最初のものを返す
    function getRegExpOnce(str, reg){
      const kRegex = new RegExp(reg, "gs")
      var results = str.match(kRegex)    
    
      if (results == null){
        return null //マッチしない場合はnullで返す
      }
    
      return results[0]
    }
    
    // preとpostに挟まれた文字列をstrから探して最初のものを返す
    function getRegExpMiddleOnce(str, pre, post){
      var r = getRegExpOnce(str, pre + ".+?" + post)
    
      if (r == null){
        return null
      }
    
      r = r.replace(pre, "")
      var result = r.replace(post, "")
      return result
    }
    
    function netkeibaGetResult() {
      const kUrl = "https://db.netkeiba.com/race/202105021211/"
      const kOptions = {method:"GET"}
      const kResponse = UrlFetchApp.fetch(kUrl ,kOptions)
      const kContent = kResponse.getContentText("EUC-JP")
    
      var str1 = getRegExpMiddleOnce(kContent, "class=\"tan\"", "</tr>")  
    
      console.log(str1)
    }
    
    出力を確認してみる。

    220053.png

    1170に近づいてきたが、まだ余計な情報が周りに存在している。
    ここからも地道に必要のない情報を取り除いていく。まずは、カンマはいらないので、取り除く。ここでもreplaceの引数に正規表現を使って、カンマが複数あっても取り除けるようにする。
    function netkeibaGetResult() {
      const kUrl = "https://db.netkeiba.com/race/202105021211/"
      const kOptions = {method:"GET"}
      const kResponse = UrlFetchApp.fetch(kUrl ,kOptions)
      const kContent = kResponse.getContentText("EUC-JP")
    
      var str1 = getRegExpMiddleOnce(kContent, "class=\"tan\"", "</tr>")  
      var str2 = str1.replace(/,/g, "")
      console.log(str2)
    }
    
    出力を確認する。

    221126.png

    カンマが取り除かれた。
    さて、ここで改めてどういう情報が残っているのかを確認する。2行目の「10」は馬番である。そして4行目の「4」は人気である。ここは構成上必ず、馬番、払戻金、人気の順番で情報が格納される。そして馬番はtdという属性なしのタグの値であり、払戻金と人気はclass属性が"txt_r"であるtdタグの値である。つまり、払戻金はclass属性が"txt_r"であるtdタグのうち、最初のものの値ということになる。属性付きのタグの値を取得することになるので、ここでもこちらで紹介した手法を利用できる。コードは下記のようになる。
    // 要素名(tag)、および属性(attr)とその値(value)を指定して、xmlから中身を取得(1つ)
    function getXmlContentOnceWithAttribute(xml, tag, attr, value){
      const kPre = "<" + tag + " " + attr + "=\"" + value + "\".*?>"
      const kPost = "</" + tag + ">"      
      const kStr = getRegExpOnce(xml, kPre + ".+?" + kPost)
      return getRegExpMiddleOnce(kStr, ">", kPost)  
    }
    
    function netkeibaGetResult() {
      const kUrl = "https://db.netkeiba.com/race/202105021211/"
      const kOptions = {method:"GET"}
      const kResponse = UrlFetchApp.fetch(kUrl ,kOptions)
      const kContent = kResponse.getContentText("EUC-JP")
    
      var str1 = getRegExpMiddleOnce(kContent, "class=\"tan\"", "</tr>")  
      var str2 = str1.replace(/,/g, "")
      const kTansyo = getXmlContentOnceWithAttribute(str2, "td", "class", "txt_r")
      console.log(kTansyo)
    }
    
    出力を確認する。

    223125.png

    無事に2021年の東京優駿の単勝払戻金である「1170」を取得できた。
    全てのコードを掲載する。
    // regにマッチする文字列をstrから探して最初のものを返す
    function getRegExpOnce(str, reg){
      const kRegex = new RegExp(reg, "gs")
      var results = str.match(kRegex)    
    
      if (results == null){
        return null //マッチしない場合はnullで返す
      }
    
      return results[0]
    }
    
    // preとpostに挟まれた文字列をstrから探して最初のものを返す
    function getRegExpMiddleOnce(str, pre, post){
      var r = getRegExpOnce(str, pre + ".+?" + post)
    
      if (r == null){
        return null
      }
    
      r = r.replace(pre, "")
      var result = r.replace(post, "")
      return result
    }
    
    // 要素名(tag)、および属性(attr)とその値(value)を指定して、xmlから中身を取得(1つ)
    function getXmlContentOnceWithAttribute(xml, tag, attr, value){
      const kPre = "<" + tag + " " + attr + "=\"" + value + "\".*?>"
      const kPost = "</" + tag + ">"      
      const kStr = getRegExpOnce(xml, kPre + ".+?" + kPost)
      return getRegExpMiddleOnce(kStr, ">", kPost)  
    }
    
    function netkeibaGetResult() {
      const kUrl = "https://db.netkeiba.com/race/202105021211/"
      const kOptions = {method:"GET"}
      const kResponse = UrlFetchApp.fetch(kUrl ,kOptions)
      const kContent = kResponse.getContentText("EUC-JP")
    
      var str1 = getRegExpMiddleOnce(kContent, "class=\"tan\"", "</tr>")  
      var str2 = str1.replace(/,/g, "")
      const kTansyo = getXmlContentOnceWithAttribute(str2, "td", "class", "txt_r")
      console.log(kTansyo)
    }
    

    [関連情報]

    スポンサーサイト



    コメント

    非公開コメント

    プロフィール

    IT Fragame

    Author:IT Fragame
    よく使う言語はPython、C++、Javascriptなど。IT関連企業勤務。首都圏在住。
    Twitter

    更新通知登録ボタン

    更新通知で新しい記事をいち早くお届けします

    免責事項

    当ブログに掲載された内容によって生じた損害などについて、一切の責任を負いません。 当ブログのコンテンツおよび情報については、正確性や安全性を保証するものではありません。 当ブログからのリンクなどから移動したサイトで提供される情報、サービスなどについて、一切の責任を負いません。

    最新コメント

    月別アーカイブ

    検索フォーム