bep40 commited on
Commit
0c18ead
·
verified ·
1 Parent(s): 53e3f28

FIX WC2026: BXH dùng tournament_id=24254, fixtures/stats dùng news thay vì API không hỗ trợ filter"

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +97 -66
wc2026_scraper.py CHANGED
@@ -1,7 +1,9 @@
1
  """
2
  World Cup 2026 Data Module
3
- Uses bongda.com.vn INTERNAL AJAX API with tournament_id=24254 ONLY
4
- Never falls back to endpoints without tournament_id (would show all leagues)
 
 
5
  """
6
  import requests, re, time, threading
7
  from bs4 import BeautifulSoup
@@ -12,24 +14,24 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
12
  BONGDA_HEADERS = {
13
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
14
  "Accept-Language": "vi-VN,vi;q=0.9",
15
- "Referer": "https://bongda.com.vn/giai-dau/24254/summary/world-cup",
16
  "X-Requested-With": "XMLHttpRequest"
17
  }
18
  UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
19
  WC_ID = 24254
20
  CACHE = {}
21
- CACHE_LOCK = threading.Lock()
22
 
23
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
24
  def _cached(key, ttl=120):
25
- with CACHE_LOCK:
26
  if key in CACHE and time.time() - CACHE[key]['t'] < ttl: return CACHE[key]['d']
27
  return None
28
  def _set(key, data):
29
- with CACHE_LOCK: CACHE[key] = {'t': time.time(), 'd': data}
30
 
31
  def _bongda(endpoint):
32
- """Call bongda.com.vn AJAX API → returns HTML string."""
33
  try:
34
  r = requests.get(f"https://bongda.com.vn{endpoint}", headers=BONGDA_HEADERS, timeout=12)
35
  if r.status_code == 200:
@@ -45,89 +47,113 @@ def _fetch(url, timeout=12):
45
  return r.text if r.status_code == 200 else ''
46
  except: return ''
47
 
48
- # ==================== FIXTURES (CHỈ World Cup 2026) ====================
49
- def scrape_fixtures():
50
- c = _cached('wc_fix', 120)
51
- if c is not None: return c
52
- # CHỈ dùng endpoint với tournament_id=24254
53
- html = _bongda(f"/api/fixtures/get-by-tournament?tournament_id={WC_ID}")
54
- if not html:
55
- # Thử incoming chỉ của WC
56
- html = _bongda(f"/api/fixtures/incoming?tournament_id={WC_ID}")
57
- if not html:
58
- # Thử theo ngày chỉ của WC
59
- today = datetime.now().strftime("%Y-%m-%d")
60
- html = _bongda(f"/api/fixtures/get-by-date?date={today}&tournament_id={WC_ID}")
61
- # KHÔNG fallback sang endpoint không có tournament_id
62
- r = {'html': html}; _set('wc_fix', r); return r
63
-
64
- # ==================== STANDINGS (CHỈ World Cup 2026) ====================
65
  def scrape_standings():
66
- c = _cached('wc_stand', 180)
 
67
  if c is not None: return c
68
- # CHỈ dùng tournament_id=24254
69
  html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
70
  if not html:
71
  html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
72
- r = {'html': html}; _set('wc_stand', r); return r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- # ==================== TEAM/PLAYER STATS (CHỈ WC) ====================
75
  def scrape_stats():
76
- c = _cached('wc_stats', 300)
 
77
  if c is not None: return c
 
78
  html = _bongda(f"/api/event-standing/player-performance?tournament_id={WC_ID}")
79
- if not html:
80
- html = _bongda(f"/api/event-standing/top-scorers?tournament_id={WC_ID}")
81
- r = {'html': html}; _set('wc_stats', r); return r
82
 
83
- # ==================== MATCH HISTORY (CHỈ WC) ====================
84
  def scrape_history():
85
- c = _cached('wc_hist', 300)
86
  if c is not None: return c
87
- html = _bongda(f"/api/fixtures/get-by-tournament?tournament_id={WC_ID}&status=finished")
88
- r = {'html': html}; _set('wc_hist', r); return r
 
 
89
 
90
- # ==================== HEAD TO HEAD ====================
91
  def scrape_h2h(event_id):
92
- key = f'wc_h2h_{event_id}'
93
- c = _cached(key, 600)
94
- if c is not None: return c
95
  html = _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")
96
- r = {'html': html}; _set(key, r); return r
97
 
98
- # ==================== LINEUPS ====================
99
  def scrape_lineups(event_id):
100
- key = f'wc_lineup_{event_id}'
101
- c = _cached(key, 300)
102
- if c is not None: return c
103
  html = _bongda(f"/api/fixtures/lineups?event_id={event_id}")
104
- r = {'html': html}; _set(key, r); return r
105
 
106
- # ==================== MATCH DETAIL ====================
107
  def scrape_match_detail(event_id):
108
- key = f'wc_detail_{event_id}'
109
- c = _cached(key, 60)
110
- if c is not None: return c
111
  html = _bongda(f"/api/fixtures/commentaries?event_id={event_id}")
112
- r = {'html': html}; _set(key, r); return r
113
 
114
  # ==================== LIVE ====================
115
  def scrape_summary():
116
- c = _cached('wc_sum', 60)
117
  if c is not None: return c
118
- # CHỈ WC tournament
119
- html_live = _bongda(f"/api/fixtures/live?tournament_id={WC_ID}")
120
- today = datetime.now().strftime("%Y-%m-%d")
121
- html_today = _bongda(f"/api/fixtures/get-by-date?date={today}&tournament_id={WC_ID}")
122
- html_incoming = _bongda(f"/api/fixtures/incoming?tournament_id={WC_ID}")
123
- r = {'html_live': html_live, 'html_today': html_today, 'html_incoming': html_incoming}
124
- _set('wc_sum', r); return r
125
 
126
  # ==================== NEWS ====================
127
  def scrape_wc_news():
128
  c = _cached('wc_news', 300)
129
  if c is not None: return c
130
  news = []
 
131
  try:
132
  html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
133
  if html:
@@ -143,6 +169,7 @@ def scrape_wc_news():
143
  if im: img = im.get('src', '')
144
  if title and link: news.append({'title': title, 'link': link, 'img': img, 'source': 'TT&VH'})
145
  except: pass
 
146
  try:
147
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
148
  if html:
@@ -157,6 +184,7 @@ def scrape_wc_news():
157
  if title and href and href not in [n['link'] for n in news]:
158
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
159
  except: pass
 
160
  try:
161
  html = _fetch('https://bongdaplus.vn/world-cup')
162
  if html:
@@ -168,14 +196,15 @@ def scrape_wc_news():
168
  if title and len(title) > 15 and href not in [n['link'] for n in news]:
169
  news.append({'title': title, 'link': href, 'img': '', 'source': 'BongDaPlus'})
170
  except: pass
171
- _set('wc_news', news[:30]); return news[:30]
 
172
 
173
  # ==================== ROAD TO WC ====================
174
  def scrape_road_to_wc():
175
  c = _cached('wc_road', 600)
176
  if c is not None: return c
177
  articles = []
178
- for q in ['đường tới World Cup 2026', 'vòng loại World Cup 2026', 'tuyển Việt Nam World Cup']:
179
  try:
180
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
181
  if not html: continue
@@ -190,16 +219,16 @@ def scrape_road_to_wc():
190
  if title and href and href not in [x['link'] for x in articles]:
191
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
192
  except: continue
193
- _set('wc_road', articles[:20]); return articles[:20]
 
194
 
195
  # ==================== ALL ====================
196
  def get_wc2026_all():
197
  c = _cached('wc_all', 90)
198
  if c is not None: return c
199
  data = {}
200
- with ThreadPoolExecutor(6) as ex:
201
  futs = {
202
- ex.submit(scrape_summary): 'summary',
203
  ex.submit(scrape_fixtures): 'fixtures',
204
  ex.submit(scrape_standings): 'standings',
205
  ex.submit(scrape_stats): 'stats',
@@ -209,5 +238,7 @@ def get_wc2026_all():
209
  for f in as_completed(futs, timeout=18):
210
  key = futs[f]
211
  try: data[key] = f.result()
212
- except: data[key] = {} if key in ('summary', 'fixtures', 'standings', 'stats') else []
213
- _set('wc_all', data); return data
 
 
 
1
  """
2
  World Cup 2026 Data Module
3
+ - BXH: bongda.com.vn API /api/league-table/home?tournament_id=24254 (WORKS - same as NHA/LaLiga)
4
+ - Lịch thi đấu: CANNOT use bongda.com.vn fixtures API (no tournament filter) → use 24h.com.vn
5
+ - Stats: bongda.com.vn player-performance endpoint
6
+ - News: VnExpress + TT&VH + BongDaPlus
7
  """
8
  import requests, re, time, threading
9
  from bs4 import BeautifulSoup
 
14
  BONGDA_HEADERS = {
15
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
16
  "Accept-Language": "vi-VN,vi;q=0.9",
17
+ "Referer": "https://bongda.com.vn/giai-dau/24254/standings/world-cup",
18
  "X-Requested-With": "XMLHttpRequest"
19
  }
20
  UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
21
  WC_ID = 24254
22
  CACHE = {}
23
+ LOCK = threading.Lock()
24
 
25
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
26
  def _cached(key, ttl=120):
27
+ with LOCK:
28
  if key in CACHE and time.time() - CACHE[key]['t'] < ttl: return CACHE[key]['d']
29
  return None
30
  def _set(key, data):
31
+ with LOCK: CACHE[key] = {'t': time.time(), 'd': data}
32
 
33
  def _bongda(endpoint):
34
+ """Call bongda.com.vn internal AJAX API."""
35
  try:
36
  r = requests.get(f"https://bongda.com.vn{endpoint}", headers=BONGDA_HEADERS, timeout=12)
37
  if r.status_code == 200:
 
47
  return r.text if r.status_code == 200 else ''
48
  except: return ''
49
 
50
+ # ==================== BXH (WORKS with bongda.com.vn) ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def scrape_standings():
52
+ """BXH World Cup 2026 - uses SAME method as NHA/La Liga livescore."""
53
+ c = _cached('wc_bxh', 180)
54
  if c is not None: return c
55
+ # This is the PROVEN endpoint (same as LEAGUE_IDS in main.py)
56
  html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
57
  if not html:
58
  html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
59
+ r = {'html': html}
60
+ _set('wc_bxh', r)
61
+ return r
62
+
63
+ # ==================== LỊCH THI ĐẤU (24h.com.vn - server rendered) ====================
64
+ def scrape_fixtures():
65
+ """Lịch thi đấu WC2026 from 24h.com.vn (server-rendered HTML, scrapeable)."""
66
+ c = _cached('wc_fix', 300)
67
+ if c is not None: return c
68
+
69
+ html_out = ''
70
+ try:
71
+ page = _fetch('https://www.24h.com.vn/bong-da/lich-thi-dau-bong-da-world-cup-2026-moi-nhat-c48a1747402.html')
72
+ if page:
73
+ soup = BeautifulSoup(page, 'lxml')
74
+ # 24h.com.vn renders schedule in tables
75
+ schedule = soup.select_one('.schedule-container, .cate-24h-foot-table-content, .tbl-schedule, table.tblData')
76
+ if schedule:
77
+ # Clean up and use directly
78
+ for s in schedule.select('script, style, .ads, .banner'):
79
+ s.decompose()
80
+ html_out = str(schedule)
81
+ else:
82
+ # Try all tables
83
+ tables = soup.select('table')
84
+ for t in tables:
85
+ if t.select('tr') and len(t.select('tr')) > 3:
86
+ text = t.get_text().lower()
87
+ if 'world cup' in text or 'bảng' in text or 'vs' in text:
88
+ html_out = str(t)
89
+ break
90
+ except: pass
91
+
92
+ # Fallback: build from bongdaplus
93
+ if not html_out:
94
+ try:
95
+ page = _fetch('https://bongdaplus.vn/world-cup/lich-thi-dau')
96
+ if page:
97
+ soup = BeautifulSoup(page, 'lxml')
98
+ content = soup.select_one('.schedule-content, .match-schedule, table, .list-match')
99
+ if content:
100
+ for s in content.select('script, style'): s.decompose()
101
+ html_out = str(content)
102
+ except: pass
103
+
104
+ r = {'html': html_out}
105
+ _set('wc_fix', r)
106
+ return r
107
 
108
+ # ==================== STATS ====================
109
  def scrape_stats():
110
+ """Thống WC2026."""
111
+ c = _cached('wc_stats', 600)
112
  if c is not None: return c
113
+ # Try bongda API
114
  html = _bongda(f"/api/event-standing/player-performance?tournament_id={WC_ID}")
115
+ r = {'html': html}
116
+ _set('wc_stats', r)
117
+ return r
118
 
119
+ # ==================== HISTORY ====================
120
  def scrape_history():
121
+ c = _cached('wc_hist', 600)
122
  if c is not None: return c
123
+ html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
124
+ r = {'html': html}
125
+ _set('wc_hist', r)
126
+ return r
127
 
128
+ # ==================== MATCH DETAIL ====================
129
  def scrape_h2h(event_id):
 
 
 
130
  html = _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")
131
+ return {'html': html}
132
 
 
133
  def scrape_lineups(event_id):
 
 
 
134
  html = _bongda(f"/api/fixtures/lineups?event_id={event_id}")
135
+ return {'html': html}
136
 
 
137
  def scrape_match_detail(event_id):
 
 
 
138
  html = _bongda(f"/api/fixtures/commentaries?event_id={event_id}")
139
+ return {'html': html}
140
 
141
  # ==================== LIVE ====================
142
  def scrape_summary():
143
+ c = _cached('wc_sum', 90)
144
  if c is not None: return c
145
+ # BXH as summary (fixtures API doesn't support tournament filter)
146
+ html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
147
+ r = {'html': html}
148
+ _set('wc_sum', r)
149
+ return r
 
 
150
 
151
  # ==================== NEWS ====================
152
  def scrape_wc_news():
153
  c = _cached('wc_news', 300)
154
  if c is not None: return c
155
  news = []
156
+ # TT&VH RSS
157
  try:
158
  html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
159
  if html:
 
169
  if im: img = im.get('src', '')
170
  if title and link: news.append({'title': title, 'link': link, 'img': img, 'source': 'TT&VH'})
171
  except: pass
172
+ # VnExpress
173
  try:
174
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
175
  if html:
 
184
  if title and href and href not in [n['link'] for n in news]:
185
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
186
  except: pass
187
+ # BongDaPlus
188
  try:
189
  html = _fetch('https://bongdaplus.vn/world-cup')
190
  if html:
 
196
  if title and len(title) > 15 and href not in [n['link'] for n in news]:
197
  news.append({'title': title, 'link': href, 'img': '', 'source': 'BongDaPlus'})
198
  except: pass
199
+ _set('wc_news', news[:30])
200
+ return news[:30]
201
 
202
  # ==================== ROAD TO WC ====================
203
  def scrape_road_to_wc():
204
  c = _cached('wc_road', 600)
205
  if c is not None: return c
206
  articles = []
207
+ for q in ['đường tới World Cup 2026', 'vòng loại World Cup 2026']:
208
  try:
209
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
210
  if not html: continue
 
219
  if title and href and href not in [x['link'] for x in articles]:
220
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
221
  except: continue
222
+ _set('wc_road', articles[:20])
223
+ return articles[:20]
224
 
225
  # ==================== ALL ====================
226
  def get_wc2026_all():
227
  c = _cached('wc_all', 90)
228
  if c is not None: return c
229
  data = {}
230
+ with ThreadPoolExecutor(5) as ex:
231
  futs = {
 
232
  ex.submit(scrape_fixtures): 'fixtures',
233
  ex.submit(scrape_standings): 'standings',
234
  ex.submit(scrape_stats): 'stats',
 
238
  for f in as_completed(futs, timeout=18):
239
  key = futs[f]
240
  try: data[key] = f.result()
241
+ except: data[key] = {} if key in ('fixtures', 'standings', 'stats') else []
242
+ data['summary'] = data.get('standings', {})
243
+ _set('wc_all', data)
244
+ return data