レース履歴情報のHTMLデータ
馬情報の欄から馬名をクリックすると、馬の個別情報とレース履歴情報のページが表示されます。プログラム的には、先に取得した馬情報の中に「馬名から取得したリンク先の文字列」を利用してページを遷移させます(後述)
このページのHTML文章中の db_main_deta にレース履歴情報が埋め込まれています。各レースの履歴データは table 要素の tr に埋め込まれていて、さらにレースの日付や競技場などの個別要素は td に埋め込まれています
ここでは、今までのように class や id での設定がなく単なる羅列になりますので、td を順番に追ってデータを取得することになります
今までにも中央競馬と地方競馬のクラス名が違ったり、履歴は羅列だったりしますのでHTMLでの定義や設定を変えられたら、このアプリは使い物にはなりません。利用させていただいている側ですので、文句は言えません。変わってしまったらアプリも変えることになりますね
レース履歴情報の取得
ここでは下記クラスにデータを格納しています
/*************************/
/* 履歴情報クラス */
/*************************/
public class HistoryInfo
{
public InfoData<string> H_Date = new InfoData<string>( "" ); // 日付
public InfoData<string> H_Track = new InfoData<string>( "" ); // 場所
public InfoData<string> H_Weather = new InfoData<string>( "" ); // 天気
public InfoData<int> H_RaceNum = new InfoData<int>( -1 ); // レース番号
public InfoData<string> H_RaceName = new InfoData<string>( "" ); // レース名
public InfoData<int> H_Starters = new InfoData<int>( -1 ); // 頭数
public InfoData<int> H_WakuNum = new InfoData<int>( -1 ); // 枠番
public InfoData<int> H_HorseNum = new InfoData<int>( -1 ); // 馬番
public InfoData<double> H_Odds = new InfoData<double>(); // オッズ
public InfoData<int> H_Favorite = new InfoData<int>( -1 ); // 人気
public InfoData<int> H_Result = new InfoData<int>( -1 ); // 着順
public InfoData<string> H_Jockey = new InfoData<string>( "" ); // 騎手
public InfoData<string> H_JockeyLink = new InfoData<string>( "" ); // 騎手リンク
public InfoData<double> H_Weight = new InfoData<double>(); // 斤量
public InfoData<string> H_TurfDirt = new InfoData<string>(); // 芝・ダート
public InfoData<int> H_Distance = new InfoData<int>( -1 ); // 距離
public InfoData<string> H_Condition = new InfoData<string>( "" ); // 馬場(重稍など)
public InfoData<string> H_Finish = new InfoData<string>( "" ); // タイム
public InfoData<double> H_Margin = new InfoData<double>(); // 着差
public InfoData<string> H_Position = new InfoData<string>( "" ); // 通過
public InfoData<double> H_Pace_1 = new InfoData<double>(); // ペース_1
public InfoData<double> H_Pace_2 = new InfoData<double>(); // ペース_2
public InfoData<double> H_Pace_3F = new InfoData<double>(); // 上り_3F
public InfoData<double> H_HorseWeight = new InfoData<double>(); // 馬体重
public InfoData<double> H_HorseWeightDiff = new InfoData<double>(); // 馬体重差
}
以下の Info は List<HorseInfo> クラスのデータです。先に取得した馬情報の中に「馬名から取得したリンク先の文字列」は、URL なので GetHtmlText() 関数でこのページの HTML を取得できます
HTML文章の中の db_main_deta を抜き出して、table 要素を探し tbody をループで探してますが、1回 table を探して、1回 tbody を探したら、それ以上ループはしません
tr の検索は過去のレース回数分ループし、そのノードが race 変数に格納されます。一頭分のレース履歴が取れたら、GetHistoryInfo( Info[ index ], race ) でレース履歴を処理します
for( int index = 0; index < Info.Count; index++ )
{
if(( Info[ index ].HorseLink == null ) || ( Info[ index ].HorseLink.Data == "" ))
{
continue;
}
Info[ index ].History.Clear(); // 履歴クリア
string htmlText=GetHtmlText(Info[ index ].HorseLink.Data); // リンク無しはスルー
if( htmlText == "" )
{
continue;
}
var htmlDoc = new HtmlAgilityPack.HtmlDocument();
htmlDoc.LoadHtml( htmlText );
var nodes = htmlDoc.DocumentNode.SelectNodes( "//div[@class=\"db_main_deta\"]" );
for( int index_1 = 0; index_1 < nodes[ 1 ].ChildNodes.Count; index_1++ )
{
var node_1 = nodes[ 1 ].ChildNodes[ index_1 ];
if( node_1.Name != "table" ) // レース履歴テーブル先頭
{
continue;
}
List<HtmlNode> race = new List<HtmlNode>();
for( int index_2 = 0; index_2 < node_1.ChildNodes.Count; index_2++ )
{
var node_2 = node_1.ChildNodes[ index_2 ];
if( node_2.Name != "tbody" ) // レース履歴テーブルの履歴情報先頭
{
continue;
}
for( int index_3 = 0; index_3 < node_2.ChildNodes.Count; index_3++ )
{
var node_3 = node_2.ChildNodes[ index_3 ];
if( node_3.Name != "tr" ) // レース履歴テーブルの履歴情報本体
{
continue;
}
race.Add( node_3 );
}
break;
}
GetHistoryInfo( Info[ index ], race );
break;
}
}
次の関数は、過去のレース毎にレース履歴を処理します
// 指定馬の馬情報オブジェクトに全履歴情報をセットする
// Info:指定馬の馬情報
// Race:指定馬の全履歴情報
private static void GetHistoryInfo( HorseInfo Info, List<HtmlNode> Race )
{
/* ---< 引数チェック >--- */
if(( Info == null ) || ( Race == null ) || ( Race.Count == 0 ))
{
return;
}
foreach( HtmlNode race in Race )
{
GetHistoryInfo( Info, race );
}
}
次の関数は、一つのレース履歴を処理します
td 要素にクラス名などの定義はなく羅列されているだけなので、for 文でひたすら td を見つけ、その td に対して処理します。td の処理は tdCount でカウントアップして、どのデータかを管理します
intEnabled と dblEnabled は、とりあえず取得した文字列全部を整数 or ダブルに変換してます。使わないものまで変換するので処理時間の無駄にはなりますが、switch 文の中がすっきりします
removeNum は、開催地データが ”5阪神6″ になっていて数値を削除したいので、削除文字として使用します。削除は Utility.RemoveChar() で実行します(ちなみに文字列は、阪神競馬5日目の6レースをさします)
removeTime は、この文字を削除して 空白(””) であれば時間データとして判定しています。ひたすら順番に処理するだけなので、判定を入れるのは不要といえば不要です。removePos も同様の扱いです
不要なデータや取得できないデータは、break するだけです
// 指定馬の馬情報オブジェクトに1つの履歴情報をセットする
// Info:指定馬の馬情報
// Race:指定馬の1つの履歴情報
private static void GetHistoryInfo( HorseInfo Info, HtmlNode Race )
{
/* ---< 引数チェック >--- */
if(( Info == null ) || ( Race == null ))
{
return;
}
HistoryInfo data = new HistoryInfo();
int tdCount = 0;
for( int index = 0; index < Race.ChildNodes.Count; index++ )
{
var node = Race.ChildNodes[ index ];
if( node.Name != "td" ) // レース履歴テーブルの履歴情報本体
{
continue;
}
string strData = GetInnerStr( node ); // ノード内の文字列取得
string[] words;
if(( strData == null ) || ( strData == "" ))
{
tdCount++;
continue;
}
bool intEnabled = Int32.TryParse( strData, out int intData );
bool dblEnabled = Double.TryParse( strData, out double dblData );
int tempInt;
string tempStr;
char[] removeNum = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
char[] removeTime = new char[] { ' ', ':', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
char[] removePos = new char[] { ' ', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
// Html内の格納順
switch( tdCount )
{
case 0: // 日付
data.H_Date.Data = strData;
break;
case 1: // 開催
strData=Utility.RemoveChar(strData,removeNum);// 数値削除
if( strData != "" )
{
data.H_Track.Data = strData;
}
break;
case 2: // 天気
data.H_Weather.Data = strData;
break;
case 3: // レース番号
if( intEnabled ){ data.H_RaceNum.Data = intData; }
break;
case 4: // レース名
data.H_RaceName.Data = strData;
break;
case 5: // 映像(なし)
break;
case 6: // 頭数
if( intEnabled ){ data.H_Starters.Data = intData; }
break;
case 7: // 枠番
if( intEnabled ){ data.H_WakuNum.Data = intData; }
break;
case 8: // 馬番
if( intEnabled ){ data.H_HorseNum.Data = intData; }
break;
case 9: // オッズ
if( dblEnabled ){ data.H_Odds.Data = dblData; }
break;
case 10: // 人気
if( intEnabled ){ data.H_Favorite.Data = intData; }
break;
case 11: // 着順
if( intEnabled ){ data.H_Result.Data = intData; }
break;
case 12: // 騎手
data.H_Jockey.Data = strData;
foreach(HtmlNode child in node.ChildNodes) // 騎手リンク
{
strData = child.GetAttributeValue( "href", null );
if(( strData != null ) && ( strData != "" ))
{
data.H_JockeyLink.Data = strData;
break;
}
}
break;
case 13: // 斤量
if( dblEnabled ){ data.H_Weight.Data = dblData; }
break;
case 14: // 距離
tempStr=Regex.Replace(strData, @"[^0-9]", "");// 数値切り出し
data.H_TurfDirt.Data = strData.Replace( tempStr, "" ); // 芝・ダート
if(Int32.TryParse(tempStr,out tempInt)) // 距離
{
data.H_Distance.Data = tempInt;
}
break;
case 15: // 馬場(重稍など)
data.H_Condition.Data = strData;
break;
case 16: // 馬場指数(なし)
break;
case 17: // タイム
if( ""==Utility.RemoveChar(strData,removeTime))// タイムデータに使用される文字を削除して、なしならタイムデータ
{
data.H_Finish.Data = strData;
}
break;
case 18: // 着差
if( dblEnabled ){ data.H_Margin.Data = dblData; }
break;
case 19: // タイム指数(なし)
break;
case 20: // 通過
if(""==Utility.RemoveChar(strData,removePos))// 通過データに使用される文字を削除して、なしなら通過データ
{
strData = strData.Replace( "-", "_" ); // エクセルで日付に変更される事への対応
data.H_Position.Data = strData; // ('を先頭につけるのは、エクセルの最初の表示で'が表示されてしまう)
}
break;
case 21: // ペース
words = strData.Split('-');
if( words?.Length == 2 )
{
if( Double.TryParse( words[ 0 ], out dblData ) )
{
data.H_Pace_1.Data = dblData;
}
if( Double.TryParse( words[ 1 ], out dblData ) )
{
data.H_Pace_2.Data = dblData;
}
}
break;
case 22: // 上り_3F
if( dblEnabled ){ data.H_Pace_3F.Data = dblData; }
break;
case 23:
if( GetUmaWeight( strData, out double weight, out double weightDiff ))
{
data.H_HorseWeight.Data = weight;
data.H_HorseWeightDiff.Data = weightDiff;
}
break;
case 24: // 厩舎コメント
case 25: // 備考
case 26: // 勝ち馬(2着馬)
case 27: // 賞金
break;
}
tdCount++;
}
Info.History.Add( data );
}
/*************************/
/* 馬体重取得 */
/*************************/
// Str:"450(+2)" の形式を 450 と 2 として取得する
private static bool GetUmaWeight( string Str, out double Weight, out double WeightDiff )
{
Weight = 0;
WeightDiff = 0;
string[] words;
words = Str.Split('(');
if( words?.Length != 2 )
{
return false;
}
words[ 1 ] = words[ 1 ].Replace( ")", "" );
if( !Double.TryParse( words[ 0 ], out Weight ))
{
return false;
}
if( !Double.TryParse( words[ 1 ], out WeightDiff ))
{
return false;
}
return true;
}
/*************************/
/* ノード内テキスト取得 */
/*************************/
private static string GetInnerStr( HtmlNode Node )
{
if( Node == null )
{
return "";
}
string strData = Node.InnerText.ToString();
strData = strData.Replace( "\n", "" ); // 改行無視
strData = strData.Replace( " ", "" ); // スペース無視
return strData;
}
public static class Utility
{
public static string RemoveChar( string Src, char[] RemoveChars )
{
foreach( char c in RemoveChars )
{
Src = Src.Replace( c.ToString(), "" );
}
return Src;
}
}