はじめに
競馬新聞やデータを読んでも比較したいデータ以外の情報があったり、(赤)ペンで書込んでも何が何だかわからなくなることが多くて、ネット表示をマウスでひとつひとつコピペしてエクセルに貼り付けたりしてました。まあ、大変な思いをしてました。そんな折にHTMLを解析してデータ収集できることを知って、見たいデータだけ比較したいなと思いちょっとやってみました
どんな言語にもHTMLを解析するライブラリはあるようですが、アプリ系では C# をメインでやってたので HtmlAgilityPack を使ってます。開発環境の概略は以下の通りです
ともかく使ってみたいという方は、④競馬データをエクセルに貼り付けるアプリに進んでください
・OS :Windows 10
・IDE:Visual Studio 2022
・アプリ:Windowsフォームアプリケーション(.NET Framework 4.8)
・言語 :C#
・Html Agility Packライブラリ(ZZZ Projects,Simon Mourrier,Jeff Klawiter,Stephan Grell)
競馬データは、netkeiba.com の出走レースを例にすると、レース情報と馬情報のページから馬名をクリックすると、馬の個別情報の下にレース履歴が表示されます。今回は、レース情報、馬情報、レース履歴情報をHTMLから取得します
レース情報のHTMLデータ
上記レース情報のレース番号、レース名などは、HTML文章下線中の RaceNum、RaceName、RaceData01 に埋め込まれています。プログラム的には、RaceList_NameBox の RaceNum、RaceName、RaceData01 をキーワードにしてHTMLから抽出すればデータ取得できることになります
レース番号 :11R
レース名 :マイルCS
レースデータ:15:40発走、芝1600m、天候曇り、馬場良
レース情報の取得
ここでは下記クラスにデータを格納しています
/*************************/
/* レース情報クラス */
/*************************/
public class RaceInfo
{
public InfoData<string> RaceNum = new InfoData<string>( "" ); // レース番号
public InfoData<string> RaceName = new InfoData<string>( "" ); // レース名
public InfoData<string> StartTime = new InfoData<string>( "" ); // 発走時間
public InfoData<string> TurfDirt = new InfoData<string>( "" ); // 芝・ダート
public InfoData<int> Distance = new InfoData<int>( -1 ); // 距離
}
まず、Htmlの文字列取得を GetHtmlText() で取得し、RaceList_NameBox をキーワードにレース情報全体を取得して、その中のノード0からレース番号を取得します。Race_Num でも取得してますが、地方競馬データの名称が Race_Num なので両方できるようにはしてます(SelectNodesの使い方は省略)
以下の Info 変数は RaceInfo クラスです
string htmlText = GetHtmlText( Url );
if( htmlText == "" )
{
return false;
}
var htmlDoc = new HtmlAgilityPack.HtmlDocument();
htmlDoc.LoadHtml( htmlText );
// レース情報全体の取得
var nodes = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceList_NameBox\")]" );
if( nodes == null )
{
return false;
}
// レース番号取得
htmlDoc.LoadHtml( nodes[ 0 ].InnerHtml );
HtmlNodeCollection node = htmlDoc.DocumentNode.SelectNodes( "//span[starts-with(@class, \"RaceNum\")]" );
if( node != null )
{
Info.RaceNum.Data = GetInnerStr( node[ 0 ] );
}
else
{
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"Race_Num\")]" );
if( node != null )
{
Info.RaceNum.Data = GetInnerStr( node[ 0 ] );
}
}
上記プログラムに続き、レース名を取得します
// レース名取得
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceName\")]" );
if( node != null )
{
Info.RaceName.Data = GetInnerStr( node[ 0 ] );
}
続いてレースデータは「15:40発走 / 芝1600m (右 外) / 天候:曇 / 馬場:良」といった文字列なので「/」で分解してデータを切り出しています
// レースデータ取得
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceData01\")]" );
if( node != null )
{
string tempStr = GetInnerStr( node[ 0 ] ).Replace( "発走", "" ); // Split後にリプレースできなかったのでここで削除
string[] words = tempStr.Split( '/' );
Info.StartTime.Data = words[ 0 ]; // 発走時間
if( words[ 1 ].Contains( "芝" )) // 芝・ダート
{
Info.TurfDirt.Data = "芝";
}
if( words[ 1 ].Contains( "ダ" ))
{
Info.TurfDirt.Data = "ダ";
}
tempStr = Regex.Replace( words[ 1 ], @"[^0-9]", "" );// 数値切り出し
if( Int32.TryParse( tempStr, out int result ))
{
Info.Distance.Data = result;
}
}
まとめると、以下の関数となります
/*************************/
/* レース情報取得 */
/*************************/
// Info :馬情報へのセット
// Url :情報取得URL
// MessageEvent:プログレス通知ハンドラ
public static bool GetRaceInfo( RaceInfo Info, string Url, EventHandler MessageEvent )
{
try
{
/* ---< 引数チェック >--- */
if(( Info == null ) || ( Url == null ) || ( Url == "" ))
{
return false;
}
string htmlText = GetHtmlText( Url );
if( htmlText == "" )
{
return false;
}
var htmlDoc = new HtmlAgilityPack.HtmlDocument();
htmlDoc.LoadHtml( htmlText );
// レース情報全体の取得
var nodes = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceList_NameBox\")]" );
if( nodes == null )
{
return false;
}
// レース番号取得
htmlDoc.LoadHtml( nodes[ 0 ].InnerHtml );
HtmlNodeCollection node = htmlDoc.DocumentNode.SelectNodes( "//span[starts-with(@class, \"RaceNum\")]" );
if( node != null )
{
Info.RaceNum.Data = GetInnerStr( node[ 0 ] );
}
else
{
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"Race_Num\")]" );
if( node != null )
{
Info.RaceNum.Data = GetInnerStr( node[ 0 ] );
}
}
// レース名取得
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceName\")]" );
if( node != null )
{
Info.RaceName.Data = GetInnerStr( node[ 0 ] );
}
// レースデータ取得
node = htmlDoc.DocumentNode.SelectNodes( "//div[starts-with(@class, \"RaceData01\")]" );
if( node != null )
{
string tempStr = GetInnerStr( node[ 0 ] ).Replace( "発走", "" ); // Split後にリプレースできなかったのでここで削除
string[] words = tempStr.Split( '/' );
Info.StartTime.Data = words[ 0 ]; // 発走時間
if( words[ 1 ].Contains( "芝" )) // 芝・ダート
{
Info.TurfDirt.Data = "芝";
}
if( words[ 1 ].Contains( "ダ" ))
{
Info.TurfDirt.Data = "ダ";
}
tempStr = Regex.Replace( words[ 1 ], @"[^0-9]", "" ); // 数値切り出し
if( Int32.TryParse( tempStr, out int result ))
{
Info.Distance.Data = result;
}
}
return true;
}
finally
{
}
}
/*************************/
/* Htmlの文字列取得 */
/*************************/
public static string GetHtmlText( string Url )
{
try
{
string str = "";
WebRequest rq = WebRequest.Create( Url );
Encoding encoding = Encoding.GetEncoding( "EUC-JP" );
using( WebResponse res = rq.GetResponse() )
{
using( Stream stream = res.GetResponseStream() )
{
using( StreamReader sr = new StreamReader( stream, encoding ))
{
str = sr.ReadToEnd();
}
}
}
return str;
}
catch
{
return "";
}
}
/*************************/
/* ノード内テキスト取得 */
/*************************/
private static string GetInnerStr( HtmlNode Node )
{
if( Node == null )
{
return "";
}
string strData = Node.InnerText.ToString();
strData = strData.Replace( "\n", "" ); // 改行無視
strData = strData.Replace( " ", "" ); // スペース無視
return strData;
}