mirror of
https://github.com/nprasad2077/NBA_Go.git
synced 2026-09-22 14:05:13 +00:00
226 lines
7.2 KiB
Go
226 lines
7.2 KiB
Go
//go:build ignore
|
||
// +build ignore
|
||
|
||
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/csv"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/PuerkitoBio/goquery"
|
||
"golang.org/x/net/html"
|
||
)
|
||
|
||
func main() {
|
||
playerID := flag.String("playerid", "", "Basketball-Reference player ID (e.g. derozde01)")
|
||
playerName := flag.String("playername", "", "Player name for file naming (e.g. \"DeMar DeRozan\")")
|
||
startSeason := flag.Int("start", 0, "Recent start season (e.g. 2024)")
|
||
endSeason := flag.Int("end", 0, "Rookie/end season (e.g. 2021)")
|
||
outDir := flag.String("outdir", "", "If set, write CSV files into this directory; otherwise stdout")
|
||
flag.Parse()
|
||
|
||
if *playerID == "" || *playerName == "" || *startSeason == 0 || *endSeason == 0 {
|
||
fmt.Fprintln(os.Stderr, "❌ must provide -playerid, -playername, -start and -end")
|
||
os.Exit(1)
|
||
}
|
||
if *startSeason < *endSeason {
|
||
fmt.Fprintln(os.Stderr, "❌ start must be >= end")
|
||
os.Exit(1)
|
||
}
|
||
|
||
for season := *startSeason; season >= *endSeason; season-- {
|
||
url := fmt.Sprintf(
|
||
"https://www.basketball-reference.com/players/%s/%s/shooting/%d",
|
||
(*playerID)[:1], *playerID, season,
|
||
)
|
||
fmt.Println("Fetching:", url)
|
||
|
||
// Build a real-browser request
|
||
client := &http.Client{}
|
||
req, err := http.NewRequest("GET", url, nil)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Request creation error for season %d: %v\n", season, err)
|
||
continue
|
||
}
|
||
req.Header.Set("User-Agent",
|
||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36",
|
||
)
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "HTTP error for %d: %v\n", season, err)
|
||
continue
|
||
}
|
||
bodyBytes, err := io.ReadAll(resp.Body)
|
||
resp.Body.Close()
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Read error for %d: %v\n", season, err)
|
||
continue
|
||
}
|
||
|
||
// Parse the full HTML to find the commented-out shot-chart block
|
||
shotHTML := extractCommentedShotChart(bodyBytes)
|
||
if shotHTML == "" {
|
||
fmt.Fprintf(os.Stderr, "❌ no shot‐chart comment found for %d\n", season)
|
||
continue
|
||
}
|
||
|
||
// Now parse that snippet
|
||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(shotHTML))
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Parse error for %d: %v\n", season, err)
|
||
continue
|
||
}
|
||
|
||
// grab the shot-wrapper inside the commented HTML
|
||
wrapper := doc.Find("div#div_shot-chart div#shot-wrapper")
|
||
if wrapper.Length() == 0 {
|
||
fmt.Fprintf(os.Stderr, "❌ no shot‐wrapper found in comment for %d\n", season)
|
||
continue
|
||
}
|
||
|
||
shots := wrapper.Find("div.tooltip.make, div.tooltip.miss")
|
||
var rows [][]string
|
||
shots.Each(func(_ int, s *goquery.Selection) {
|
||
style, _ := s.Attr("style")
|
||
tip, _ := s.Attr("tip")
|
||
|
||
// parse top/left
|
||
parts := strings.Split(style, ";")
|
||
topVal := parsePx(parts[0])
|
||
leftVal:= parsePx(parts[1])
|
||
|
||
tipParts := strings.Split(tip, "<br>")
|
||
// date, team code, opponent
|
||
dateSegs := strings.SplitN(tipParts[0], ", ", 3)
|
||
date := dateSegs[0] + "," + dateSegs[1]
|
||
team := ""
|
||
if len(dateSegs) == 3 {
|
||
team = dateSegs[2]
|
||
}
|
||
opponent := ""
|
||
if parts := strings.SplitN(tipParts[0], " at ", 2); len(parts) == 2 {
|
||
opponent = parts[1]
|
||
} else if parts := strings.SplitN(tipParts[0], " vs ", 2); len(parts) == 2 {
|
||
opponent = parts[1]
|
||
}
|
||
|
||
// quarter & time
|
||
qt := strings.SplitN(tipParts[1], ",", 2)
|
||
quarter := qt[0]
|
||
timeRem := strings.Fields(qt[1])[0]
|
||
|
||
// result, shot type, distance
|
||
rt := strings.Fields(tipParts[2])
|
||
result := (rt[0] == "Made")
|
||
shotType := rt[1]
|
||
distance := parseInt(rt[len(rt)-2])
|
||
|
||
// scores
|
||
last := strings.Fields(tipParts[3])
|
||
sc := strings.Split(last[len(last)-1], "-")
|
||
teamScore := parseInt(sc[0])
|
||
opponentScore := parseInt(sc[1])
|
||
lead := teamScore > opponentScore
|
||
|
||
rows = append(rows, []string{
|
||
*playerName,
|
||
fmt.Sprint(topVal),
|
||
fmt.Sprint(leftVal),
|
||
date,
|
||
quarter,
|
||
timeRem,
|
||
fmt.Sprint(result),
|
||
shotType,
|
||
fmt.Sprint(distance),
|
||
fmt.Sprint(lead),
|
||
fmt.Sprint(teamScore),
|
||
fmt.Sprint(opponentScore),
|
||
opponent,
|
||
team,
|
||
fmt.Sprint(season),
|
||
*playerID,
|
||
})
|
||
})
|
||
|
||
headers := []string{
|
||
"playerName","top","left","date","qtr","timeRemaining",
|
||
"result","shotType","distanceFt","lead",
|
||
"teamScore","opponentTeamScore","opponent","team","season","playerId",
|
||
}
|
||
|
||
var w *csv.Writer
|
||
if *outDir != "" {
|
||
os.MkdirAll(*outDir, 0755)
|
||
fname := filepath.Join(*outDir,
|
||
fmt.Sprintf("%s_%d_shotchart.csv", *playerID, season),
|
||
)
|
||
f, err := os.Create(fname)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Could not create %s: %v\n", fname, err)
|
||
continue
|
||
}
|
||
defer f.Close()
|
||
w = csv.NewWriter(f)
|
||
fmt.Printf("→ Writing %d rows to %s\n", len(rows), fname)
|
||
} else {
|
||
w = csv.NewWriter(os.Stdout)
|
||
}
|
||
|
||
w.Write(headers)
|
||
for _, r := range rows {
|
||
w.Write(r)
|
||
}
|
||
w.Flush()
|
||
if err := w.Error(); err != nil {
|
||
fmt.Fprintf(os.Stderr, "CSV error: %v\n", err)
|
||
}
|
||
|
||
fmt.Printf("\nFirst 5 rows for %d:\n", season)
|
||
for i := 0; i < len(rows) && i < 5; i++ {
|
||
fmt.Println(rows[i])
|
||
}
|
||
fmt.Printf("✅ Season %d: %d total shots\n\n", season, len(rows))
|
||
}
|
||
}
|
||
|
||
// extractCommentedShotChart scans the HTML for a comment node containing
|
||
// the div#div_shot-chart block, and returns its inner HTML.
|
||
func extractCommentedShotChart(htmlBytes []byte) string {
|
||
root, err := html.Parse(bytes.NewReader(htmlBytes))
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
var found string
|
||
var f func(*html.Node)
|
||
f = func(n *html.Node) {
|
||
if n.Type == html.CommentNode && strings.Contains(n.Data, `id="div_shot-chart"`) {
|
||
found = n.Data
|
||
return
|
||
}
|
||
for c := n.FirstChild; c != nil && found == ""; c = c.NextSibling {
|
||
f(c)
|
||
}
|
||
}
|
||
f(root)
|
||
return found
|
||
}
|
||
|
||
func parsePx(s string) int {
|
||
parts := strings.Split(s, ":")
|
||
return parseInt(strings.TrimSuffix(parts[1], "px"))
|
||
}
|
||
|
||
func parseInt(s string) int {
|
||
v, _ := strconv.Atoi(strings.TrimSpace(s))
|
||
return v
|
||
} |