レース履歴情報の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;
		}
	}