#!/usr/bin/env python3 """Scrape FC Bizoni UH data from 2fl-v.futsalliga.cz into the club.json shape the site frontend expects (same as facr.tdvorak.dev API output). Usage: python tools/scrape_futsalliga.py [--out data/club.json] """ import json import re import sys import urllib.request from datetime import datetime, timezone sys.stdout.reconfigure(encoding="utf-8", errors="replace") BASE = "https://2fl-v.futsalliga.cz" SEASON = "2027" # 2026/2027 TEAM_ID = "60" # FC Bizoni Uherske Hradiste CLUB_NAME = "FC Bizoni Uherské Hradiště" COMP_NAME = "2. Futsal liga - východ 2026/2027" CLUB_LOGO = "/img/logo.png" UA = {"User-Agent": "Mozilla/5.0 (bizoni-site-scraper)"} def fetch(url): req = urllib.request.Request(url, headers=UA) with urllib.request.urlopen(req, timeout=30) as r: return r.read().decode("utf-8", "replace") def strip_tags(s): return re.sub(r"<[^>]+>", "", s) def clean(s): return re.sub(r"\s+", " ", strip_tags(s)).strip() def team_logo(content): m = re.search(r'(?:data-src|src)="(https?://[^"]+)"', content) return m.group(1) if m else "" def is_bizoni(name): return "bizoni" in name.lower() def parse_matches(html_text): # Each date group:

...weekday D. M. YYYY

followed by match rows h2re = re.compile(r'

]*>.*?

', re.S) headers = list(h2re.finditer(html_text)) matches = [] for i, h in enumerate(headers): date_m = re.search(r"(\d{1,2})\.\s*(\d{1,2})\.\s*(\d{4})", strip_tags(h.group(0))) if not date_m: continue d, mo, y = int(date_m.group(1)), int(date_m.group(2)), int(date_m.group(3)) section = html_text[h.end():headers[i + 1].start() if i + 1 < len(headers) else len(html_text)] # each match row starts with the time anchor rowre = re.compile( r']*>\s*
\s*([0-9:]+)\s*
', re.S, ) rows = list(rowre.finditer(section)) for j, row in enumerate(rows): seg = section[row.end():rows[j + 1].start() if j + 1 < len(rows) else len(section)] match_id, time_str = row.group(1), row.group(2) teams = re.findall(r'
(.*?)
', seg, re.S) if len(teams) < 2: continue home, away = clean(teams[0]), clean(teams[1]) home_logo, away_logo = team_logo(teams[0]), team_logo(teams[1]) score = "" sm = re.search(r'match-results[^>]*>\s*([^<]*?)\s*', seg, re.S) if sm: sc = re.search(r"(\d+)\s*:\s*(\d+)", sm.group(1)) if sc: score = f"{sc.group(1)}:{sc.group(2)}" vm = re.search(r'
\s*([^<]*?)\s*
', seg, re.S) venue = vm.group(1).strip() if vm else "" if is_bizoni(home): home_logo = CLUB_LOGO if is_bizoni(away): away_logo = CLUB_LOGO matches.append({ "date_time": f"{d:02d}.{mo:02d}.{y:04d} {time_str}", "home": home, "home_id": TEAM_ID if is_bizoni(home) else "", "home_logo_url": home_logo, "away": away, "away_id": TEAM_ID if is_bizoni(away) else "", "away_logo_url": away_logo, "score": score, "venue": venue, "match_id": match_id, "report_url": f"{BASE}/match/{match_id}", "facr_link": f"{BASE}/match/{match_id}", }) return matches def parse_standings(html_text): tm = re.search(r']*standingsTable[^>]*>(.*?)', html_text, re.S) if not tm: return [] rows = [] for tr in re.findall(r"(.*?)", tm.group(1), re.S): cells = re.findall(r"]*>(.*?)", tr, re.S) if len(cells) < 8: continue team = clean(cells[1]) rows.append({ "rank": clean(cells[0]), "team": team, "team_id": TEAM_ID if is_bizoni(team) else "", "team_logo_url": CLUB_LOGO if is_bizoni(team) else team_logo(cells[1]), "played": clean(cells[2]), "wins": clean(cells[3]), "draws": clean(cells[4]), "losses": clean(cells[5]), "score": clean(cells[6]), "points": clean(cells[7]), }) return rows def main(): out = sys.argv[sys.argv.index("--out") + 1] if "--out" in sys.argv else None matches_html = fetch(f"{BASE}/matches/{SEASON}?team={TEAM_ID}") standings_html = fetch(f"{BASE}/standings/{SEASON}") matches = parse_matches(matches_html) table = parse_standings(standings_html) comp = { "id": SEASON, "code": "2FL-V", "name": COMP_NAME, "team_count": str(len(table)), "matches_link": f"{BASE}/matches/{SEASON}?team={TEAM_ID}", "matches": matches, } detail = { "name": CLUB_NAME, "club_id": TEAM_ID, "club_type": "futsal", "url": f"{BASE}/team/{TEAM_ID}", "logo_url": CLUB_LOGO, "address": "", "category": "Futsal", "competitions": [comp], } table_obj = { "name": CLUB_NAME, "club_id": TEAM_ID, "club_type": "futsal", "logo_url": CLUB_LOGO, "competitions": [{ "id": SEASON, "code": "2FL-V", "name": COMP_NAME, "team_count": str(len(table)), "matches_link": comp["matches_link"], "table": {"overall": table}, }], } combined = { "fetched_at": datetime.now(timezone.utc).isoformat(), "club_detail": detail, "club_table": table_obj, } print(f"matches={len(matches)} standings_rows={len(table)}") for m in matches[:4]: print(f" {m['date_time']} {m['home']} {m['score'] or '-:-'} {m['away']} @ {m['venue']}") print(" ...") for m in matches[-2:]: print(f" {m['date_time']} {m['home']} {m['score'] or '-:-'} {m['away']} @ {m['venue']}") if out: with open(out, "w", encoding="utf-8") as f: json.dump(combined, f, ensure_ascii=False, indent=2) print(f"wrote {out}") if __name__ == "__main__": main()