WNBA_SALARY_CAP: The Scraper That Turns Messy Team Pages Into a Clean 2026 Cap Model
A small Python pipeline uses link scoring, parser fallbacks, and data-cleaning heuristics to model WNBA payrolls from a source that was never designed to be machine-friendly.
- The repo treats sports HTML like an unreliable input stream and wins by layering fallbacks instead of trusting one parser path.
- Its smartest move is link scoring, which finds the right team page even when the site structure shifts.
- The real output is not scraped rows, but a workbook shaped for cap analysis and franchise-by-franchise comparison.
- The project matters because the WNBA's financial future is moving fast, and durable tooling is more useful than one-off scraping scripts.
The interesting thing about WNBA_SALARY_CAP is not that it scrapes a sports site. It is that it assumes the site will be messy, inconsistent, and a little hostile to automation, then builds around that assumption.
This is a Python ETL script with a narrow job: find the right Her Hoop Stats team page, clean the table, and push the results into a 2026 salary-cap workbook. That sounds simple until you inspect the source HTML and see why the code needs scoring, parser fallbacks, and cleanup heuristics just to stay upright.
The real job is not scraping. It is surviving bad HTML.
Her Hoop Stats is built for humans first. Links move, tables carry odd artifacts, and the page can include text that looks like data but should never make it into a model. The repo responds like a careful analyst would: do not trust the first parse, do not trust the first link, and do not trust raw table cells without cleaning them.
def _parse_currency_cell(cell_text: str) -> Optional[int]:
text = _clean_cell(cell_text)
if not text:
return None
text = text.replace('$', '').replace(',', '').strip()
return int(text) if text.isdigit() else None
def _is_summary_row(row: list[str]) -> bool:
joined = ' '.join(row).lower()
return 'summary' in joined or 'total' in joined
That small block captures the repo's temperament. It does not celebrate messy input. It trims it, normalizes it, and refuses to let summary noise contaminate the workbook.
How the pipeline chooses the right team page
The function that matters most is find_team_link. Instead of hardcoding a URL or relying on a single exact match, it scores candidates by signal quality: visible text, href text, and surrounding context like the target year. That makes the scraper more forgiving when the page gets rearranged, because the matcher is looking for evidence, not perfection.
| Signal | What it adds | Why it matters |
|---|---|---|
| Visible text match | High confidence | The link likely names the team directly. |
| Href match | Medium confidence | The URL may expose the right destination even when the text is abbreviated. |
| Nearby year signal | Context boost | The function can prefer the page tied to the target season. |
| Partial matches | Tie-breaking | The scraper still has a way to choose when several links look similar. |
That is the right instinct for a sports site. Team pages are not API endpoints. They are living pages with editorial drift, and a brittle matcher will break the first time a site owner changes a class name or shifts a navigation block.
Why the scraper needs two personalities: fast and stubborn
The fetch layer has two modes. In the happy path, requests keeps things fast and simple. When JavaScript rendering matters, Selenium steps in and acts stubbornly until the page is fully formed. That is not overengineering. It is the minimum viable defense for a source that may render different content depending on how it is loaded.
| Mode | Strength | Trade-off |
|---|---|---|
| requests | Fast and lightweight | Can miss content that appears after JavaScript runs. |
| Selenium | More complete page rendering | Slower and heavier to maintain. |
| Hybrid strategy | Balances speed and resilience | Requires a clear fallback policy. |
The point is not to worship Selenium. The point is to reserve it for the cases where static fetches fail. That keeps the tool practical instead of turning every scrape into a browser automation project.
Cleaning sports data is where the real work happens
_AUDIO_RE = re.compile(r'\s*This HTML5 audio player.*')
def _clean_cell(text: str) -> str:
text = _AUDIO_RE.sub('', text)
return ' '.join(text.split()).strip()
That regex tells you almost everything about the repo's personality. Someone ran into a weird source artifact, identified it precisely, and then encoded the fix so it would never poison the data again. That is boring in the best possible way.
transformative step forward
The Excel workbook is the product, not the scraper
The repository may look like a scraper, but the actual deliverable is the workbook. That matters. Analysts do not want raw HTML or even cleaned tables if the result cannot be compared across franchises, audited later, or dropped into a financial model.
| Raw scrape | Workbook output | Why the difference matters |
|---|---|---|
| One page at a time | Multi-sheet structure | Lets each franchise live in its own tab. |
| Unstable HTML | Structured cells | Makes review and recalculation safer. |
| Ad hoc extraction | Decision-ready artifact | Supports salary-cap analysis instead of one-off inspection. |
This is what makes the repo feel more like a tool than a demo. The code is optimized for an analyst's workflow, not for bragging rights about scraping coverage.
This league is transforming,
Why this matters for the WNBA's next financial phase
The 2026 focus is the other half of the story. As the league's economics evolve, the value of reliable salary-cap modeling rises with them. A scraper like this is modest on its own, but it becomes useful infrastructure when the numbers underneath the league start moving faster.
That is why the repo's restraint feels right. It does not try to explain the entire CBA, and it does not pretend to be a universal sports-finance platform. It solves one sharp problem well: get trustworthy team payroll data out of an unruly source and into a form that can actually support analysis.
What this project gets right, and what it still is
It is a niche utility, not a polished package. That is fine. The code shows an unusually mature understanding of hostile data surfaces, and it proves that a small repo can still embody good engineering judgment: score rather than guess, fall back rather than fail, clean before modeling, and export something useful.
That is the real lesson here. In a domain where the source HTML is not stable and the financial context keeps changing, resilience is the feature.





