Skip to main content

A Python application to scrape and manage odds data from OddsPortal website.

Project description

OddsHarvester

Scrape sports betting odds from OddsPortal.com with ease

Extract upcoming & historical odds, plus community predictions, tipster profiles and per-match votes, across 11 sports, 100+ leagues, and dozens of betting markets.
Powered by Playwright browser automation. Output to JSON, CSV, or S3.


PyPI version License: MIT Build Status Scraper Health codecov Python


Quick Start

# Install
pip install oddsharvester

# Or clone & setup with uv
git clone https://github.com/jordantete/OddsHarvester.git && cd OddsHarvester
pip install uv && uv sync

# Scrape upcoming football matches
oddsharvester upcoming -s football -d 20250301 -m 1x2 --headless

# Scrape historical Premier League odds
oddsharvester historic -s football -l england-premier-league --season 2024-2025 -m 1x2 --headless

# Snapshot odds for matches in play right now
oddsharvester live -s tennis -m match_winner --headless

# Scrape community data (top predictions here; also --user profiles and --match-url votes)
oddsharvester community -s football --headless

Features

Feature Description
Upcoming Scrape upcoming matches Fetch odds and event details for upcoming sports matches by date or league
Historic Scrape historical odds Retrieve past odds and match results for any season
Live Snapshot in-play odds One-shot capture of matches in play, with live score, period and scrape timestamp
Community Scrape community data Top predictions, tipster profiles (stats + picks), and per-match community votes
Multi-market Advanced parsing Structured data: dates, teams, scores, venues, and per-bookmaker odds
Storage Flexible output JSON, CSV (local), or direct upload to AWS S3
Docker Container-ready Run seamlessly in Docker with environment variable configuration
Proxy Proxy support Route through SOCKS/HTTP proxies for geolocation and anti-blocking

Supported Sports & Markets

Sport Markets
⚽ Football 1x2 btts double_chance draw_no_bet over/under european_handicap asian_handicap
🎾 Tennis match_winner total_sets_over/under total_games_over/under asian_handicap correct_score
🏀 Basketball 1x2 moneyline asian_handicap over/under
🏉 Rugby League 1x2 home_away double_chance draw_no_bet over/under handicap
🏉 Rugby Union 1x2 home_away double_chance draw_no_bet over/under handicap
🏒 Ice Hockey 1x2 home_away double_chance draw_no_bet btts over/under
⚾ Baseball moneyline over/under
🏈 American Football 1x2 moneyline over/under asian_handicap
🤾 Handball 1x2 home_away double_chance draw_no_bet over/under handicap
🏐 Volleyball home_away total_sets_over/under total_points_over/under asian_handicap correct_score
🏏 Cricket home_away

Umbrella tokens (football): over_under and asian_handicap are umbrella market tokens — pass either as --market and it expands at scrape time to every line OddsPortal actually renders for that match (e.g. over_under_1_5_market, over_under_2_5_market, …), instead of listing each line by hand.

Cricket: OddsPortal does not currently publish a per-bookmaker odds table for cricket, so cricket scraping returns match metadata (teams, league, score, result) with an empty odds list. The home_away market is wired and will populate if OddsPortal adds cricket odds.

100+ leagues supported across all sports: Premier League, La Liga, Serie A, NBA, NFL, MLB, NHL, ATP/WTA Grand Slams, and many more.


CLI Usage

OddsHarvester has four commands: upcoming, historic, live, and community. They share most options, with a few command-specific ones.

oddsharvester upcoming

Scrape odds for upcoming matches — by date, by league, or by specific match URL.

# By date
oddsharvester upcoming -s football -d 20250301 -m 1x2 --headless

# By league (scrapes all upcoming matches for that league)
oddsharvester upcoming -s football -l england-premier-league -m 1x2,btts --headless

# Multiple leagues
oddsharvester upcoming -s football -l england-premier-league,spain-laliga -m 1x2 --headless

# Specific match URLs (repeat the flag; works for past matches too)
oddsharvester upcoming -s football --match-link "https://www.oddsportal.com/football/..." -m 1x2

# Preview mode (faster — best/highest odds only, no individual bookmakers)
oddsharvester upcoming -s football -d 20250301 -m over_under --preview-only --headless

# Only matches kicking off within the next 6 hours (fewer requests)
oddsharvester upcoming -s football -l england-premier-league -m 1x2 --kickoff-within-hours 6 --headless

oddsharvester historic

Scrape historical odds and results for past seasons.

# Single league & season
oddsharvester historic -s football -l england-premier-league --season 2022-2023 -m 1x2 --headless

# Current season
oddsharvester historic -s football -l england-premier-league --season current -m 1x2 --headless

# Limit pagination
oddsharvester historic -s football -l england-premier-league --season 2022-2023 -m 1x2 --max-pages 3 --headless

# Output as CSV
oddsharvester historic -s football -l england-premier-league --season 2024-2025 -m 1x2 -f csv -o premier_league_odds --headless

# Umbrella market — expands to every Over/Under line rendered on the page
oddsharvester historic -s football -l england-premier-league --season 2023-2024 --market over_under -f csv

oddsharvester live

Take a one-shot snapshot of matches currently in play, with per-bookmaker in-play odds.

# Every live match for a sport
oddsharvester live -s tennis -m match_winner --headless

# Restricted to one league
oddsharvester live -s football -l england-premier-league -m 1x2 --headless

# Re-sample one known match without reloading the listing
oddsharvester live -s football --match-link "https://www.oddsportal.com/football/..." -m 1x2 --headless

# Collect live match links only
oddsharvester live -s football --links-only -o live_links.json --headless

Each record carries the usual match metadata plus live context: live_period (the period marker exactly as the site displays it, so it is sport-specific: 4' or Half-time in football, 1st Set in tennis, 9th Inning in baseball), live_score_home, live_score_away, live_score_raw (keeps compound formats such as 0:1 (3:6, 4:2)), and scraped_at_utc, which is what makes a series of snapshots comparable.

Notes:

  • Zero live matches is a normal outcome: the command prints a message and exits 0 without writing a file.
  • Matches that end between listing and scrape are dropped, so a snapshot only ever contains genuinely live matches.
  • In-play bookmaker coverage is thinner than pre-match (often 2 to 4 bookmakers instead of 15 to 20) and varies by region.
  • --odds-history and --period are rejected: the in-play view exposes neither.
  • For repeated sampling, schedule the command externally (cron or similar). Keep at least 60 seconds between snapshots, and prefer --match-link to re-sample a known match without re-reading the listing. The command itself never refreshes a page, which keeps its request profile identical to upcoming.

oddsharvester community

Scrape OddsPortal Community data. community has three mutually-exclusive modes; exactly one is required:

  • Top predictions (--sport): the most-voted community picks for the next 7 days.
  • User profile (--user <username>): a tipster's stats, monthly performance and recent predictions.
  • Match community votes (--match-url <url>): per-market community vote volume for a single match.

Top predictions (--sport)

# Top predictions for a sport
oddsharvester community -s football --headless

# Write to a named JSON file
oddsharvester community -s football -f json -o top_predictions.json --headless

Each record contains the match (home_team, away_team, match_url, kickoff, plus the raw kickoff_text label kept as fallback when the date token fails to parse), the league (sport, country, league), the voted market, best odds per outcome (odds), the community vote split (community_votes_pct), and scraped_at.

  • OddsPortal surfaces ~10 picks per sport (no pagination) with rounded percentages.
  • Pre-match only: OddsPortal drops community data from finished-match pages, so build longitudinal datasets by scraping while matches are still upcoming.

User profile (--user <username>)

oddsharvester community --user BLAPRO --headless

Emits one record: header (username, roi_pct, member_since, country, privacy), the monthly statistics table (month, total_predictions, won, lost, plus_minus, roi_pct, incl. a Total row), and the rendered predictions batch. Each prediction has market, home_team/away_team, score (when finished), and a positional outcomes list of {odds, community_pct, picked} plus pick_odds. Most profiles are private: a private profile returns the header only (privacy: "private", empty stats/predictions) and exits 0.

Match community votes (--match-url <url>)

oddsharvester community --match-url "https://www.oddsportal.com/football/h2h/.../" --headless

Emits one record with per-market community vote volume: markets[] of {market, scope, handicap, betting_type_id, scope_id, total_votes, outcome_counts}, most-voted first, plus top_community_pick. Pre-match only (OddsPortal drops community data from finished matches).

Limitations: --match-url outcome vote counts are unlabeled: OddsPortal obfuscates the per-outcome ids, so only per-market volume, the count distribution, and the single aggregate pick are recoverable. --user captures the first rendered predictions batch (no deep pagination) and does not emit per-prediction win/loss (use the monthly stats table).

CLI Options Reference

Core Options

Option Short Description Default
--sport -s Sport to scrape (football, tennis, basketball, etc.) required
--date -d Target date in YYYYMMDD format
--league -l Comma-separated league slugs (e.g. england-premier-league)
--market -m Comma-separated markets (e.g. 1x2,btts)
--match-link Specific match URL (repeatable). Skips listing pages; --date/--league/--season are then ignored

--match-link usage: --sport is still required. Prefer upcoming over historic for arbitrary match URLs: match links bypass the listing pages entirely, so upcoming also works for matches already played, while historic would additionally demand a --season it never uses.

upcoming only: --date is required unless --league or --match-link is provided. --date and --league can be combined to filter the league's upcoming matches down to a specific calendar day. When combining both, the reference timezone for resolving the date is --timezone if provided, otherwise UTC. --kickoff-within-hours N keeps only matches starting within N hours from now; the filter runs during link collection, so far-off matches are never visited. It pairs with the default upcoming-only behaviour to bound the window on both sides, and uses --timezone (else UTC) as the reference clock.

historic only:

Option Description Default
--season Comma-separated seasons to scrape (YYYY, YYYY-YYYY, or current). Scraped as the cartesian product with --league. Duplicates are ignored. required
--max-pages Max number of result pages to scrape. Applies per league/season combo, not per run. unlimited

live only: no --date and no --season; the command always reads whatever is in play at the moment it runs. --league accepts at most one slug. --odds-history and --period are rejected outright, because the in-play view exposes neither. A --match-link given in classic form is normalized to its in-play URL automatically, so either form works. When every match fails to scrape the command exits non-zero, which is what lets a scheduled sampler tell a blocked run apart from a genuinely empty one.

Output Options

Option Short Description Default
--storage local or remote (S3) local
--format -f json or csv json
--output -o Output file path scraped_data
--append Append to the output file instead of overwriting it (--no-append to opt out explicitly) --no-append
--links-only Collect match links only, without scraping odds (--no-links-only to opt out explicitly) --no-links-only
--local-kickoff Add venue-local kickoff time to each record (--no-local-kickoff to opt out explicitly). Distinct from --timezone --no-local-kickoff

Breaking change: every output row now carries a season column. For odds rows it is inserted directly after match_date; --links-only rows have no match_date and carry season alongside the other link fields instead. It holds the scraped season for historic and is empty for upcoming and --match-link runs. Appending to a file produced by an earlier version yields a file with two different column layouts, so start a new output file rather than appending across the upgrade.

Browser & Scraping Options

Option Short Description Default
--headless Run browser in headless mode False
--concurrency -c Concurrent scraping tasks 3
--request-delay Delay (sec) between match requests 1.0
--user-agent Custom browser user agent
--locale Browser locale (e.g. fr-BE)
--timezone Browser timezone (e.g. Europe/Brussels)
--base-url Scrape a regional OddsPortal mirror instead of www.oddsportal.com (e.g. https://www.centroquote.it). Page structure is identical; only the domain changes. Regional mirrors may expose a different/larger set of bookmakers. Recommended: pair with --locale/--timezone matching the region. Env var: OH_BASE_URL.

Proxy Options

Option Description
--proxy-url Proxy URL (http://... or socks5://...). Repeatable — pass it multiple times to rotate per-match scraping round-robin across proxies. Each URL may embed credentials (scheme://user:pass@host:port).
--proxy-user Proxy username. Applies only when a single --proxy-url without embedded credentials is given; ignored (with a warning) if multiple proxies are passed.
--proxy-pass Proxy password. Same single-proxy restriction as --proxy-user.

Tip: For best results, match --locale and --timezone to your proxy's region.

Multi-proxy example — spread scraping across three proxies with embedded credentials:

oddsharvester historic --sport football --league england-premier-league --season 2013-2014 \
  --market 1x2 --concurrency 6 \
  --proxy-url http://user:pass@p1.example.com:8000 \
  --proxy-url http://user:pass@p2.example.com:8000 \
  --proxy-url http://user:pass@p3.example.com:8000

Matches are dispatched round-robin across the proxies; a proxy that fails 3 times in a row (navigation/rate-limit errors) is dropped from rotation and the run continues on the survivors.

Advanced Options

Option Description Default
--target-bookmaker Filter odds for a specific bookmaker
--odds-history Include historical odds movement per match False
--odds-format Odds display format Decimal Odds
--preview-only Fast mode — best/highest odds, no bookmaker details False
--bookies-filter Bookmaker filter: all, classic, or crypto all
--period Match period (sport-specific: full-time, halves, etc.) sport default
Preview Mode vs Full Mode
Aspect Full Mode Preview Mode
Speed Slower (interactive) Faster (passive)
Data All submarkets + bookmakers Visible submarkets + best odds
Bookmakers Individual bookmaker odds Best/highest odds only
Odds History Available Not available
Structure By bookmaker By submarket (best odds)

Preview mode (--preview-only) is useful for quick exploration, testing data format, or light monitoring with reduced resource usage. It reads the collapsed submarket row — the single best/highest price OddsPortal shows per line, not a per-bookmaker breakdown and not a computed average (see docs/agentic-gotchas.md §12).

Two-pass workflow: collect links, then scrape

For large runs it can be safer to collect all match links first, then scrape odds per link and re-run only the failures (see issue #75):

# Pass 1 - collect the season's match links (no odds scraped)
oddsharvester historic -s football -l england-premier-league --season 2022-2023 \
    --links-only -f csv -o links.csv

# Pass 2 - scrape odds per link (repeat --match-link; --append fills recovered failures)
oddsharvester historic -s football --season 2022-2023 -m 1x2 -f csv -o odds.csv --append \
    --match-link "https://www.oddsportal.com/football/england/premier-league-2022-2023/..."

Output rows contain match_link, sport, league, and season (date for upcoming; live emits neither), in the site's listing order. Options that only affect odds scraping (--market, --period, --odds-history, --preview-only, --target-bookmaker, --bookies-filter) are ignored when --links-only is set. --links-only cannot be combined with --match-link.

Bulk scraping: multiple leagues, multiple seasons

--season and --league both accept comma-separated lists. historic scrapes every combination as the cartesian product, sequentially, league outer and season inner, so output stays grouped and deterministic. --max-pages applies per combo, not per run.

# Several seasons of one league
oddsharvester historic --sport football --league england-premier-league \
    --season 2020-2021,2021-2022,2022-2023 --links-only --format csv --output links.csv

# Cartesian product: every league by every season
oddsharvester historic --sport football \
    --league england-premier-league,spain-laliga \
    --season 2021-2022,2022-2023 --links-only

# A league that changed season format mid-history: pass both formats and
# let the invalid pairs report zero
oddsharvester historic --sport football --league russia-premier-league \
    --season 2010,2010-2011,2011,2011-2012 --links-only

No pre-filtering is attempted to figure out which (league, season) pairs are valid before scraping them; a wrong-format pair (e.g. 2010 for a league that only ever used 2010-2011) is simply scraped and returns zero links. That is normal, not an error: OddsPortal returns HTTP 200 for a dead season URL, so the only way to tell a valid combo from an invalid one is to scrape it and count the results (see docs/agentic-gotchas.md §15). When more than one combo runs, an end-of-run table lists each league/season pair with its count, then reports how many combos returned nothing and how many errored. A zero-count combo does not affect the exit code; only errored combos are worth re-running.

Local kickoff time

--local-kickoff adds two fields to each record: venue_timezone (the venue's IANA timezone id) and match_date_venue_local (the kickoff converted to that timezone, with an explicit offset), e.g. 2022-05-01 16:00:00 BST+0100. match_date stays UTC; the two fields are additive and only appear when the flag is set.

Resolution is best-effort from the record's venue country/town. Single-timezone countries resolve by country; USA, Canada, Mexico, Brazil, Russia, and Australia resolve by host city instead. A venue that can't be resolved gets null for both fields.

Not compatible with --links-only (no match pages are visited, so there's no venue to resolve). Distinct from --timezone, which sets the browser's context timezone and does not affect the output fields.

If you --append onto an existing CSV file, the header is frozen on the first write, so start a fresh file when you turn the flag on.


Environment Variables

All CLI options can be set via environment variables — useful for Docker or CI/CD.

View all environment variables
Variable CLI Option Description
OH_SPORT --sport Sport to scrape
OH_LEAGUES --league Comma-separated leagues
OH_MARKETS --market Comma-separated markets
OH_STORAGE --storage Storage type (local/remote)
OH_FORMAT --format Output format (json/csv)
OH_FILE_PATH --output Output file path
OH_APPEND --append Append to the output file instead of overwriting
OH_LINKS_ONLY --links-only Collect match links only, without scraping odds
OH_LOCAL_KICKOFF --local-kickoff Add venue-local kickoff time to each record
OH_HEADLESS --headless Run in headless mode
OH_CONCURRENCY --concurrency Number of concurrent tasks
OH_REQUEST_DELAY --request-delay Delay between requests (sec)
OH_PROXY_URL --proxy-url Proxy server URL(s) — space-separated for multiple proxies
OH_PROXY_USER --proxy-user Proxy username
OH_PROXY_PASS --proxy-pass Proxy password
OH_USER_AGENT --user-agent Custom browser user agent
OH_LOCALE --locale Browser locale
OH_TIMEZONE --timezone Browser timezone ID
OH_BASE_URL --base-url Regional OddsPortal mirror base URL
export OH_SPORT=football
export OH_HEADLESS=true
export OH_PROXY_URL=http://proxy.example.com:8080

oddsharvester upcoming -d 20250301 -m 1x2

Installation

With pip (from PyPI)

pip install oddsharvester

From source (with uv)

git clone https://github.com/jordantete/OddsHarvester.git
cd OddsHarvester
pip install uv
uv sync
Manual setup (venv + pip or poetry)
python3 -m venv .venv
source .venv/bin/activate    # Unix/macOS
# .venv\Scripts\activate     # Windows

pip install . --use-pep517
# or: poetry install

Verify installation:

oddsharvester --help

Docker

# Build
docker build -t odds-harvester:local .

# Run (CLI args are appended to the ENTRYPOINT `python3 -m oddsharvester`)
docker run --rm odds-harvester:local upcoming -s football -d 20250301 -m 1x2 --headless

# Run and keep the JSON output on the host (mount a volume + use -o)
# On macOS+colima, prefer a path under $HOME (e.g. $PWD); /tmp is not shared by default.
docker run --rm -v "$PWD/_docker_out:/out" odds-harvester:local \
  upcoming -s football -d 20250301 -m 1x2 --headless -o /out/result.json

# Or with environment variables
docker run --rm \
  -e OH_SPORT=football \
  -e OH_HEADLESS=true \
  odds-harvester:local upcoming -d 20250301 -m 1x2

Contributing

Contributions are welcome! Submit an issue or pull request. Please follow the project's coding standards and include clear descriptions for any changes.

License

MIT License

Disclaimer

This package is intended for educational purposes only. The author is not affiliated with or endorsed by oddsportal.com. Use responsibly and ensure compliance with their terms of service and applicable laws.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

oddsharvester-0.7.0.tar.gz (124.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

oddsharvester-0.7.0-py3-none-any.whl (135.8 kB view details)

Uploaded Python 3

File details

Details for the file oddsharvester-0.7.0.tar.gz.

File metadata

  • Download URL: oddsharvester-0.7.0.tar.gz
  • Upload date:
  • Size: 124.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for oddsharvester-0.7.0.tar.gz
Algorithm Hash digest
SHA256 aeb85045f6bc1a02bfac26c604894907c6b540503880643430372929aad302a2
MD5 8a4dda9e63e4e480bb441e164914adbc
BLAKE2b-256 6aad53840914eaacbe2ad675c4f2175f7284dfd345a1764c1478eb1fb7235831

See more details on using hashes here.

Provenance

The following attestation bundles were made for oddsharvester-0.7.0.tar.gz:

Publisher: release.yml on jordantete/OddsHarvester

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file oddsharvester-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: oddsharvester-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 135.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for oddsharvester-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3a6d829bead116fd8797153cc53ff0d0ee6a039304b78451ed98b25fccdc131a
MD5 f66b5ede66c6db64b4b1be8035ccab4b
BLAKE2b-256 3fafef970365cf1534bbe196d7b5600e65c30188dd6f92aa4d5d8168fd608bdc

See more details on using hashes here.

Provenance

The following attestation bundles were made for oddsharvester-0.7.0-py3-none-any.whl:

Publisher: release.yml on jordantete/OddsHarvester

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page