bep40 commited on
Commit
46d9fb4
·
verified ·
1 Parent(s): 9a37332

Fix WC2026 scraper: use bongda.com.vn internal AJAX API (same as livescore) with tournament_id=24254

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +121 -235
wc2026_scraper.py CHANGED
@@ -1,17 +1,23 @@
1
  """
2
- World Cup 2026 Data Module - WORKING sources
3
- bongda.com.vn is a JavaScript SPA (empty with requests), so we use:
4
- - Tin tức: VnExpress, Dân Trí, BongDaPlus RSS/search
5
- - Lịch thi đấu + BXH: livescore.com scraping (server-rendered)
6
- - Đường tới WC: VnExpress + news search
7
- All cached with short TTL for LIVE updates.
8
  """
9
  import requests, re, time, json, os, threading
10
  from bs4 import BeautifulSoup
11
  from urllib.parse import quote
 
12
  from concurrent.futures import ThreadPoolExecutor, as_completed
13
 
14
- UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'}
 
 
 
 
 
 
 
15
  CACHE = {}
16
  CACHE_LOCK = threading.Lock()
17
 
@@ -28,6 +34,18 @@ def _set(key, data):
28
  with CACHE_LOCK:
29
  CACHE[key] = {'t': time.time(), 'd': data}
30
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def _fetch(url, timeout=12):
32
  try:
33
  r = requests.get(url, headers=UA, timeout=timeout, allow_redirects=True)
@@ -38,6 +56,76 @@ def _fetch(url, timeout=12):
38
  pass
39
  return ''
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  # ==================== NEWS ====================
42
  def scrape_wc_news():
43
  """Get World Cup 2026 news from multiple VN sources."""
@@ -46,30 +134,30 @@ def scrape_wc_news():
46
 
47
  news = []
48
 
49
- # 1. VnExpress World Cup RSS
50
  try:
51
- html = _fetch('https://vnexpress.net/rss/the-thao.rss')
52
  if html:
53
  soup = BeautifulSoup(html, 'xml')
54
- for item in soup.find_all('item')[:30]:
55
  title = _clean(item.find('title').get_text() if item.find('title') else '')
56
  link = _clean(item.find('link').get_text() if item.find('link') else '')
57
- if title and link and any(kw in title.lower() for kw in ['world cup', 'wc 2026', 'fifa', 'vòng chung kết']):
58
- desc = item.find('description')
59
- img = ''
60
- if desc:
61
- ds = BeautifulSoup(desc.get_text(), 'lxml')
62
- im = ds.find('img')
63
- if im: img = im.get('src', '')
64
- news.append({'title': title, 'link': link, 'img': img, 'source': 'VnExpress'})
65
  except: pass
66
 
67
- # 2. Search VnExpress for WC2026
68
  try:
69
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
70
  if html:
71
  soup = BeautifulSoup(html, 'lxml')
72
- for art in soup.select('article.item-news')[:12]:
73
  a = art.select_one('h2 a, h3 a')
74
  if not a: continue
75
  title = _clean(a.get('title', '') or a.get_text())
@@ -80,25 +168,12 @@ def scrape_wc_news():
80
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
81
  except: pass
82
 
83
- # 3. Dân Trí search
84
- try:
85
- html = _fetch(f'https://dantri.com.vn/tim-kiem/World+Cup+2026.htm')
86
- if html:
87
- soup = BeautifulSoup(html, 'lxml')
88
- for a in soup.select('h3 a[href], .article-title a[href]')[:10]:
89
- title = _clean(a.get_text())
90
- href = a.get('href', '')
91
- if not href.startswith('http'): href = 'https://dantri.com.vn' + href
92
- if title and len(title) > 15 and href not in [n['link'] for n in news]:
93
- news.append({'title': title, 'link': href, 'img': '', 'source': 'Dân Trí'})
94
- except: pass
95
-
96
- # 4. BongDaPlus
97
  try:
98
  html = _fetch('https://bongdaplus.vn/world-cup')
99
  if html:
100
  soup = BeautifulSoup(html, 'lxml')
101
- for a in soup.select('h2 a[href], h3 a[href], .title a[href]')[:10]:
102
  title = _clean(a.get_text())
103
  href = a.get('href', '')
104
  if not href.startswith('http'): href = 'https://bongdaplus.vn' + href
@@ -106,211 +181,22 @@ def scrape_wc_news():
106
  news.append({'title': title, 'link': href, 'img': '', 'source': 'BongDaPlus'})
107
  except: pass
108
 
109
- # 5. 24h.com.vn World Cup
110
  try:
111
  html = _fetch('https://www.24h.com.vn/world-cup-2026-c860.html')
112
  if html:
113
  soup = BeautifulSoup(html, 'lxml')
114
- for a in soup.select('a.news-title, h2 a, h3 a')[:8]:
115
- title = _clean(a.get_text())
116
  href = a.get('href', '')
117
  if not href.startswith('http'): href = 'https://www.24h.com.vn' + href
118
- if title and len(title) > 15 and 'world cup' in title.lower() and href not in [n['link'] for n in news]:
119
  news.append({'title': title, 'link': href, 'img': '', 'source': '24h'})
120
  except: pass
121
 
122
  _set('wc_news', news[:30])
123
  return news[:30]
124
 
125
- # ==================== FIXTURES (from livescore.com) ====================
126
- def scrape_fixtures():
127
- """Scrape WC2026 fixtures from livescore.com (server-rendered)."""
128
- c = _cached('wc_fixtures', 120)
129
- if c is not None: return c
130
-
131
- matches = []
132
- # livescore.com has server-rendered pages
133
- urls = [
134
- 'https://www.livescore.com/en/football/world-cup-2026/fixtures/',
135
- 'https://www.livescore.com/en/football/world-cup-2026/results/',
136
- ]
137
-
138
- for url in urls:
139
- html = _fetch(url)
140
- if not html: continue
141
- soup = BeautifulSoup(html, 'lxml')
142
-
143
- current_date = ''
144
- for el in soup.select('[data-testid], .match, .event, div[class*="match"], div[class*="event"]'):
145
- # Try to extract match data
146
- teams = el.select('[class*="team"], [class*="Team"]')
147
- score_el = el.select_one('[class*="score"], [class*="Score"]')
148
- time_el = el.select_one('[class*="time"], [class*="Time"], [class*="status"]')
149
-
150
- if len(teams) >= 2:
151
- home = _clean(teams[0].get_text())
152
- away = _clean(teams[1].get_text())
153
- score = _clean(score_el.get_text()) if score_el else ''
154
- match_time = _clean(time_el.get_text()) if time_el else ''
155
- if home and away:
156
- matches.append({
157
- 'home': home, 'away': away,
158
- 'score': score, 'date': match_time,
159
- 'status': 'live' if any(x in match_time.lower() for x in ['live', "'", 'ht']) else '',
160
- 'group': ''
161
- })
162
-
163
- # Fallback: parse from thethao247.vn (server-rendered)
164
- if not matches:
165
- try:
166
- html = _fetch('https://thethao247.vn/world-cup/426-lich-thi-dau-world-cup-2026-d399919.html')
167
- if html:
168
- soup = BeautifulSoup(html, 'lxml')
169
- for tr in soup.select('table tr'):
170
- tds = tr.select('td')
171
- if len(tds) >= 3:
172
- matches.append({
173
- 'home': _clean(tds[0].get_text()),
174
- 'score': _clean(tds[1].get_text()),
175
- 'away': _clean(tds[2].get_text()),
176
- 'date': _clean(tds[3].get_text()) if len(tds) > 3 else '',
177
- 'status': '', 'group': ''
178
- })
179
- except: pass
180
-
181
- # Fallback 2: 24h.com.vn fixtures
182
- if not matches:
183
- try:
184
- html = _fetch('https://www.24h.com.vn/bong-da/lich-thi-dau-bong-da-world-cup-2026-moi-nhat-c48a1747402.html')
185
- if html:
186
- soup = BeautifulSoup(html, 'lxml')
187
- for row in soup.select('.schedule-item, .match-item, tr'):
188
- teams = row.select('.team, td')
189
- if len(teams) >= 2:
190
- home = _clean(teams[0].get_text())
191
- away = _clean(teams[-1].get_text())
192
- if home and away and len(home) < 40:
193
- matches.append({'home': home, 'away': away, 'score': '', 'date': '', 'status': '', 'group': ''})
194
- except: pass
195
-
196
- _set('wc_fixtures', matches[:60])
197
- return matches[:60]
198
-
199
- # ==================== STANDINGS ====================
200
- def scrape_standings():
201
- """Scrape WC2026 standings from livescore.com."""
202
- c = _cached('wc_standings', 180)
203
- if c is not None: return c
204
-
205
- groups = {}
206
-
207
- # Try livescore.com group standings
208
- group_letters = 'ABCDEFGHIJKL'
209
- for letter in group_letters:
210
- url = f'https://www.livescore.com/en/football/world-cup-2026/group-{letter.lower()}/standings/'
211
- html = _fetch(url, timeout=8)
212
- if not html: continue
213
- soup = BeautifulSoup(html, 'lxml')
214
-
215
- teams = []
216
- for row in soup.select('tr, [class*="row"], [class*="Row"]'):
217
- cells = row.select('td, [class*="cell"], [class*="Cell"]')
218
- if len(cells) >= 3:
219
- # Find team name (longest text that's not a number)
220
- texts = [_clean(c.get_text()) for c in cells]
221
- team_name = ''
222
- pts = ''
223
- played = ''
224
- for t in texts:
225
- if len(t) > 2 and not t.isdigit() and len(t) > len(team_name):
226
- team_name = t
227
- if texts:
228
- pts = texts[-1] if texts[-1].isdigit() else ''
229
- for t in texts[1:4]:
230
- if t.isdigit() and not played:
231
- played = t
232
- if team_name:
233
- teams.append({'team': team_name, 'played': played, 'pts': pts})
234
-
235
- if teams:
236
- groups[f'Bảng {letter}'] = teams
237
-
238
- # Fallback: get from 24h.com.vn BXH page
239
- if not groups:
240
- try:
241
- html = _fetch('https://www.24h.com.vn/bong-da/bang-xep-hang-bong-da-world-cup-2026-moi-nhat-c48a1747408.html')
242
- if html:
243
- soup = BeautifulSoup(html, 'lxml')
244
- current_group = ''
245
- for el in soup.select('h3, h4, table, tr'):
246
- if el.name in ('h3', 'h4'):
247
- text = _clean(el.get_text())
248
- if 'bảng' in text.lower() or 'group' in text.lower():
249
- current_group = text
250
- if current_group not in groups:
251
- groups[current_group] = []
252
- elif el.name == 'tr' and current_group:
253
- tds = el.select('td')
254
- if len(tds) >= 3:
255
- texts = [_clean(td.get_text()) for td in tds]
256
- team = max([t for t in texts if t and not t.isdigit()], key=len, default='')
257
- pts = texts[-1] if texts and texts[-1].isdigit() else ''
258
- if team:
259
- groups[current_group].append({'team': team, 'played': '', 'pts': pts})
260
- except: pass
261
-
262
- _set('wc_standings', groups)
263
- return groups
264
-
265
- # ==================== STATS ====================
266
- def scrape_stats():
267
- """Get WC2026 stats - top scorers, cards etc."""
268
- c = _cached('wc_stats', 600)
269
- if c is not None: return c
270
-
271
- stats = {'top_scorers': [], 'top_assists': []}
272
-
273
- # World Cup hasn't started yet (June 2026), so stats may be empty
274
- # Try to get from livescore
275
- html = _fetch('https://www.livescore.com/en/football/world-cup-2026/top-scorers/')
276
- if html:
277
- soup = BeautifulSoup(html, 'lxml')
278
- for row in soup.select('tr, [class*="row"]')[:15]:
279
- cells = row.select('td, [class*="cell"]')
280
- if len(cells) >= 2:
281
- texts = [_clean(c.get_text()) for c in cells]
282
- player = max([t for t in texts if t and not t.isdigit() and len(t) > 2], key=len, default='')
283
- goals = ''
284
- for t in reversed(texts):
285
- if t.isdigit():
286
- goals = t; break
287
- if player and goals:
288
- stats['top_scorers'].append({'player': player, 'team': '', 'goals': goals})
289
-
290
- _set('wc_stats', stats)
291
- return stats
292
-
293
- # ==================== SUMMARY ====================
294
- def scrape_summary():
295
- """Live/upcoming WC2026 matches."""
296
- c = _cached('wc_summary', 60)
297
- if c is not None: return c
298
-
299
- data = {'live_matches': [], 'upcoming': [], 'recent_results': []}
300
- fixtures = scrape_fixtures()
301
-
302
- for m in fixtures:
303
- status = (m.get('status', '') or '').lower()
304
- if 'live' in status or "'" in m.get('date', ''):
305
- data['live_matches'].append(m)
306
- elif m.get('score') and m['score'] != 'vs' and '-' in m['score']:
307
- data['recent_results'].append(m)
308
- else:
309
- data['upcoming'].append(m)
310
-
311
- _set('wc_summary', data)
312
- return data
313
-
314
  # ==================== ROAD TO WORLD CUP ====================
315
  def scrape_road_to_wc():
316
  """Get 'Đường tới World Cup 2026' articles."""
@@ -318,14 +204,14 @@ def scrape_road_to_wc():
318
  if c is not None: return c
319
 
320
  articles = []
321
- queries = ['đường tới World Cup 2026', 'vòng loại World Cup 2026', 'tuyển Việt Nam World Cup']
322
 
323
  for q in queries:
324
  try:
325
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
326
  if not html: continue
327
  soup = BeautifulSoup(html, 'lxml')
328
- for art in soup.select('article.item-news')[:6]:
329
  a = art.select_one('h2 a, h3 a')
330
  if not a: continue
331
  title = _clean(a.get('title', '') or a.get_text())
@@ -336,9 +222,9 @@ def scrape_road_to_wc():
336
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
337
  except: continue
338
 
339
- # Dân Trí
340
  try:
341
- html = _fetch('https://dantri.com.vn/tim-kiem/v%C3%B2ng+lo%E1%BA%A1i+World+Cup+2026.htm')
342
  if html:
343
  soup = BeautifulSoup(html, 'lxml')
344
  for a in soup.select('h3 a[href]')[:6]:
@@ -359,7 +245,7 @@ def get_wc2026_all():
359
  if c is not None: return c
360
 
361
  data = {}
362
- with ThreadPoolExecutor(5) as ex:
363
  futs = {
364
  ex.submit(scrape_summary): 'summary',
365
  ex.submit(scrape_fixtures): 'fixtures',
@@ -368,12 +254,12 @@ def get_wc2026_all():
368
  ex.submit(scrape_wc_news): 'news',
369
  ex.submit(scrape_road_to_wc): 'road',
370
  }
371
- for f in as_completed(futs, timeout=20):
372
  key = futs[f]
373
  try:
374
  data[key] = f.result()
375
  except:
376
- data[key] = [] if key in ('fixtures', 'news', 'road') else {}
377
 
378
  _set('wc_all', data)
379
  return data
 
1
  """
2
+ World Cup 2026 Data Module
3
+ Uses bongda.com.vn INTERNAL AJAX API (same method as livescore)
4
+ Tournament ID: 24254
5
+ Also fetches news from VnExpress, DanTri, BongDaPlus, 24h, TT&VH
 
 
6
  """
7
  import requests, re, time, json, os, threading
8
  from bs4 import BeautifulSoup
9
  from urllib.parse import quote
10
+ from datetime import datetime
11
  from concurrent.futures import ThreadPoolExecutor, as_completed
12
 
13
+ BONGDA_HEADERS = {
14
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
15
+ "Accept-Language": "vi-VN,vi;q=0.9",
16
+ "Referer": "https://bongda.com.vn/giai-dau/24254/summary/world-cup",
17
+ "X-Requested-With": "XMLHttpRequest"
18
+ }
19
+ UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
20
+ WC_TOURNAMENT_ID = 24254
21
  CACHE = {}
22
  CACHE_LOCK = threading.Lock()
23
 
 
34
  with CACHE_LOCK:
35
  CACHE[key] = {'t': time.time(), 'd': data}
36
 
37
+ def _fetch_bongda_api(endpoint):
38
+ """Call bongda.com.vn internal AJAX API - returns HTML string."""
39
+ try:
40
+ r = requests.get(f"https://bongda.com.vn{endpoint}", headers=BONGDA_HEADERS, timeout=12)
41
+ if r.status_code == 200:
42
+ data = r.json()
43
+ if data.get("status") == "success":
44
+ return data.get("html", "")
45
+ except:
46
+ pass
47
+ return ""
48
+
49
  def _fetch(url, timeout=12):
50
  try:
51
  r = requests.get(url, headers=UA, timeout=timeout, allow_redirects=True)
 
56
  pass
57
  return ''
58
 
59
+ # ==================== FIXTURES (from bongda.com.vn API) ====================
60
+ def scrape_fixtures():
61
+ """Get WC2026 fixtures using bongda.com.vn AJAX API."""
62
+ c = _cached('wc_fixtures', 120)
63
+ if c is not None: return c
64
+
65
+ # Try multiple API endpoints that bongda.com.vn uses
66
+ html = _fetch_bongda_api(f"/api/fixtures/get-by-tournament?tournament_id={WC_TOURNAMENT_ID}")
67
+ if not html:
68
+ html = _fetch_bongda_api(f"/api/fixtures/get-by-tournament?tournament_id={WC_TOURNAMENT_ID}&status=all")
69
+ if not html:
70
+ # Try date-based for today
71
+ today = datetime.now().strftime("%Y-%m-%d")
72
+ html = _fetch_bongda_api(f"/api/fixtures/get-by-date?date={today}&tournament_id={WC_TOURNAMENT_ID}")
73
+
74
+ result = {'html': html} if html else {'html': '', 'matches': []}
75
+ _set('wc_fixtures', result)
76
+ return result
77
+
78
+ # ==================== STANDINGS (BXH) ====================
79
+ def scrape_standings():
80
+ """Get WC2026 standings/league table using bongda.com.vn AJAX API."""
81
+ c = _cached('wc_standings', 180)
82
+ if c is not None: return c
83
+
84
+ html = _fetch_bongda_api(f"/api/league-table/home?tournament_id={WC_TOURNAMENT_ID}&is_detail=True")
85
+ if not html:
86
+ html = _fetch_bongda_api(f"/api/league-table/home?tournament_id={WC_TOURNAMENT_ID}")
87
+
88
+ result = {'html': html} if html else {'html': ''}
89
+ _set('wc_standings', result)
90
+ return result
91
+
92
+ # ==================== STATS ====================
93
+ def scrape_stats():
94
+ """Get WC2026 statistics using bongda.com.vn AJAX API."""
95
+ c = _cached('wc_stats', 300)
96
+ if c is not None: return c
97
+
98
+ # Player performance / top scorers
99
+ html = _fetch_bongda_api(f"/api/event-standing/top-scorers?tournament_id={WC_TOURNAMENT_ID}")
100
+ if not html:
101
+ html = _fetch_bongda_api(f"/api/event-standing/player-performance?tournament_id={WC_TOURNAMENT_ID}")
102
+
103
+ result = {'html': html} if html else {'html': ''}
104
+ _set('wc_stats', result)
105
+ return result
106
+
107
+ # ==================== SUMMARY (Live) ====================
108
+ def scrape_summary():
109
+ """Get WC2026 live/recent matches."""
110
+ c = _cached('wc_summary', 60)
111
+ if c is not None: return c
112
+
113
+ # Live matches for this tournament
114
+ html_live = _fetch_bongda_api(f"/api/fixtures/live?tournament_id={WC_TOURNAMENT_ID}")
115
+ # Today's matches
116
+ today = datetime.now().strftime("%Y-%m-%d")
117
+ html_today = _fetch_bongda_api(f"/api/fixtures/get-by-date?date={today}&tournament_id={WC_TOURNAMENT_ID}")
118
+ # Incoming
119
+ html_incoming = _fetch_bongda_api(f"/api/fixtures/incoming?tournament_id={WC_TOURNAMENT_ID}")
120
+
121
+ result = {
122
+ 'html_live': html_live,
123
+ 'html_today': html_today,
124
+ 'html_incoming': html_incoming
125
+ }
126
+ _set('wc_summary', result)
127
+ return result
128
+
129
  # ==================== NEWS ====================
130
  def scrape_wc_news():
131
  """Get World Cup 2026 news from multiple VN sources."""
 
134
 
135
  news = []
136
 
137
+ # 1. TT&VH World Cup RSS
138
  try:
139
+ html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
140
  if html:
141
  soup = BeautifulSoup(html, 'xml')
142
+ for item in soup.find_all('item')[:12]:
143
  title = _clean(item.find('title').get_text() if item.find('title') else '')
144
  link = _clean(item.find('link').get_text() if item.find('link') else '')
145
+ desc = item.find('description')
146
+ img = ''
147
+ if desc:
148
+ ds = BeautifulSoup(desc.get_text(), 'lxml')
149
+ im = ds.find('img')
150
+ if im: img = im.get('src', '')
151
+ if title and link:
152
+ news.append({'title': title, 'link': link, 'img': img, 'source': 'TT&VH'})
153
  except: pass
154
 
155
+ # 2. VnExpress search World Cup
156
  try:
157
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
158
  if html:
159
  soup = BeautifulSoup(html, 'lxml')
160
+ for art in soup.select('article.item-news')[:10]:
161
  a = art.select_one('h2 a, h3 a')
162
  if not a: continue
163
  title = _clean(a.get('title', '') or a.get_text())
 
168
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
169
  except: pass
170
 
171
+ # 3. BongDaPlus World Cup
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
173
  html = _fetch('https://bongdaplus.vn/world-cup')
174
  if html:
175
  soup = BeautifulSoup(html, 'lxml')
176
+ for a in soup.select('h2 a[href], h3 a[href], .title a[href]')[:8]:
177
  title = _clean(a.get_text())
178
  href = a.get('href', '')
179
  if not href.startswith('http'): href = 'https://bongdaplus.vn' + href
 
181
  news.append({'title': title, 'link': href, 'img': '', 'source': 'BongDaPlus'})
182
  except: pass
183
 
184
+ # 4. 24h World Cup
185
  try:
186
  html = _fetch('https://www.24h.com.vn/world-cup-2026-c860.html')
187
  if html:
188
  soup = BeautifulSoup(html, 'lxml')
189
+ for a in soup.select('a[title]')[:8]:
190
+ title = _clean(a.get('title', ''))
191
  href = a.get('href', '')
192
  if not href.startswith('http'): href = 'https://www.24h.com.vn' + href
193
+ if title and len(title) > 15 and href not in [n['link'] for n in news]:
194
  news.append({'title': title, 'link': href, 'img': '', 'source': '24h'})
195
  except: pass
196
 
197
  _set('wc_news', news[:30])
198
  return news[:30]
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  # ==================== ROAD TO WORLD CUP ====================
201
  def scrape_road_to_wc():
202
  """Get 'Đường tới World Cup 2026' articles."""
 
204
  if c is not None: return c
205
 
206
  articles = []
207
+ queries = ['đường tới World Cup 2026', 'vòng loại World Cup 2026', 'tuyển Việt Nam World Cup 2026']
208
 
209
  for q in queries:
210
  try:
211
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
212
  if not html: continue
213
  soup = BeautifulSoup(html, 'lxml')
214
+ for art in soup.select('article.item-news')[:5]:
215
  a = art.select_one('h2 a, h3 a')
216
  if not a: continue
217
  title = _clean(a.get('title', '') or a.get_text())
 
222
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
223
  except: continue
224
 
225
+ # Dân Trí search
226
  try:
227
+ html = _fetch(f'https://dantri.com.vn/tim-kiem/World+Cup+2026.htm')
228
  if html:
229
  soup = BeautifulSoup(html, 'lxml')
230
  for a in soup.select('h3 a[href]')[:6]:
 
245
  if c is not None: return c
246
 
247
  data = {}
248
+ with ThreadPoolExecutor(6) as ex:
249
  futs = {
250
  ex.submit(scrape_summary): 'summary',
251
  ex.submit(scrape_fixtures): 'fixtures',
 
254
  ex.submit(scrape_wc_news): 'news',
255
  ex.submit(scrape_road_to_wc): 'road',
256
  }
257
+ for f in as_completed(futs, timeout=18):
258
  key = futs[f]
259
  try:
260
  data[key] = f.result()
261
  except:
262
+ data[key] = {} if key in ('summary', 'fixtures', 'standings', 'stats') else []
263
 
264
  _set('wc_all', data)
265
  return data