bep40 commited on
Commit
85b1e18
·
verified ·
1 Parent(s): 032498a

FIX: scrape WC2026 fixtures from bongdanet.net (server-rendered, dedicated WC page)"

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +79 -65
wc2026_scraper.py CHANGED
@@ -1,8 +1,7 @@
1
  """
2
  World Cup 2026 Data Module
3
- - Lịch thi đấu: bongda24h.vn/vck-world-cup/lich-thi-dau-41.html (server-rendered, scrapeable)
4
- - BXH: bongda.com.vn API /api/league-table/home?tournament_id=24254 (PROVEN)
5
- - Stats: bongda.com.vn API
6
  - News: VnExpress + TT&VH + BongDaPlus
7
  """
8
  import requests, re, time, threading
@@ -17,12 +16,29 @@ BONGDA_HEADERS = {
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 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36'}
21
  WC_ID = 24254
22
  CACHE = {}
23
  LOCK = threading.Lock()
24
  _FIXTURES_ENDPOINT = None
25
- FIXTURES_CANDIDATES = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
28
  def _cached(key, ttl=120):
@@ -50,71 +66,73 @@ def _fetch(url, timeout=15):
50
  return r.text if r.status_code == 200 else ''
51
  except: return ''
52
 
53
- # ==================== FIXTURES from bongda24h.vn ====================
54
  def scrape_fixtures():
55
- """Lịch thi đấu WC2026 from bongda24h.vn (server-rendered HTML)."""
56
  c = _cached('wc_fix', 300)
57
  if c is not None: return c
58
-
59
  html_out = ''
 
 
60
  try:
61
- page = _fetch('https://bongda24h.vn/vck-world-cup/lich-thi-dau-41.html')
62
- if page:
63
  soup = BeautifulSoup(page, 'lxml')
64
- # Remove unwanted elements
65
- for s in soup.select('script, style, .ads, .banner, .adv, iframe, .social-share, .breadcrumb, nav, header, footer, .sidebar'):
66
  s.decompose()
67
-
68
- # bongda24h.vn schedule page has match tables/divs
69
- # Look for schedule content container
70
- content = (
71
- soup.select_one('.schedule-content') or
72
- soup.select_one('.match-schedule') or
73
- soup.select_one('.ltdContent') or
74
- soup.select_one('.content-tab') or
75
- soup.select_one('.box-content') or
76
- soup.select_one('#schedule') or
77
- soup.select_one('.main-content article') or
78
- soup.select_one('.main-content')
79
- )
80
-
81
- if content:
82
- # Remove nav, sidebars inside content
83
- for s in content.select('.sidebar, .ads, .banner, .social, .breadcrumb, .related-news'):
84
- s.decompose()
85
  html_out = str(content)
86
  else:
87
- # Fallback: get all tables that look like schedules
88
- tables = soup.select('table')
89
- for t in tables:
90
- rows = t.select('tr')
91
- if len(rows) > 3:
92
- text = t.get_text().lower()
93
- if any(kw in text for kw in ['vs', ' - ', 'bảng', 'vòng', 'group']):
94
- html_out = str(t)
95
- break
96
-
97
- # If still no table, try to get match divs
98
- if not html_out:
99
- matches = soup.select('.match-item, .match-row, .item-schedule, .match-fixture, .event-row')
100
- if matches:
101
- html_out = '<div class="wc-matches">' + ''.join(str(m) for m in matches[:60]) + '</div>'
102
-
103
- # Last resort: get body main content
104
  if not html_out:
105
- main = soup.select_one('main') or soup.select_one('.main-content') or soup.select_one('#content')
106
- if main:
107
- for s in main.select('.sidebar, .ads, nav, header, footer'): s.decompose()
108
- # Only keep elements with match-like content
109
- parts = []
110
- for el in main.select('table, .match-item, .schedule-round, .round-matches, div[class*=match], div[class*=schedule]'):
111
- parts.append(str(el))
112
- if parts:
113
- html_out = '<div>' + ''.join(parts[:30]) + '</div>'
114
- else:
115
- html_out = str(main)[:40000]
116
  except: pass
117
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  r = {'html': html_out}
119
  _set('wc_fix', r)
120
  return r
@@ -147,18 +165,14 @@ def scrape_history():
147
 
148
  def scrape_h2h(event_id):
149
  return {'html': _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")}
150
-
151
  def scrape_lineups(event_id):
152
  return {'html': _bongda(f"/api/fixtures/lineups?event_id={event_id}")}
153
-
154
  def scrape_match_detail(event_id):
155
  return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
156
-
157
  def scrape_summary():
158
  c = _cached('wc_sum', 90)
159
  if c is not None: return c
160
- html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
161
- r = {'html': html}
162
  _set('wc_sum', r)
163
  return r
164
 
@@ -245,7 +259,7 @@ def get_wc2026_all():
245
  ex.submit(scrape_wc_news): 'news',
246
  ex.submit(scrape_road_to_wc): 'road',
247
  }
248
- for f in as_completed(futs, timeout=20):
249
  key = futs[f]
250
  try: data[key] = f.result()
251
  except: data[key] = {} if key in ('fixtures', 'standings', 'stats') else []
 
1
  """
2
  World Cup 2026 Data Module
3
+ - BXH: bongda.com.vn API (tournament_id=24254) - PROVEN WORKING
4
+ - Fixtures: bongdanet.net/lich-thi-dau/world-cup-2026/ (server-rendered)
 
5
  - News: VnExpress + TT&VH + BongDaPlus
6
  """
7
  import requests, re, time, threading
 
16
  "Referer": "https://bongda.com.vn/giai-dau/24254/standings/world-cup",
17
  "X-Requested-With": "XMLHttpRequest"
18
  }
19
+ 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', 'Accept-Language': 'vi-VN,vi;q=0.9'}
20
  WC_ID = 24254
21
  CACHE = {}
22
  LOCK = threading.Lock()
23
  _FIXTURES_ENDPOINT = None
24
+ FIXTURES_CANDIDATES = [
25
+ f"/api/fixtures/home?tournament_id={WC_ID}",
26
+ f"/api/fixtures/home?tournament_id={WC_ID}&is_detail=True",
27
+ f"/api/fixtures/list?tournament_id={WC_ID}",
28
+ f"/api/fixtures/get-by-tournament?tournament_id={WC_ID}",
29
+ f"/api/fixtures/get-by-season?tournament_id={WC_ID}",
30
+ f"/api/fixtures/incoming?tournament_id={WC_ID}",
31
+ f"/api/fixtures/index?tournament_id={WC_ID}",
32
+ f"/api/tournament/{WC_ID}/fixtures",
33
+ f"/api/tournament-fixtures?tournament_id={WC_ID}",
34
+ f"/api/fixtures?tournament_id={WC_ID}",
35
+ f"/api/fixtures/all?tournament_id={WC_ID}",
36
+ f"/api/fixtures/schedule?tournament_id={WC_ID}",
37
+ f"/api/season-fixtures?tournament_id={WC_ID}",
38
+ f"/api/giai-dau/{WC_ID}/fixtures",
39
+ f"/api/fixtures/filter?tournament_id={WC_ID}",
40
+ f"/api/fixtures/by-tournament?tournament_id={WC_ID}",
41
+ ]
42
 
43
  def _clean(s): return re.sub(r'\s+', ' ', str(s or '')).strip()
44
  def _cached(key, ttl=120):
 
66
  return r.text if r.status_code == 200 else ''
67
  except: return ''
68
 
69
+ # ==================== FIXTURES ====================
70
  def scrape_fixtures():
71
+ """Lịch thi đấu WC2026 from bongdanet.net (server-rendered, reliable)."""
72
  c = _cached('wc_fix', 300)
73
  if c is not None: return c
74
+
75
  html_out = ''
76
+
77
+ # Source 1: bongdanet.net - dedicated WC2026 schedule page
78
  try:
79
+ page = _fetch('https://bongdanet.net/lich-thi-dau/world-cup-2026/')
80
+ if page and len(page) > 1000:
81
  soup = BeautifulSoup(page, 'lxml')
82
+ # Remove scripts, ads, nav
83
+ for s in soup.select('script, style, .ads, .banner, .adv, iframe, nav, header, footer, .sidebar, .breadcrumb'):
84
  s.decompose()
85
+ # Find schedule content
86
+ content = soup.select_one('.schedule-content, .match-schedule, .main-content, .content, article, .post-content')
87
+ if content and len(content.get_text()) > 100:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  html_out = str(content)
89
  else:
90
+ # Try finding tables with match data
91
+ for table in soup.select('table'):
92
+ if len(table.select('tr')) > 3:
93
+ html_out = str(table)
94
+ break
95
+ # Or divs with schedule
 
 
 
 
 
 
 
 
 
 
 
96
  if not html_out:
97
+ for div in soup.select('div[class*=schedule], div[class*=match], div[class*=fixture]'):
98
+ if len(div.get_text()) > 100:
99
+ html_out = str(div)
100
+ break
 
 
 
 
 
 
 
101
  except: pass
102
+
103
+ # Source 2: webthethao.vn
104
+ if not html_out:
105
+ try:
106
+ page = _fetch('https://webthethao.vn/bong-da-quoc-te/lich-thi-dau-day-du-104-tran-dau-tai-world-cup-2026-theo-gio-viet-nam-WmooXjWvR.htm')
107
+ if page and len(page) > 1000:
108
+ soup = BeautifulSoup(page, 'lxml')
109
+ for s in soup.select('script, style, .ads, iframe, nav, header, footer'):
110
+ s.decompose()
111
+ content = soup.select_one('.detail-content, .content-detail, article, .entry-content, .singular-content')
112
+ if content and len(content.get_text()) > 200:
113
+ html_out = str(content)
114
+ except: pass
115
+
116
+ # Source 3: 24h.com.vn
117
+ if not html_out:
118
+ try:
119
+ page = _fetch('https://www.24h.com.vn/bong-da/lich-thi-dau-bong-da-world-cup-2026-moi-nhat-c48a1747402.html')
120
+ if page and len(page) > 1000:
121
+ soup = BeautifulSoup(page, 'lxml')
122
+ for s in soup.select('script, style, .ads, iframe'):
123
+ s.decompose()
124
+ # Look for schedule tables
125
+ for table in soup.select('table'):
126
+ text = table.get_text().lower()
127
+ if any(kw in text for kw in ['bảng', 'group', 'vs', ' - ', 'world cup']):
128
+ html_out = str(table)
129
+ break
130
+ except: pass
131
+
132
+ # Truncate if too long
133
+ if html_out and len(html_out) > 50000:
134
+ html_out = html_out[:50000]
135
+
136
  r = {'html': html_out}
137
  _set('wc_fix', r)
138
  return r
 
165
 
166
  def scrape_h2h(event_id):
167
  return {'html': _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")}
 
168
  def scrape_lineups(event_id):
169
  return {'html': _bongda(f"/api/fixtures/lineups?event_id={event_id}")}
 
170
  def scrape_match_detail(event_id):
171
  return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
 
172
  def scrape_summary():
173
  c = _cached('wc_sum', 90)
174
  if c is not None: return c
175
+ r = {'html': _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")}
 
176
  _set('wc_sum', r)
177
  return r
178
 
 
259
  ex.submit(scrape_wc_news): 'news',
260
  ex.submit(scrape_road_to_wc): 'road',
261
  }
262
+ for f in as_completed(futs, timeout=25):
263
  key = futs[f]
264
  try: data[key] = f.result()
265
  except: data[key] = {} if key in ('fixtures', 'standings', 'stats') else []