Game Report Periods to JSON Script - notnotjeff/ahl_scraper GitHub Wiki

Sometimes games have missing JSON penalty and/or scoring data (see 1001050) and you will need to create a manual JSON game file from the Game Report (see 1001050). Place the custom JSON file in the in the fixed_games folder and add its id to the BROKEN_GAMES array in AhlScraper::GameDataFetcher to override the call with your custom fixed JSON.

If you fix a game make sure to create a pull request to add the game to the gem to help others get accurate data.

Code

In order to easily create JSON data for a games scoring and penalties you can use the following Ruby script to generate period JSON data from the Game Report

# frozen_string_literal: true

# RUBY VERSION: 2.7.3

gem "pry"
gem "json"
gem "nokogiri"

require 'pry'
require "json"
require "nokogiri"
require "open-uri"

# CHOOSE GAME ID
game_id = 1020527

# GET JSON FOR ROSTER AND TEAM DATA
GAME_JSON = JSON.parse(Nokogiri::HTML(URI.parse("http://lscluster.hockeytech.com/feed/index.php?feed=statviewfeed&view=gameSummary&game_id=#{game_id}&key=50c2cd9b5e18e390&site_id=1&client_code=ahl&lang=en&league_id=&callback=json").open).text[5..-2], symbolize_names: true)

# UNCOMMENT THE FOLLOWING LINE IF YOU WANT TO CREATE A FILE TO LOOK THROUGH
# File.write("#{game_id}_original.json", GAME_JSON.to_json)

TEAM_HASH = {
  "H" => GAME_JSON[:homeTeam][:info],
  "V" => GAME_JSON[:visitingTeam][:info],
  GAME_JSON[:homeTeam][:info][:abbreviation] => "H",
  GAME_JSON[:visitingTeam][:info][:abbreviation] => "V"
}.freeze

ROSTERS = {
  "H" => [*GAME_JSON[:homeTeam][:skaters].map { |player| player[:info] }, *GAME_JSON[:homeTeam][:goalies].map { |player| player[:info] }],
  "V" => [*GAME_JSON[:visitingTeam][:skaters].map { |player| player[:info] }, *GAME_JSON[:visitingTeam][:goalies].map { |player| player[:info] }]
}.freeze

PERIOD_INFO = {
  "1st" => { id: "1", shortName: "", longName: "1st" },
  "2nd" => { id: "2", shortName: "", longName: "2nd" },
  "3rd" => { id: "3", shortName: "", longName: "3rd" },
  "OT" => { id: "4", shortName: "OT", longName: "OT" }
}.freeze

data = {
  periods: [
    { info: PERIOD_INFO["1st"], stats: { homeGoals: "0", homeShots: "0", visitingGoals: "0", visitingShots: "0" }, goals: [], penalties: [] },
    { info: PERIOD_INFO["2nd"], stats: { homeGoals: "0", homeShots: "0", visitingGoals: "0", visitingShots: "0" }, goals: [], penalties: [] },
    { info: PERIOD_INFO["3rd"], stats: { homeGoals: "0", homeShots: "0", visitingGoals: "0", visitingShots: "0" }, goals: [], penalties: [] }
  ]
}

document = Nokogiri::HTML(URI.parse("https://lscluster.hockeytech.com/game_reports/official-game-report.php?client_code=ahl&game_id=#{game_id}&lang_id=1").open)

penalty_rows = document.css("table")[13].css("tr")[2..-1].map { |pen| pen.css("td").map(&:text) }
scoring_rows = document.css("table")[7].css("tr")[1..-1].map { |pen| pen.css("td").map(&:text) }
visiting_shots, home_shots = document.css("table")[5].css("tr")[1..-1].map { |pen| pen.css("td").map(&:text) }.map { |team| team[1..-2] }

# ADD OVERTIME PERIOD IF IT EXISTS
overtime = penalty_rows.filter { |pen| pen[0] == "OT" }.any? || scoring_rows.filter { |goal| goal[2] == "OT" }.any? ? true : false
data[:periods] << { info: PERIOD_INFO["OT"], stats: { homeGoals: "0", homeShots: "0", visitingGoals: "0", visitingShots: "0" }, goals: [], penalties: [] } if overtime

# SHOTS
visiting_shots.each.with_index do |shots, i|
  data[:periods][i][:stats][:visitingShots] = shots.to_i.to_s
end
home_shots.each.with_index do |shots, i|
  data[:periods][i][:stats][:homeShots] = shots.to_i.to_s
end

# PENALTIES
penalty_rows.map do |row|
  served_first, served_last = row[2].scan(/Served by(.*)/)&.dig(0, 0)&.strip&.split(" ")
  first, last = served_last ? row[2].scan(/(.*)Served by/)&.dig(0, 0)&.strip&.split(" ") : row[2].split(" ")
  taken_player_data = last ? ROSTERS[row[1]].find { |player| player[:firstName][0] == first[0] && player[:lastName] == last } : nil
  served_player_data = served_last ? ROSTERS[row[1]].find { |player| player[:firstName][0] == served_first[0] && player[:lastName] == served_last } : nil

  penalty = {
    game_penalty_id: nil,
    period: PERIOD_INFO[row[0]],
    time: row[5],
    againstTeam: TEAM_HASH[row[1]],
    minutes: row[3].split(".")[0].to_i,
    description: row[4].strip,
    ruleNumber: "",
    takenBy: taken_player_data,
    servedBy: served_player_data || taken_player_data,
    isPowerPlay: true,
    isBench: false
  }

  data[:periods][PERIOD_INFO[row[0]][:id].to_i - 1][:penalties] << penalty
end

# SCORING
scoring_rows.map do |row|
  scoring_team = TEAM_HASH[row[3]] # H or V
  against_team = scoring_team == "H" ? "V" : "H"

  scorer_first, scorer_last, goal_number = row[5].split(" ")
  goal_number = /(?<=\().*?(?=\))/.match(goal_number)[0]
  
  a1_full_name, a2_full_name = row[6].split(", ")
  a1_first, a1_last = a1_full_name&.split(" ")
  a2_first, a2_last = a2_full_name&.split(" ")

  scorer_data = ROSTERS[scoring_team].find { |player| player[:firstName][0] == scorer_first[0] && player[:lastName] == scorer_last }
  a1_data = a1_last.nil? ? nil : ROSTERS[scoring_team].find { |player| player[:firstName][0] == a1_first[0] && player[:lastName] == a1_last }
  a2_data = a2_last.nil? ? nil : ROSTERS[scoring_team].find { |player| player[:firstName][0] == a2_first[0] && player[:lastName] == a2_last }

  scoring_team_player_numbers = scoring_team == "H" ? row[11].scan(/\d{1,2}/).map(&:to_i) : row[9].scan(/\d{1,2}/).map(&:to_i)
  against_team_player_numbers = scoring_team == "H" ? row[9].scan(/\d{1,2}/).map(&:to_i) : row[11].scan(/\d{1,2}/).map(&:to_i)
  plus_players = ROSTERS[scoring_team].filter { |player| scoring_team_player_numbers.include?(player[:jerseyNumber]) }.sort { |a, b| a[:jerseyNumber] <=> b[:jerseyNumber] }
  minus_players = ROSTERS[against_team].filter { |player| against_team_player_numbers.include?(player[:jerseyNumber]) }.sort { |a, b| a[:jerseyNumber] <=> b[:jerseyNumber] }

  goal = {
    game_goal_id: nil,
    team: TEAM_HASH[scoring_team],
    period: PERIOD_INFO[row[2]],
    time: row[4],
    scorerGoalNumber: goal_number,
    scoredBy: scorer_data,
    assists: [a1_data, a2_data].filter { |assist| !assist.nil? },
    assistNumbers: [nil, nil],
    properties: {
      isPowerPlay: row[7] == "PP" ? "1" : "0",
      isShortHanded: row[7] == "SH" ? "1" : "0",
      isEmptyNet: row[7] == "EN" ? "1" : "0",
      isPenaltyShot: row[7] == "PS" ? "1" : "0",
      isInsuranceGoal: "0",
      isGameWinningGoal: "0"
    },
    plus_players: plus_players,
    minus_players: minus_players
  }

  data[:periods][PERIOD_INFO[row[2]][:id].to_i - 1][:goals] << goal
end

# PERIOD GOAL TOTALS
data[:periods].each do |period|
  home_goal_count = period[:goals].filter { |goal| TEAM_HASH[goal[:team][:abbreviation]] == "H" }.length
  visiting_goal_count = period[:goals].filter { |goal| TEAM_HASH[goal[:team][:abbreviation]] == "V" }.length
  period[:stats][:homeGoals] = home_goal_count.to_s
  period[:stats][:visitingGoals] = visiting_goal_count.to_s
end

File.write("#{game_id}_created_data.json", data.to_json)

Caveats

There are a few things you'll still need to do manually:

  • Goals

    • assistNumbers
    • game_goal_id
    • isInsuranceGoal
    • isGameWinningGoal
  • Penalties

    • game_penalty_id
    • isPowerPlay
    • isBench
    • ruleNumber

Some of these may be able to be automated but are not implemented yet.