"""VNEWS — Bongda Proxy Endpoint (for fast match detail loading)""" import requests from bs4 import BeautifulSoup import re import json # Import-safe parsing functions def _cl(s): return re.sub(r'\s+', ' ', str(s or '')).strip() def _normalize_time(raw): t = _cl(raw) t = re.sub(r"(\d+)'\s*\+(\d+)", r"\1+\2'", t) t = t.replace("''", "'") return t def scrape_match_html(event_id, url=None): """Fast scrape bongda.com.vn for match detail - no external CORS needed.""" result = {"event_id": event_id, "found": False, "sections": []} # Fetch HTML headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Referer": "https://bongda.com.vn/", } html = None urls_to_try = [url] if url else [] urls_to_try += [ f"https://bongda.com.vn/tran-dau/{event_id}/centre/", f"https://bongda.com.vn/tran-dau/{event_id}/preview/", ] for u in urls_to_try: if not u: continue try: resp = requests.get(u, headers=headers, timeout=15, allow_redirects=True) if resp.status_code == 200 and len(resp.text) > 1000: html = resp.text break except Exception: continue if not html: return result try: soup = BeautifulSoup(html, 'html.parser') info = {} # Teams + score tel = soup.select_one('.teams') if tel: he = tel.select_one('.team.home') if he: ne = he.select_one('p:not(.logo)') or he.find('p') if ne: info['home_team'] = _cl(ne.get_text()) lo = he.select_one('img') if lo: info['home_logo'] = lo.get('src', '') ae = tel.select_one('.team.away') if ae: ne = ae.select_one('p:not(.logo)') or ae.find('p') if ne: info['away_team'] = _cl(ne.get_text()) lo = ae.select_one('img') if lo: info['away_logo'] = lo.get('src', '') sc = tel.select_one('.score') if sc: parts = [_cl(p.get_text()) for p in sc.select('p')] if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}" lb = sc.select_one('.label') if lb: info['status_label'] = _cl(lb.get_text()) if info.get('home_team') and info.get('away_team'): result['info'] = info result['found'] = True result['sections'].append('info') else: return result # Events parsing events = [] events_div = soup.select_one('.events') if events_div: period = '' for child in events_div.children: if not hasattr(child, 'name') or not child.name: continue cls = ' '.join(child.get('class', [])) if 'period' in cls: h2 = child.find('h2') if h2: period = _cl(h2.get_text()) for ev in child.children: if not hasattr(ev, 'name') or not ev.name: continue ev_cls = ' '.join(ev.get('class', [])) if 'event' not in ev_cls: continue ev_data = { 'team': 'home' if 'home' in ev_cls else 'away', 'period': period, 'type': 'unknown', 'time': '', } # Type detection type_el = ev.select_one('.event-type') if type_el: if type_el.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard' elif type_el.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard' elif type_el.select_one('[class*="goal"]'): ev_data['type'] = 'goal' elif type_el.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution' players_el = ev.select_one('.players') if players_el: time_el = players_el.select_one('.event-time') if time_el: ev_data['time'] = _normalize_time(time_el.get_text()) # Parse player names text = _cl(players_el.get_text(' ', strip=True).replace(ev_data['time'], '').strip()) ev_data['players'] = text if ev_data['type'] == 'goal': words = text.split() if len(words) >= 2: ev_data['scorer'] = ' '.join(words[:2]) elif len(words) == 1: ev_data['scorer'] = words[0] elif ev_data['type'] == 'substitution': words = text.split() if len(words) >= 4: ev_data['player_out'] = ' '.join(words[:len(words)//2]) ev_data['player_in'] = ' '.join(words[len(words)//2:]) elif ev_data['type'] in ('redcard', 'yellowcard'): ev_data['player'] = text events.append(ev_data) if events: result['events'] = events result['sections'].append('events') # Prediction pred = soup.select_one('.prediction-card') if pred: pred_data = {} team_info = pred.select_one('.team-info') if team_info: teams = team_info.select('.team') if len(teams) >= 2: pred_data['home_name'] = _cl(teams[0].select_one('.team-name').get_text() or '') if teams[0].select_one('.team-name') else '' pred_data['away_name'] = _cl(teams[1].select_one('.team-name').get_text() or '') if teams[1].select_one('.team-name') else '' divider = team_info.select_one('.divider') if divider: pred_data['result'] = _cl(divider.get_text()) vc = pred.select_one('.vote-count') if vc: pred_data['vote_count'] = _cl(vc.get_text()) result['prediction'] = pred_data # Recent matches recent = [] ml = soup.select_one('.matches-list') if ml: for item in ml.select('.match-detail, .match-item, li'): de = item.select_one('.date, .time') le = item.select_one('.league') he = item.select_one('.home, .team-home') ae = item.select_one('.away, .team-away') se = item.select_one('.score, .result') if he or ae: recent.append({ 'date': _cl(de.get_text()) if de else '', 'league': _cl(le.get_text()) if le else '', 'home': _cl(he.get_text()) if he else '', 'away': _cl(ae.get_text()) if ae else '', 'score': _cl(se.get_text()) if se else 'vs', }) if recent: result['recent_matches'] = recent result['sections'].append('recent') # H2H stats API try: api_headers = { "User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/", } r = requests.get( f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_headers, timeout=10 ) if r.status_code == 200: ad = r.json() if ad.get('status') == 'success' and ad.get('html'): asp = BeautifulSoup(ad['html'], 'html.parser') ast = {} for row in asp.select('li, tr'): cells = row.select('td, span, p') if len(cells) >= 3: lb = _cl(cells[0].get_text()) if lb: ast[lb] = {'home': _cl(cells[1].get_text()), 'away': _cl(cells[2].get_text())} if ast: result['h2h_stats_parsed'] = ast result['sections'].append('h2h_stats') except Exception: pass except Exception as e: result['error'] = str(e) return result # FastAPI endpoint (to be added to app_v2_entry.py) from fastapi import Query from fastapi.responses import JSONResponse def add_bongda_proxy_endpoint(app): @app.get('/api/proxy/bongda') def proxy_bongda(event_id: int = Query(default=None), url: str = Query(default=None)): """Proxy bongda.com.vn match data - fast server-side scraping.""" if event_id is None: return JSONResponse({'error': 'event_id required'}, status_code=400) return JSONResponse(scrape_match_html(event_id, url))