bep40 commited on
Commit
989df80
·
verified ·
1 Parent(s): 9bbfaaf

Fix fixtures download headers + add github fallback + fix news image extraction"

Browse files
Files changed (1) hide show
  1. wc2026_scraper.py +80 -64
wc2026_scraper.py CHANGED
@@ -1,9 +1,8 @@
1
  """
2
- World Cup 2026 Data - CORRECT official data
3
- - Fixtures: fixturedownload.com JSON (104 matches, stadiums, times) + convert UTC→VN
4
- - BXH: bongda.com.vn API tournament_id=24254
5
- - News: with images from VnExpress + Thanh Niên + TT&VH
6
- - Livescore: when match is live, score updates from bongda API
7
  """
8
  import requests, re, time, threading
9
  from bs4 import BeautifulSoup
@@ -18,7 +17,7 @@ BONGDA_HEADERS = {
18
  "Referer": "https://bongda.com.vn/giai-dau/24254/standings/world-cup",
19
  "X-Requested-With": "XMLHttpRequest"
20
  }
21
- UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
22
  WC_ID = 24254
23
  VN_TZ = timezone(timedelta(hours=7))
24
  CACHE = {}
@@ -49,47 +48,59 @@ def _fetch(url, timeout=15):
49
  except: return ''
50
 
51
  def _utc_to_vn(date_str):
52
- """Convert UTC datetime string to Vietnam time string."""
53
  try:
54
- if 'T' in date_str:
55
- dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
 
56
  if dt.tzinfo is None:
57
  dt = dt.replace(tzinfo=timezone.utc)
58
  vn = dt.astimezone(VN_TZ)
59
  return vn.strftime('%H:%M %d/%m/%Y')
60
- return date_str
61
  except:
62
- return date_str
63
 
64
- # ==================== FIXTURES (JSON chính xác 104 trận) ====================
65
  def scrape_fixtures():
66
- """Get WC2026 fixtures from fixturedownload.com JSON - OFFICIAL 104 matches."""
67
- c = _cached('wc_fix', 3600) # 1h cache - data doesn't change often
68
  if c is not None: return c
69
 
70
  matches = []
71
- try:
72
- r = requests.get('https://fixturedownload.com/download/json/fifa-world-cup-2026', headers=UA, timeout=15)
73
- if r.status_code == 200:
74
- data = r.json()
75
- for m in data:
76
- matches.append({
77
- 'match_number': m.get('MatchNumber', ''),
78
- 'round': m.get('RoundNumber', ''),
79
- 'date_vn': _utc_to_vn(m.get('DateUtc', '')),
80
- 'date_utc': m.get('DateUtc', ''),
81
- 'location': m.get('Location', ''),
82
- 'home': m.get('HomeTeam', ''),
83
- 'away': m.get('AwayTeam', ''),
84
- 'group': m.get('Group', ''),
85
- 'home_score': m.get('HomeTeamScore'),
86
- 'away_score': m.get('AwayTeamScore'),
87
- })
88
- except: pass
89
 
90
- r = {'matches': matches, 'total': len(matches)}
91
- _set('wc_fix', r)
92
- return r
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  # ==================== BXH ====================
95
  def scrape_standings():
@@ -118,13 +129,34 @@ def scrape_lineups(event_id): return {'html': _bongda(f"/api/fixtures/lineups?ev
118
  def scrape_match_detail(event_id): return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
119
  def scrape_summary(): return scrape_standings()
120
 
121
- # ==================== NEWS (with images!) ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  def scrape_wc_news():
123
- """Get WC2026 news WITH images from multiple sources."""
124
  c = _cached('wc_news', 300)
125
  if c is not None: return c
126
  news = []
127
- # VnExpress (always has og:image in article items)
128
  try:
129
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
130
  if html:
@@ -134,42 +166,28 @@ def scrape_wc_news():
134
  if not a: continue
135
  title = _clean(a.get('title', '') or a.get_text())
136
  href = a.get('href', '')
137
- # Get image - VnExpress uses data-src or source srcset
138
- img = ''
139
- img_el = art.select_one('img')
140
- if img_el:
141
- img = img_el.get('data-src', '') or img_el.get('src', '')
142
- if not img:
143
- source_el = art.select_one('source')
144
- if source_el:
145
- srcset = source_el.get('srcset', '')
146
- if srcset: img = srcset.split(',')[0].strip().split(' ')[0]
147
- if not img:
148
- # Try picture element
149
- pic = art.select_one('picture img, .thumb-art img')
150
- if pic: img = pic.get('data-src', '') or pic.get('src', '')
151
  if title and href:
152
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
153
  except: pass
154
- # Thanh Niên WC2026
155
  try:
156
  html = _fetch('https://worldcup2026.thanhnien.vn/')
157
  if html:
158
  soup = BeautifulSoup(html, 'lxml')
159
- for item in soup.select('.story, .news-item, article, .item')[:12]:
160
  a = item.select_one('a[href]')
161
  if not a: continue
162
  title = _clean(a.get('title', '') or a.get_text())
163
  if not title or len(title) < 15: continue
164
  href = a.get('href', '')
165
  if not href.startswith('http'): href = 'https://worldcup2026.thanhnien.vn' + href
166
- img_el = item.select_one('img')
167
- img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
168
  if href not in [n['link'] for n in news]:
169
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
170
  if len(news) >= 20: break
171
  except: pass
172
- # Dân Trí WC
173
  try:
174
  html = _fetch('https://dantri.com.vn/the-thao/world-cup.htm')
175
  if html:
@@ -180,8 +198,10 @@ def scrape_wc_news():
180
  title = _clean(a.get('title', '') or a.get_text())
181
  href = a.get('href', '')
182
  if not href.startswith('http'): href = 'https://dantri.com.vn' + href
183
- img_el = art.select_one('img')
184
- img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
 
 
185
  if title and len(title) > 15 and href not in [n['link'] for n in news]:
186
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Dân Trí'})
187
  except: pass
@@ -202,11 +222,7 @@ def scrape_road_to_wc():
202
  if not a: continue
203
  title = _clean(a.get('title', '') or a.get_text())
204
  href = a.get('href', '')
205
- img_el = art.select_one('img')
206
- img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
207
- if not img:
208
- source_el = art.select_one('source')
209
- if source_el: img = source_el.get('srcset', '').split(',')[0].strip().split(' ')[0]
210
  if title and href and href not in [x['link'] for x in articles]:
211
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
212
  except: continue
 
1
  """
2
+ World Cup 2026 Data
3
+ - Fixtures: fixturedownload.com JSON (with Referer header) + GitHub fallback
4
+ - BXH: bongda.com.vn API
5
+ - News: VnExpress + Thanh Niên + Dân Trí (with proper image extraction)
 
6
  """
7
  import requests, re, time, threading
8
  from bs4 import BeautifulSoup
 
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
  VN_TZ = timezone(timedelta(hours=7))
23
  CACHE = {}
 
48
  except: return ''
49
 
50
  def _utc_to_vn(date_str):
 
51
  try:
52
+ if not date_str: return ''
53
+ if 'T' in str(date_str):
54
+ dt = datetime.fromisoformat(str(date_str).replace('Z', '+00:00'))
55
  if dt.tzinfo is None:
56
  dt = dt.replace(tzinfo=timezone.utc)
57
  vn = dt.astimezone(VN_TZ)
58
  return vn.strftime('%H:%M %d/%m/%Y')
59
+ return str(date_str)
60
  except:
61
+ return str(date_str)
62
 
63
+ # ==================== FIXTURES ====================
64
  def scrape_fixtures():
65
+ c = _cached('wc_fix', 3600)
 
66
  if c is not None: return c
67
 
68
  matches = []
69
+ urls = [
70
+ ('https://fixturedownload.com/download/json/fifa-world-cup-2026', {
71
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
72
+ 'Accept': 'application/json, text/plain, */*',
73
+ 'Referer': 'https://fixturedownload.com/view/json/fifa-world-cup-2026'
74
+ }),
75
+ ('https://raw.githubusercontent.com/mjwebmaster/world-cup-2026-schedule-data/main/world-cup-2026.json', UA),
76
+ ]
 
 
 
 
 
 
 
 
 
 
77
 
78
+ for url, headers in urls:
79
+ if matches: break
80
+ try:
81
+ r = requests.get(url, headers=headers, timeout=20)
82
+ if r.status_code == 200:
83
+ data = r.json()
84
+ if isinstance(data, list) and len(data) > 10:
85
+ for m in data:
86
+ date_raw = m.get('DateUtc') or m.get('date_utc') or m.get('date') or ''
87
+ matches.append({
88
+ 'match_number': m.get('MatchNumber') or m.get('match_number') or '',
89
+ 'round': m.get('RoundNumber') or m.get('round') or '',
90
+ 'date_vn': _utc_to_vn(date_raw),
91
+ 'date_utc': date_raw,
92
+ 'location': m.get('Location') or m.get('location') or m.get('venue') or '',
93
+ 'home': m.get('HomeTeam') or m.get('home_team') or m.get('home') or '',
94
+ 'away': m.get('AwayTeam') or m.get('away_team') or m.get('away') or '',
95
+ 'group': m.get('Group') or m.get('group') or m.get('stage') or '',
96
+ 'home_score': m.get('HomeTeamScore') or m.get('home_score'),
97
+ 'away_score': m.get('AwayTeamScore') or m.get('away_score'),
98
+ })
99
+ except: continue
100
+
101
+ result = {'matches': matches, 'total': len(matches)}
102
+ _set('wc_fix', result)
103
+ return result
104
 
105
  # ==================== BXH ====================
106
  def scrape_standings():
 
129
  def scrape_match_detail(event_id): return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
130
  def scrape_summary(): return scrape_standings()
131
 
132
+ # ==================== NEWS ====================
133
+ def _get_img_from_article(art):
134
+ """Extract image URL from a VnExpress/generic article element."""
135
+ # Try img tag
136
+ for img in art.select('img'):
137
+ src = img.get('data-src') or img.get('data-original') or img.get('src') or ''
138
+ if src and 'blank' not in src and 'data:image' not in src and len(src) > 20:
139
+ if src.startswith('//'): src = 'https:' + src
140
+ return src
141
+ # Try source srcset (VnExpress picture element)
142
+ for source in art.select('source[srcset]'):
143
+ srcset = source.get('srcset', '')
144
+ if srcset:
145
+ first = srcset.split(',')[0].strip().split(' ')[0]
146
+ if first and len(first) > 20:
147
+ if first.startswith('//'): first = 'https:' + first
148
+ return first
149
+ # Try background-image in style
150
+ for el in art.select('[style*="background"]'):
151
+ m = re.search(r'url\(["\']?([^"\')\s]+)', el.get('style', ''))
152
+ if m: return m.group(1)
153
+ return ''
154
+
155
  def scrape_wc_news():
 
156
  c = _cached('wc_news', 300)
157
  if c is not None: return c
158
  news = []
159
+ # VnExpress
160
  try:
161
  html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
162
  if html:
 
166
  if not a: continue
167
  title = _clean(a.get('title', '') or a.get_text())
168
  href = a.get('href', '')
169
+ img = _get_img_from_article(art)
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  if title and href:
171
  news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
172
  except: pass
173
+ # Thanh Niên
174
  try:
175
  html = _fetch('https://worldcup2026.thanhnien.vn/')
176
  if html:
177
  soup = BeautifulSoup(html, 'lxml')
178
+ for item in soup.select('.story, .news-item, article, .item, .zone--timeline .story')[:12]:
179
  a = item.select_one('a[href]')
180
  if not a: continue
181
  title = _clean(a.get('title', '') or a.get_text())
182
  if not title or len(title) < 15: continue
183
  href = a.get('href', '')
184
  if not href.startswith('http'): href = 'https://worldcup2026.thanhnien.vn' + href
185
+ img = _get_img_from_article(item)
 
186
  if href not in [n['link'] for n in news]:
187
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
188
  if len(news) >= 20: break
189
  except: pass
190
+ # Dân Trí
191
  try:
192
  html = _fetch('https://dantri.com.vn/the-thao/world-cup.htm')
193
  if html:
 
198
  title = _clean(a.get('title', '') or a.get_text())
199
  href = a.get('href', '')
200
  if not href.startswith('http'): href = 'https://dantri.com.vn' + href
201
+ img = _get_img_from_article(art)
202
+ # Proxy dantri images
203
+ if img and 'cdnphoto.dantri' in img:
204
+ img = '/api/proxy/img?url=' + quote(img, safe='')
205
  if title and len(title) > 15 and href not in [n['link'] for n in news]:
206
  news.append({'title': title, 'link': href, 'img': img, 'source': 'Dân Trí'})
207
  except: pass
 
222
  if not a: continue
223
  title = _clean(a.get('title', '') or a.get_text())
224
  href = a.get('href', '')
225
+ img = _get_img_from_article(art)
 
 
 
 
226
  if title and href and href not in [x['link'] for x in articles]:
227
  articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
228
  except: continue