bep40 commited on
Commit
a3ce762
·
verified ·
1 Parent(s): 692f9d2

Fix WC2026: scrape fixtures+standings from dantri.com.vn/the-thao/world-cup (server-rendered)"

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +92 -50
wc2026_scraper.py CHANGED
@@ -1,10 +1,10 @@
1
  """
2
  World Cup 2026 Data Module
3
- - Lịch thi đấu: static JSON (official FIFA schedule, 104 matches, giờ VN)
4
- - BXH: bongda.com.vn API tournament_id=24254
5
  - News: Thanh Niên + TT&VH + VnExpress
6
  """
7
- import requests, re, time, threading, json, os
8
  from bs4 import BeautifulSoup
9
  from urllib.parse import quote
10
  from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -16,7 +16,7 @@ BONGDA_HEADERS = {
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'}
20
  WC_ID = 24254
21
  CACHE = {}
22
  LOCK = threading.Lock()
@@ -45,53 +45,75 @@ def _fetch(url, timeout=15):
45
  return r.text if r.status_code == 200 else ''
46
  except: return ''
47
 
48
- # ==================== LỊCH THI ĐẤU (static JSON - official FIFA data) ====================
49
  def scrape_fixtures():
50
- """Load official WC2026 fixtures from static JSON file."""
51
- c = _cached('wc_fix', 86400) # Cache 24h - data is static
52
  if c is not None: return c
53
 
54
- matches = []
55
- json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'wc2026_fixtures.json')
56
- try:
57
- with open(json_path, 'r', encoding='utf-8') as f:
58
- matches = json.load(f)
59
- except:
60
- pass
61
-
62
- # Group matches by group
63
- groups = {}
64
- for m in matches:
65
- g = m.get('group', 'Khác')
66
- if g not in groups: groups[g] = []
67
- groups[g].append(m)
68
-
69
- group_list = [{'group': k, 'matches': v} for k, v in groups.items()]
70
- r = {'groups': group_list, 'total_matches': len(matches)}
 
 
 
 
 
 
 
 
 
71
  _set('wc_fix', r)
72
  return r
73
 
74
- def debug_fixtures():
75
- """Debug: show what data is loaded."""
76
- json_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static', 'wc2026_fixtures.json')
77
- exists = os.path.exists(json_path)
78
- count = 0
79
- if exists:
80
- try:
81
- with open(json_path, 'r', encoding='utf-8') as f:
82
- data = json.load(f)
83
- count = len(data)
84
- except: pass
85
- return {'file_exists': exists, 'path': json_path, 'match_count': count}
86
-
87
- # ==================== BXH ====================
88
  def scrape_standings():
89
- c = _cached('wc_bxh', 180)
 
90
  if c is not None: return c
91
- html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
92
- if not html:
93
- html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
94
- r = {'html': html}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  _set('wc_bxh', r)
96
  return r
97
 
@@ -105,17 +127,23 @@ def scrape_stats():
105
  return r
106
 
107
  # ==================== OTHER ====================
108
- def scrape_history(): return scrape_standings()
109
- def scrape_h2h(event_id): return {'html': _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")}
110
- def scrape_lineups(event_id): return {'html': _bongda(f"/api/fixtures/lineups?event_id={event_id}")}
111
- def scrape_match_detail(event_id): return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
112
- def scrape_summary(): return scrape_standings()
 
 
 
 
 
113
 
114
  # ==================== NEWS ====================
115
  def scrape_wc_news():
116
  c = _cached('wc_news', 300)
117
  if c is not None: return c
118
  news = []
 
119
  try:
120
  html = _fetch('https://worldcup2026.thanhnien.vn/')
121
  if html:
@@ -131,21 +159,35 @@ def scrape_wc_news():
131
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
132
  if len(news) >= 10: break
133
  except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  try:
135
  html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
136
  if html:
137
  soup = BeautifulSoup(html, 'xml')
138
- for item in soup.find_all('item')[:10]:
139
  title = _clean(item.find('title').get_text() if item.find('title') else '')
140
  link = _clean(item.find('link').get_text() if item.find('link') else '')
141
  if title and link and link not in [n['link'] for n in news]:
142
  news.append({'title': title, 'link': link, 'img': '', 'source': 'TT&VH'})
143
  except: pass
 
144
  try:
145
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
146
  if html:
147
  soup = BeautifulSoup(html, 'lxml')
148
- for art in soup.select('article.item-news')[:8]:
149
  a = art.select_one('h2 a, h3 a')
150
  if not a: continue
151
  title = _clean(a.get('title', '') or a.get_text())
 
1
  """
2
  World Cup 2026 Data Module
3
+ - Lịch thi đấu + BXH: dantri.com.vn/the-thao/world-cup/ (server-rendered, scrapeable)
4
+ - Stats: bongda.com.vn API tournament_id=24254
5
  - News: Thanh Niên + TT&VH + VnExpress
6
  """
7
+ import requests, re, time, threading
8
  from bs4 import BeautifulSoup
9
  from urllib.parse import quote
10
  from concurrent.futures import ThreadPoolExecutor, as_completed
 
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()
 
45
  return r.text if r.status_code == 200 else ''
46
  except: return ''
47
 
48
+ # ==================== LỊCH THI ĐẤU (dantri.com.vn) ====================
49
  def scrape_fixtures():
50
+ """Scrape lịch thi đấu WC2026 từ dantri.com.vn/the-thao/world-cup/lich-thi-dau.htm"""
51
+ c = _cached('wc_fix', 300)
52
  if c is not None: return c
53
 
54
+ html_content = ''
55
+ page = _fetch('https://dantri.com.vn/the-thao/world-cup/lich-thi-dau.htm')
56
+ if page:
57
+ soup = BeautifulSoup(page, 'lxml')
58
+ # Remove junk
59
+ for s in soup.select('script, style, nav, footer, header, .ads, .banner, .sidebar, .social-share, .comment-wrap, .relate-news'):
60
+ s.decompose()
61
+ # Dân Trí schedule page uses specific containers
62
+ content = (
63
+ soup.select_one('.schedule-container') or
64
+ soup.select_one('.match-schedule') or
65
+ soup.select_one('.livescore-container') or
66
+ soup.select_one('.content-schedule') or
67
+ soup.select_one('#schedule-content') or
68
+ soup.select_one('.singular-content') or
69
+ soup.select_one('main .e-magazine__body') or
70
+ soup.select_one('article') or
71
+ soup.select_one('main')
72
+ )
73
+ if content:
74
+ # Clean internal junk
75
+ for s in content.select('.ads, .banner, script, style, .social, .relate'):
76
+ s.decompose()
77
+ html_content = str(content)
78
+
79
+ r = {'html': html_content}
80
  _set('wc_fix', r)
81
  return r
82
 
83
+ # ==================== BXH (dantri.com.vn) ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  def scrape_standings():
85
+ """Scrape BXH WC2026 từ dantri.com.vn/the-thao/world-cup/bang-xep-hang.htm"""
86
+ c = _cached('wc_bxh', 300)
87
  if c is not None: return c
88
+
89
+ html_content = ''
90
+ # Try dantri first
91
+ page = _fetch('https://dantri.com.vn/the-thao/world-cup/bang-xep-hang.htm')
92
+ if page:
93
+ soup = BeautifulSoup(page, 'lxml')
94
+ for s in soup.select('script, style, nav, footer, header, .ads, .banner, .sidebar, .social-share, .comment-wrap, .relate-news'):
95
+ s.decompose()
96
+ content = (
97
+ soup.select_one('.standings-container') or
98
+ soup.select_one('.ranking-container') or
99
+ soup.select_one('.livescore-container') or
100
+ soup.select_one('#standings-content') or
101
+ soup.select_one('.singular-content') or
102
+ soup.select_one('article') or
103
+ soup.select_one('main')
104
+ )
105
+ if content:
106
+ for s in content.select('.ads, .banner, script, style, .social, .relate'):
107
+ s.decompose()
108
+ html_content = str(content)
109
+
110
+ # Fallback to bongda.com.vn API if dantri empty
111
+ if not html_content or len(html_content) < 100:
112
+ html_content = _bongda(f"/api/league-table/home?tournament_id={WC_ID}&is_detail=True")
113
+ if not html_content:
114
+ html_content = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
115
+
116
+ r = {'html': html_content}
117
  _set('wc_bxh', r)
118
  return r
119
 
 
127
  return r
128
 
129
  # ==================== OTHER ====================
130
+ def scrape_history():
131
+ return scrape_standings()
132
+ def scrape_h2h(event_id):
133
+ return {'html': _bongda(f"/api/fixtures/head-to-head?event_id={event_id}")}
134
+ def scrape_lineups(event_id):
135
+ return {'html': _bongda(f"/api/fixtures/lineups?event_id={event_id}")}
136
+ def scrape_match_detail(event_id):
137
+ return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
138
+ def scrape_summary():
139
+ return scrape_standings()
140
 
141
  # ==================== NEWS ====================
142
  def scrape_wc_news():
143
  c = _cached('wc_news', 300)
144
  if c is not None: return c
145
  news = []
146
+ # Thanh Niên WC2026
147
  try:
148
  html = _fetch('https://worldcup2026.thanhnien.vn/')
149
  if html:
 
159
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
160
  if len(news) >= 10: break
161
  except: pass
162
+ # Dân Trí WC
163
+ try:
164
+ html = _fetch('https://dantri.com.vn/the-thao/world-cup.htm')
165
+ if html:
166
+ soup = BeautifulSoup(html, 'lxml')
167
+ for a in soup.select('h3 a[href], .article-title a[href], .news-item a[href]')[:10]:
168
+ title = _clean(a.get('title', '') or a.get_text())
169
+ href = a.get('href', '')
170
+ if not href.startswith('http'): href = 'https://dantri.com.vn' + href
171
+ if title and len(title) > 15 and href not in [n['link'] for n in news]:
172
+ news.append({'title': title, 'link': href, 'img': '', 'source': 'Dân Trí'})
173
+ except: pass
174
+ # TT&VH RSS
175
  try:
176
  html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
177
  if html:
178
  soup = BeautifulSoup(html, 'xml')
179
+ for item in soup.find_all('item')[:8]:
180
  title = _clean(item.find('title').get_text() if item.find('title') else '')
181
  link = _clean(item.find('link').get_text() if item.find('link') else '')
182
  if title and link and link not in [n['link'] for n in news]:
183
  news.append({'title': title, 'link': link, 'img': '', 'source': 'TT&VH'})
184
  except: pass
185
+ # VnExpress
186
  try:
187
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
188
  if html:
189
  soup = BeautifulSoup(html, 'lxml')
190
+ for art in soup.select('article.item-news')[:6]:
191
  a = art.select_one('h2 a, h3 a')
192
  if not a: continue
193
  title = _clean(a.get('title', '') or a.get_text())