Spaces:
Running
Running
| """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): | |
| 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)) |