maxpreps-scraper 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maxpreps_scraper-0.1.0/LICENSE +21 -0
- maxpreps_scraper-0.1.0/PKG-INFO +108 -0
- maxpreps_scraper-0.1.0/README.md +78 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper/__init__.py +1 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper/scraper.py +286 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper.egg-info/PKG-INFO +108 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper.egg-info/SOURCES.txt +11 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper.egg-info/dependency_links.txt +1 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper.egg-info/requires.txt +6 -0
- maxpreps_scraper-0.1.0/maxpreps_scraper.egg-info/top_level.txt +1 -0
- maxpreps_scraper-0.1.0/setup.cfg +4 -0
- maxpreps_scraper-0.1.0/setup.py +27 -0
- maxpreps_scraper-0.1.0/tests/test_scraper.py +42 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Raghav Dhir
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maxpreps-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python scraper for MaxPreps high school sports data
|
|
5
|
+
Home-page: https://github.com/raghavdhir03/maxpreps_scraper
|
|
6
|
+
Author: Raghav Dhir
|
|
7
|
+
Author-email: dhir.raghav@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: beautifulsoup4
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: lxml
|
|
19
|
+
Requires-Dist: html5lib
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# MaxPreps Web Scraper
|
|
32
|
+
|
|
33
|
+
This Python module scrapes high school sports data from [MaxPreps.com](https://www.maxpreps.com), enabling efficient extraction of team rankings and game results across states and sports.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Retrieve team rankings with strength of schedule, rating, and team links.
|
|
38
|
+
- Scrape team schedules, game outcomes, rankings
|
|
39
|
+
- Includes address/location data for each team.
|
|
40
|
+
- Multi-threaded scraping for speed and efficiency.
|
|
41
|
+
- Cleans and structures game metadata (venue, game type, outcome, etc.).
|
|
42
|
+
|
|
43
|
+
## File Structure
|
|
44
|
+
|
|
45
|
+
- `scraper.py`: Contains the `MaxPrepsScraper` class
|
|
46
|
+
|
|
47
|
+
## ️ Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install requests beautifulsoup4 pandas tqdm
|
|
51
|
+
```
|
|
52
|
+
## Functions
|
|
53
|
+
|
|
54
|
+
##### `get_rankings(state: str, sport: str, year: str, boys: bool = True)`
|
|
55
|
+
|
|
56
|
+
Retrieves state rankings for a given sport and academic year.
|
|
57
|
+
**Parameters:**
|
|
58
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'` for Texas)
|
|
59
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
60
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
61
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
62
|
+
|
|
63
|
+
**Returns:**
|
|
64
|
+
`pandas.DataFrame` containing:
|
|
65
|
+
- School Name
|
|
66
|
+
- State Rank
|
|
67
|
+
- Strength of Schedule (SOS)
|
|
68
|
+
- Team Rating
|
|
69
|
+
- Team URL
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
##### `get_contests(state: str, sport: str, year: str, boys: bool = True, cities: list = None)`
|
|
74
|
+
|
|
75
|
+
Scrapes all contests (games) for selected schools by state, sport, and year.
|
|
76
|
+
**Parameters:**
|
|
77
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'`)
|
|
78
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
79
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
80
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
81
|
+
- `cities` (`list`, optional): A list of cities to filter for, e.g., `['austin', 'el paso']` (if omitted, full state will be scraped)
|
|
82
|
+
|
|
83
|
+
**Returns:**
|
|
84
|
+
`pandas.DataFrame` containing game-level contest data, including:
|
|
85
|
+
- Date, opponent, venue, scores
|
|
86
|
+
- Team location details (address, city, state, zipcode)
|
|
87
|
+
- URLs to the MaxPreps pages of both teams
|
|
88
|
+
|
|
89
|
+
## Usage and Example Output
|
|
90
|
+
```
|
|
91
|
+
from scraper import MaxPrepsScraper
|
|
92
|
+
scraper = MaxPrepsScraper()
|
|
93
|
+
|
|
94
|
+
# Get Team Rankings
|
|
95
|
+
rankings_df = scraper.get_rankings(state = 'de', sport = 'football', year = '23-24')
|
|
96
|
+
|
|
97
|
+
# Get Contest Data
|
|
98
|
+
contests_df = scraper.get_contests(state='tx', sport='basketball', year='21-22', boys=False, cities = ['san antonio'])
|
|
99
|
+
|
|
100
|
+
#output datframes
|
|
101
|
+
rankings_df.head(10)
|
|
102
|
+
contests_df.head(10)
|
|
103
|
+
```
|
|
104
|
+

|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# MaxPreps Web Scraper
|
|
2
|
+
|
|
3
|
+
This Python module scrapes high school sports data from [MaxPreps.com](https://www.maxpreps.com), enabling efficient extraction of team rankings and game results across states and sports.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Retrieve team rankings with strength of schedule, rating, and team links.
|
|
8
|
+
- Scrape team schedules, game outcomes, rankings
|
|
9
|
+
- Includes address/location data for each team.
|
|
10
|
+
- Multi-threaded scraping for speed and efficiency.
|
|
11
|
+
- Cleans and structures game metadata (venue, game type, outcome, etc.).
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
- `scraper.py`: Contains the `MaxPrepsScraper` class
|
|
16
|
+
|
|
17
|
+
## ️ Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install requests beautifulsoup4 pandas tqdm
|
|
21
|
+
```
|
|
22
|
+
## Functions
|
|
23
|
+
|
|
24
|
+
##### `get_rankings(state: str, sport: str, year: str, boys: bool = True)`
|
|
25
|
+
|
|
26
|
+
Retrieves state rankings for a given sport and academic year.
|
|
27
|
+
**Parameters:**
|
|
28
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'` for Texas)
|
|
29
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
30
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
31
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
32
|
+
|
|
33
|
+
**Returns:**
|
|
34
|
+
`pandas.DataFrame` containing:
|
|
35
|
+
- School Name
|
|
36
|
+
- State Rank
|
|
37
|
+
- Strength of Schedule (SOS)
|
|
38
|
+
- Team Rating
|
|
39
|
+
- Team URL
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
##### `get_contests(state: str, sport: str, year: str, boys: bool = True, cities: list = None)`
|
|
44
|
+
|
|
45
|
+
Scrapes all contests (games) for selected schools by state, sport, and year.
|
|
46
|
+
**Parameters:**
|
|
47
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'`)
|
|
48
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
49
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
50
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
51
|
+
- `cities` (`list`, optional): A list of cities to filter for, e.g., `['austin', 'el paso']` (if omitted, full state will be scraped)
|
|
52
|
+
|
|
53
|
+
**Returns:**
|
|
54
|
+
`pandas.DataFrame` containing game-level contest data, including:
|
|
55
|
+
- Date, opponent, venue, scores
|
|
56
|
+
- Team location details (address, city, state, zipcode)
|
|
57
|
+
- URLs to the MaxPreps pages of both teams
|
|
58
|
+
|
|
59
|
+
## Usage and Example Output
|
|
60
|
+
```
|
|
61
|
+
from scraper import MaxPrepsScraper
|
|
62
|
+
scraper = MaxPrepsScraper()
|
|
63
|
+
|
|
64
|
+
# Get Team Rankings
|
|
65
|
+
rankings_df = scraper.get_rankings(state = 'de', sport = 'football', year = '23-24')
|
|
66
|
+
|
|
67
|
+
# Get Contest Data
|
|
68
|
+
contests_df = scraper.get_contests(state='tx', sport='basketball', year='21-22', boys=False, cities = ['san antonio'])
|
|
69
|
+
|
|
70
|
+
#output datframes
|
|
71
|
+
rankings_df.head(10)
|
|
72
|
+
contests_df.head(10)
|
|
73
|
+
```
|
|
74
|
+

|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .scraper import MaxPrepsScraper
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from bs4 import BeautifulSoup
|
|
3
|
+
from tqdm import tqdm
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from io import StringIO
|
|
6
|
+
import concurrent.futures
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
class MaxPrepsScraper():
|
|
11
|
+
|
|
12
|
+
BASE_URL = 'https://www.maxpreps.com'
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
"""Initialize scraper settings (headers, session, etc.)."""
|
|
16
|
+
self.session = requests.Session()
|
|
17
|
+
|
|
18
|
+
def get_rankings(self, state: str, sport: str, year: str, boys=True):
|
|
19
|
+
"""
|
|
20
|
+
Get rankings for a given sport, state, and year.
|
|
21
|
+
Returns a Pandas DataFrame with:
|
|
22
|
+
- School Name
|
|
23
|
+
- State Rank
|
|
24
|
+
- Strength of Schedule (SOS)
|
|
25
|
+
- Team Rating
|
|
26
|
+
- Team URL
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if sport not in ['basketball', 'football', 'baseball', 'soccer', 'volleyball', 'lacrosse', 'softball']:
|
|
30
|
+
raise ValueError(f"Sport '{sport}' is not supported. Please choose from: basketball, football, baseball, soccer, volleyball, lacrosse, softball")
|
|
31
|
+
|
|
32
|
+
if (boys == False and sport in ['football', 'baseball']) or (boys and sport in ['softball', 'volleyball']):
|
|
33
|
+
raise ValueError(f"{'boys' if boys else 'girls'} {sport} is not supported")
|
|
34
|
+
|
|
35
|
+
if sport == 'soccer' and state not in ['tx', 'la', 'ms', 'hi', 'ca', 'fl', 'az']:
|
|
36
|
+
raise ValueError(f"Soccer is not supported in {state}")
|
|
37
|
+
|
|
38
|
+
state_url = f"{self.BASE_URL}/{state}/{sport}/{'' if (boys or sport in ['softball', 'volleyball']) else 'girls/'}{'winter/' if sport == 'soccer' else ''}{year}/rankings"
|
|
39
|
+
|
|
40
|
+
page = 1
|
|
41
|
+
full_df = pd.DataFrame()
|
|
42
|
+
|
|
43
|
+
while True:
|
|
44
|
+
page_url = f'{state_url}/{page}/'
|
|
45
|
+
|
|
46
|
+
response = requests.get(page_url)
|
|
47
|
+
|
|
48
|
+
# Stop if the page does not exist
|
|
49
|
+
if response.status_code != 200:
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
53
|
+
|
|
54
|
+
# You can add extra checks here to stop if there's no data
|
|
55
|
+
table = soup.find('table')
|
|
56
|
+
if not table:
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
# Process your table here
|
|
60
|
+
df = self._scrape_table(soup)
|
|
61
|
+
|
|
62
|
+
full_df = pd.concat([full_df, df]).reset_index(drop=True)
|
|
63
|
+
|
|
64
|
+
page += 1
|
|
65
|
+
|
|
66
|
+
full_df['Team'] = full_df['Team'].str.replace(r'^([A-Z])\1', r'\1', regex=True) #take care of schools with no mascot. Turns AAustin into Austin
|
|
67
|
+
|
|
68
|
+
return full_df
|
|
69
|
+
|
|
70
|
+
#sports that this function suppoprts: basketball, football, baseball, soccer, volleyball, lacrosse, softball,
|
|
71
|
+
def get_contests(self, state: str, sport: str, year: str, boys: bool = True, cities=None):
|
|
72
|
+
|
|
73
|
+
if sport not in ['basketball', 'football', 'baseball', 'soccer', 'volleyball', 'lacrosse', 'softball']:
|
|
74
|
+
raise ValueError(f"Sport '{sport}' is not supported. Please choose from: basketball, football, baseball, soccer, volleyball, lacrosse, softball")
|
|
75
|
+
|
|
76
|
+
if (boys == False and sport in ['football', 'baseball']) or (boys and sport in ['softball', 'volleyball']):
|
|
77
|
+
raise ValueError(f"{'boys' if boys else 'girls'} {sport} is not supported")
|
|
78
|
+
|
|
79
|
+
if sport == 'soccer' and state not in ['tx', 'la', 'ms', 'hi', 'ca', 'fl', 'az']:
|
|
80
|
+
raise ValueError(f"Soccer is not supported in {state}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
state_url = f"{self.BASE_URL}/{state}/{sport}/{'' if (boys or sport in ['softball', 'volleyball']) else 'girls/'}{'winter/' if sport == 'soccer' else ''}{year}/rankings"
|
|
84
|
+
page = 1
|
|
85
|
+
school_list = []
|
|
86
|
+
|
|
87
|
+
# Step 1: Collect all school links
|
|
88
|
+
while True:
|
|
89
|
+
page_url = f'{state_url}/{page}/'
|
|
90
|
+
#print(page_url)
|
|
91
|
+
response = requests.get(page_url)
|
|
92
|
+
|
|
93
|
+
if response.status_code != 200:
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
97
|
+
page_list = self._get_school_list(soup, cities)
|
|
98
|
+
#print(page_list)
|
|
99
|
+
school_list += page_list
|
|
100
|
+
#print(len(school_list))
|
|
101
|
+
page += 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Step 2: Thread-safe scrape function that still uses self._scrape_table()
|
|
106
|
+
def _fetch_and_scrape(school, url, base_url='https://www.maxpreps.com'):
|
|
107
|
+
try:
|
|
108
|
+
response = requests.get(base_url + url, timeout=10)
|
|
109
|
+
if response.status_code == 200:
|
|
110
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
111
|
+
table = self._scrape_table(soup)
|
|
112
|
+
table['Team 2 URL'] = self._extract_opponent_urls(soup)
|
|
113
|
+
location_info = self._extract_location_info(soup)
|
|
114
|
+
table['Team 1'] = school
|
|
115
|
+
table['Team 1 Address'] = location_info['address']
|
|
116
|
+
table['Team 1 City'] = location_info['city']
|
|
117
|
+
table['Team 1 State'] = location_info['state']
|
|
118
|
+
table['Team 1 Zipcode'] = location_info['zipcode']
|
|
119
|
+
table['Team 1 URL'] = url
|
|
120
|
+
return table
|
|
121
|
+
else:
|
|
122
|
+
print(f"Failed to fetch {url} (status code: {response.status_code})")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
print(f"Error scraping {school} at {url}: {e}")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Step 3: Run threads
|
|
128
|
+
|
|
129
|
+
full_df = pd.DataFrame()
|
|
130
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
131
|
+
futures = [executor.submit(_fetch_and_scrape, school, url) for school, url in school_list]
|
|
132
|
+
|
|
133
|
+
with tqdm(total=len(futures), desc=f"Scraping Schools for {', '.join(city for city in cities) if cities else ''} {state}", unit=" schools") as pbar:
|
|
134
|
+
for future in as_completed(futures):
|
|
135
|
+
result_df = future.result()
|
|
136
|
+
if result_df is not None and not result_df.empty:
|
|
137
|
+
full_df = pd.concat([full_df, result_df], ignore_index=True)
|
|
138
|
+
pbar.update(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
full_df = self._clean_contest_data(full_df)
|
|
142
|
+
|
|
143
|
+
return full_df
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _scrape_table(self, soup):
|
|
147
|
+
table = soup.find('table')
|
|
148
|
+
html = str(table)
|
|
149
|
+
df = pd.read_html(StringIO(html))[0]
|
|
150
|
+
return df
|
|
151
|
+
|
|
152
|
+
def _get_school_list(self, soup, cities=None, base_url='https://www.maxpreps.com'):
|
|
153
|
+
"""
|
|
154
|
+
Extracts school names and links to schedule pages based on new MaxPreps HTML structure.
|
|
155
|
+
"""
|
|
156
|
+
school_data = []
|
|
157
|
+
|
|
158
|
+
for td in soup.find_all('td'):
|
|
159
|
+
a_tag = td.find('a', href=True)
|
|
160
|
+
if a_tag and 'schedule' in a_tag['href']:
|
|
161
|
+
href = a_tag['href']
|
|
162
|
+
|
|
163
|
+
# Check if cities is None (no filter), or if any city is in the href
|
|
164
|
+
if cities is None or any(city.lower().replace(" ", "-") == href.lower().split('/')[2] for city in cities):
|
|
165
|
+
name = a_tag.get_text(strip=True)
|
|
166
|
+
link = href
|
|
167
|
+
school_data.append((name, link))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
return school_data
|
|
171
|
+
|
|
172
|
+
def _extract_location_info(self, soup):
|
|
173
|
+
|
|
174
|
+
address_element = soup.select_one('address')
|
|
175
|
+
if not address_element:
|
|
176
|
+
return {
|
|
177
|
+
"address": None,
|
|
178
|
+
"city": None,
|
|
179
|
+
"state": None,
|
|
180
|
+
"zipcode": None
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
# Get city/state/zip from <span>
|
|
184
|
+
city_state_span = address_element.find('span')
|
|
185
|
+
if city_state_span:
|
|
186
|
+
city_state_text = city_state_span.get_text(strip=True)
|
|
187
|
+
city_state_span.decompose() # Remove span from the address block
|
|
188
|
+
else:
|
|
189
|
+
city_state_text = ""
|
|
190
|
+
|
|
191
|
+
# Now get street address (without the span)
|
|
192
|
+
street_address = address_element.get_text(strip=True)
|
|
193
|
+
|
|
194
|
+
# Parse city, state, and zip using regex
|
|
195
|
+
city, state, zipcode = None, None, None
|
|
196
|
+
match = re.match(r'^(.*?),\s*([A-Z]{2})\s*(\d{5})(?:-\d{4})?$', city_state_text)
|
|
197
|
+
if match:
|
|
198
|
+
city, state, zipcode = match.groups()
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
"address": street_address or None,
|
|
202
|
+
"city": city,
|
|
203
|
+
"state": state,
|
|
204
|
+
"zipcode": zipcode
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def _extract_opponent_urls(self, soup): #Extracts a list of opponent URLs (or None) from the 'Opponent' column in the schedule table.
|
|
208
|
+
|
|
209
|
+
opponent_urls = []
|
|
210
|
+
|
|
211
|
+
# Find the schedule table
|
|
212
|
+
table = soup.find('table')
|
|
213
|
+
if not table:
|
|
214
|
+
return []
|
|
215
|
+
|
|
216
|
+
# Step 1: Get header row and find the column index for 'Opponent'
|
|
217
|
+
header_row = table.find('tr')
|
|
218
|
+
headers = [th.get_text(strip=True).lower() for th in header_row.find_all('th')]
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
opponent_idx = headers.index('opponent')
|
|
222
|
+
except ValueError:
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
# Step 2: Go through all remaining rows and get href from the opponent column
|
|
226
|
+
for row in table.find_all('tr')[1:]: # skip header row
|
|
227
|
+
cells = row.find_all('td')
|
|
228
|
+
if len(cells) > opponent_idx:
|
|
229
|
+
opponent_cell = cells[opponent_idx]
|
|
230
|
+
a_tag = opponent_cell.find('a', href=True)
|
|
231
|
+
|
|
232
|
+
if a_tag and a_tag['href'].endswith('/schedule/'):
|
|
233
|
+
opponent_urls.append(a_tag['href'])
|
|
234
|
+
else:
|
|
235
|
+
opponent_urls.append(None)
|
|
236
|
+
|
|
237
|
+
return opponent_urls
|
|
238
|
+
|
|
239
|
+
def _clean_contest_data(self, df):
|
|
240
|
+
# --- 1. Clean 'Opponent' column ---
|
|
241
|
+
opponent_pattern = r'(?P<VenueRaw>vs\.?|@)?\s*(?P<Team2>.+?)(?P<Star>\*{0,3})$'
|
|
242
|
+
|
|
243
|
+
opponent_info = df['Opponent'].str.extract(opponent_pattern)
|
|
244
|
+
|
|
245
|
+
# Venue
|
|
246
|
+
opponent_info['Venue'] = opponent_info['VenueRaw'].map({
|
|
247
|
+
'vs': 'Home',
|
|
248
|
+
'vs.': 'Home',
|
|
249
|
+
'@': 'Away'
|
|
250
|
+
}).fillna('Neutral')
|
|
251
|
+
|
|
252
|
+
# Game Type
|
|
253
|
+
opponent_info['Game Type'] = opponent_info['Star'].map({
|
|
254
|
+
'': 'Regular Season',
|
|
255
|
+
'*': 'District',
|
|
256
|
+
'**': 'Playoff',
|
|
257
|
+
'***': 'Tournament'
|
|
258
|
+
}).fillna('Regular Season')
|
|
259
|
+
|
|
260
|
+
df['Team 2'] = opponent_info['Team2'].str.strip()
|
|
261
|
+
df['Venue'] = opponent_info['Venue']
|
|
262
|
+
df['Game Type'] = opponent_info['Game Type']
|
|
263
|
+
|
|
264
|
+
# --- 2. Clean 'Result' column ---
|
|
265
|
+
result_pattern = r'(?P<Outcome>[WL])(?: (?P<Team1_Score>\d+)-(?P<Team2_Score>\d+)|\((?P<Forfeit>FF)\))'
|
|
266
|
+
|
|
267
|
+
result_info = df['Result'].str.extract(result_pattern)
|
|
268
|
+
|
|
269
|
+
df['Outcome'] = result_info['Outcome']
|
|
270
|
+
df['Team 1 Score'] = pd.to_numeric(result_info['Team1_Score'], errors='coerce')
|
|
271
|
+
df['Team 2 Score'] = pd.to_numeric(result_info['Team2_Score'], errors='coerce')
|
|
272
|
+
df['Forfeit'] = result_info['Forfeit'].notna()
|
|
273
|
+
|
|
274
|
+
cols_to_drop = ['Opponent', 'Result', 'Game Info', 'Match Info']
|
|
275
|
+
df.drop(columns=[col for col in cols_to_drop if col in df.columns], inplace=True)
|
|
276
|
+
|
|
277
|
+
df.loc[df['Outcome'] == 'L', ['Team 1 Score', 'Team 2 Score']] = df.loc[df['Outcome'] == 'L', ['Team 2 Score', 'Team 1 Score']].values # Swap scores if team 1 lost
|
|
278
|
+
|
|
279
|
+
df['Team 1'] = df['Team 1'].str.replace(r'(^[A-Z])\1', '', regex=True) #take care of schools with no mascot. Turns AAustin into Austin
|
|
280
|
+
|
|
281
|
+
# Reorder columns
|
|
282
|
+
new_order = ['Date', 'Team 1', 'Team 2', 'Team 1 Score', 'Team 2 Score', 'Outcome', 'Forfeit', 'Venue', 'Game Type', 'Team 1 Address', 'Team 1 City', 'Team 1 State', 'Team 1 Zipcode', 'Team 1 URL', 'Team 2 URL']
|
|
283
|
+
df = df[new_order]
|
|
284
|
+
|
|
285
|
+
return df
|
|
286
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maxpreps-scraper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python scraper for MaxPreps high school sports data
|
|
5
|
+
Home-page: https://github.com/raghavdhir03/maxpreps_scraper
|
|
6
|
+
Author: Raghav Dhir
|
|
7
|
+
Author-email: dhir.raghav@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: beautifulsoup4
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: lxml
|
|
19
|
+
Requires-Dist: html5lib
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: classifier
|
|
23
|
+
Dynamic: description
|
|
24
|
+
Dynamic: description-content-type
|
|
25
|
+
Dynamic: home-page
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# MaxPreps Web Scraper
|
|
32
|
+
|
|
33
|
+
This Python module scrapes high school sports data from [MaxPreps.com](https://www.maxpreps.com), enabling efficient extraction of team rankings and game results across states and sports.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- Retrieve team rankings with strength of schedule, rating, and team links.
|
|
38
|
+
- Scrape team schedules, game outcomes, rankings
|
|
39
|
+
- Includes address/location data for each team.
|
|
40
|
+
- Multi-threaded scraping for speed and efficiency.
|
|
41
|
+
- Cleans and structures game metadata (venue, game type, outcome, etc.).
|
|
42
|
+
|
|
43
|
+
## File Structure
|
|
44
|
+
|
|
45
|
+
- `scraper.py`: Contains the `MaxPrepsScraper` class
|
|
46
|
+
|
|
47
|
+
## ️ Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install requests beautifulsoup4 pandas tqdm
|
|
51
|
+
```
|
|
52
|
+
## Functions
|
|
53
|
+
|
|
54
|
+
##### `get_rankings(state: str, sport: str, year: str, boys: bool = True)`
|
|
55
|
+
|
|
56
|
+
Retrieves state rankings for a given sport and academic year.
|
|
57
|
+
**Parameters:**
|
|
58
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'` for Texas)
|
|
59
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
60
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
61
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
62
|
+
|
|
63
|
+
**Returns:**
|
|
64
|
+
`pandas.DataFrame` containing:
|
|
65
|
+
- School Name
|
|
66
|
+
- State Rank
|
|
67
|
+
- Strength of Schedule (SOS)
|
|
68
|
+
- Team Rating
|
|
69
|
+
- Team URL
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
##### `get_contests(state: str, sport: str, year: str, boys: bool = True, cities: list = None)`
|
|
74
|
+
|
|
75
|
+
Scrapes all contests (games) for selected schools by state, sport, and year.
|
|
76
|
+
**Parameters:**
|
|
77
|
+
- `state` (`str`): Two-letter state abbreviation (e.g., `'tx'`)
|
|
78
|
+
- `sport` (`str`): Sport name (e.g., `'basketball'`)
|
|
79
|
+
- `year` (`str`): Academic year of the season (e.g., `'21-22'`)
|
|
80
|
+
- `boys` (`bool`, optional): Set to `True` for boys' sports and `False` for girls'. Defaults to `True`.
|
|
81
|
+
- `cities` (`list`, optional): A list of cities to filter for, e.g., `['austin', 'el paso']` (if omitted, full state will be scraped)
|
|
82
|
+
|
|
83
|
+
**Returns:**
|
|
84
|
+
`pandas.DataFrame` containing game-level contest data, including:
|
|
85
|
+
- Date, opponent, venue, scores
|
|
86
|
+
- Team location details (address, city, state, zipcode)
|
|
87
|
+
- URLs to the MaxPreps pages of both teams
|
|
88
|
+
|
|
89
|
+
## Usage and Example Output
|
|
90
|
+
```
|
|
91
|
+
from scraper import MaxPrepsScraper
|
|
92
|
+
scraper = MaxPrepsScraper()
|
|
93
|
+
|
|
94
|
+
# Get Team Rankings
|
|
95
|
+
rankings_df = scraper.get_rankings(state = 'de', sport = 'football', year = '23-24')
|
|
96
|
+
|
|
97
|
+
# Get Contest Data
|
|
98
|
+
contests_df = scraper.get_contests(state='tx', sport='basketball', year='21-22', boys=False, cities = ['san antonio'])
|
|
99
|
+
|
|
100
|
+
#output datframes
|
|
101
|
+
rankings_df.head(10)
|
|
102
|
+
contests_df.head(10)
|
|
103
|
+
```
|
|
104
|
+

|
|
105
|
+

|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
maxpreps_scraper/__init__.py
|
|
5
|
+
maxpreps_scraper/scraper.py
|
|
6
|
+
maxpreps_scraper.egg-info/PKG-INFO
|
|
7
|
+
maxpreps_scraper.egg-info/SOURCES.txt
|
|
8
|
+
maxpreps_scraper.egg-info/dependency_links.txt
|
|
9
|
+
maxpreps_scraper.egg-info/requires.txt
|
|
10
|
+
maxpreps_scraper.egg-info/top_level.txt
|
|
11
|
+
tests/test_scraper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
maxpreps_scraper
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='maxpreps-scraper', # Package name (pip install maxpreps-scraper)
|
|
5
|
+
version='0.1.0', # Initial version
|
|
6
|
+
packages=find_packages(), # Automatically find all modules inside your package folder
|
|
7
|
+
install_requires=[
|
|
8
|
+
'requests',
|
|
9
|
+
'beautifulsoup4',
|
|
10
|
+
'tqdm',
|
|
11
|
+
'pandas',
|
|
12
|
+
'lxml',
|
|
13
|
+
'html5lib',
|
|
14
|
+
],
|
|
15
|
+
author='Raghav Dhir',
|
|
16
|
+
author_email='dhir.raghav@gmail.com',
|
|
17
|
+
description='A Python scraper for MaxPreps high school sports data',
|
|
18
|
+
long_description=open('README.md').read(),
|
|
19
|
+
long_description_content_type='text/markdown', # So Markdown renders on PyPI
|
|
20
|
+
url='https://github.com/raghavdhir03/maxpreps_scraper', # Your GitHub repo
|
|
21
|
+
classifiers=[
|
|
22
|
+
'Programming Language :: Python :: 3',
|
|
23
|
+
'License :: OSI Approved :: MIT License',
|
|
24
|
+
'Operating System :: OS Independent',
|
|
25
|
+
],
|
|
26
|
+
python_requires='>=3.7',
|
|
27
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from bs4 import BeautifulSoup
|
|
4
|
+
from io import StringIO
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
8
|
+
from maxpreps_scraper.scraper import MaxPrepsScraper
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
import concurrent.futures
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## testing scraping state rankings
|
|
15
|
+
'''scraper = MaxPrepsScraper()
|
|
16
|
+
table = scraper.get_rankings('de', 'basketball', '21-22', boys=False)
|
|
17
|
+
print(table.head())'''
|
|
18
|
+
|
|
19
|
+
## testing scraping contests
|
|
20
|
+
scraper = MaxPrepsScraper()
|
|
21
|
+
#football_data = scraper.get_contests('de', 'football', '21-22')
|
|
22
|
+
#basketball_data = scraper.get_contests('tx', 'basketball', '21-22', cities=['austin', 'san antonio'])
|
|
23
|
+
baseball_data = scraper.get_contests('hi', 'baseball', '21-22')
|
|
24
|
+
#lacrosse_data = scraper.get_contests('tx', 'lacrosse', '21-22', boys=False)
|
|
25
|
+
#soccer_data = scraper.get_contests('la', 'soccer', '21-22')
|
|
26
|
+
#softball_data = scraper.get_contests('de', 'softball', '21-22')
|
|
27
|
+
#print(football_data.head())
|
|
28
|
+
#print(basketball_data.head())
|
|
29
|
+
print(baseball_data.head())
|
|
30
|
+
#print(softball_data.head())
|
|
31
|
+
#print(lacrosse_data.head())
|
|
32
|
+
#print(soccer_data.head())
|
|
33
|
+
|
|
34
|
+
"""baseball_rankings = scraper.get_rankings('de', 'baseball', '21-22')
|
|
35
|
+
print(baseball_rankings.head())"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
##testing roster function
|
|
39
|
+
"""scraper = MaxPrepsScraper()
|
|
40
|
+
roster = scraper.get_roster('https://www.maxpreps.com/tx/austin/bowie-bulldogs/basketball/21-22/roster/')
|
|
41
|
+
print(roster.head())"""
|
|
42
|
+
|