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.

7 min read • View on GitHub • More from clementvg-create

A wide editorial scene shows a cluttered sports webpage on one side and a clean payroll workbook on the other. Arrows carry tangled links, odd table artifacts, and page noise into a neat stack of franchise sheets, explaining how the project converts messy HTML into decision-ready salary data.
The repo is not just scraping pages. It is translating unstable web markup into a workbook an analyst can actually use.
Key Takeaways

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 clever part is not a single string match. It is a small scoring system that keeps working when the site's layout changes.

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.

SignalWhat it addsWhy it matters
Visible text matchHigh confidenceThe link likely names the team directly.
Href matchMedium confidenceThe URL may expose the right destination even when the text is abbreviated.
Nearby year signalContext boostThe function can prefer the page tied to the target season.
Partial matchesTie-breakingThe 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.

ModeStrengthTrade-off
requestsFast and lightweightCan miss content that appears after JavaScript runs.
SeleniumMore complete page renderingSlower and heavier to maintain.
Hybrid strategyBalances speed and resilienceRequires 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

A close-up workshop scene shows a hand sorting a stream of table cells into separate trays for clean data, discard, and summary rows. Among the cells are currency values and one strange audio-player fragment, showing why the scraper has to strip artifacts before analysis can begin.
The unglamorous part of the pipeline is also the most important. If the cleaning rules fail, the workbook becomes unreliable.
_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

Cathy Engelbert, WNBA Commissioner · WNBA and Players' Union Reach Agreement on New CBA (2026)

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 scrapeWorkbook outputWhy the difference matters
One page at a timeMulti-sheet structureLets each franchise live in its own tab.
Unstable HTMLStructured cellsMakes review and recalculation safer.
Ad hoc extractionDecision-ready artifactSupports 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.