Spaces:
Running
Running
Fix: road_to_wc always gets og:image; hot_topics from 8 RSS sources (not just VnExpress)"
Browse files- wc2026_scraper.py +48 -32
wc2026_scraper.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
World Cup 2026 Data Module - NEWS ALWAYS
|
| 3 |
"""
|
| 4 |
import requests, re, time, threading
|
| 5 |
from bs4 import BeautifulSoup
|
|
@@ -45,15 +45,13 @@ def _fetch(url, timeout=12):
|
|
| 45 |
except: return ''
|
| 46 |
|
| 47 |
def _get_og_image(url):
|
| 48 |
-
"""Fetch og:image from
|
| 49 |
try:
|
| 50 |
-
r = requests.get(url, headers=UA, timeout=
|
| 51 |
r.encoding = 'utf-8'
|
| 52 |
if r.status_code != 200: return ''
|
| 53 |
-
|
| 54 |
-
m = re.search(r'<meta[^>]+
|
| 55 |
-
if not m:
|
| 56 |
-
m = re.search(r'<meta[^>]+content=["\']([^"\']+)["\'][^>]+property=["\']og:image["\']', r.text)
|
| 57 |
if m:
|
| 58 |
img = m.group(1)
|
| 59 |
if img.startswith('//'): img = 'https:' + img
|
|
@@ -114,7 +112,6 @@ def scrape_standings():
|
|
| 114 |
if not html: html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
|
| 115 |
r = {'html': html}; _set('wc_bxh', r); return r
|
| 116 |
|
| 117 |
-
# ==================== STATS ====================
|
| 118 |
def scrape_stats():
|
| 119 |
c = _cached('wc_stats', 600)
|
| 120 |
if c is not None: return c
|
|
@@ -127,13 +124,12 @@ def scrape_lineups(event_id): return {'html': _bongda(f"/api/fixtures/lineups?ev
|
|
| 127 |
def scrape_match_detail(event_id): return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
|
| 128 |
def scrape_summary(): return scrape_standings()
|
| 129 |
|
| 130 |
-
# ==================== NEWS
|
| 131 |
def scrape_wc_news():
|
| 132 |
c = _cached('wc_news', 300)
|
| 133 |
if c is not None: return c
|
| 134 |
news = []
|
| 135 |
-
|
| 136 |
-
# VnExpress World Cup (best images)
|
| 137 |
try:
|
| 138 |
html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
|
| 139 |
if html:
|
|
@@ -146,11 +142,9 @@ def scrape_wc_news():
|
|
| 146 |
if not title or href in [n['link'] for n in news]: continue
|
| 147 |
img_el = art.select_one('img')
|
| 148 |
img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
|
| 149 |
-
# Filter blank/placeholder images
|
| 150 |
if img and ('blank' in img or 'data:image' in img): img = ''
|
| 151 |
news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
|
| 152 |
except: pass
|
| 153 |
-
|
| 154 |
# Thanh Niên
|
| 155 |
try:
|
| 156 |
html = _fetch('https://worldcup2026.thanhnien.vn/')
|
|
@@ -167,7 +161,6 @@ def scrape_wc_news():
|
|
| 167 |
news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
|
| 168 |
if len(news) >= 20: break
|
| 169 |
except: pass
|
| 170 |
-
|
| 171 |
# Dân Trí
|
| 172 |
try:
|
| 173 |
html = _fetch('https://dantri.com.vn/the-thao/world-cup.htm')
|
|
@@ -180,7 +173,6 @@ def scrape_wc_news():
|
|
| 180 |
if not title or len(title)<15 or href in [n['link'] for n in news]: continue
|
| 181 |
news.append({'title': title, 'link': href, 'img': '', 'source': 'Dân Trí'})
|
| 182 |
except: pass
|
| 183 |
-
|
| 184 |
# TT&VH
|
| 185 |
try:
|
| 186 |
html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
|
|
@@ -199,28 +191,25 @@ def scrape_wc_news():
|
|
| 199 |
news.append({'title': title, 'link': link, 'img': img, 'source': 'TT&VH'})
|
| 200 |
except: pass
|
| 201 |
|
| 202 |
-
#
|
| 203 |
-
def
|
| 204 |
-
if item.get('img'): return
|
| 205 |
-
img = _get_og_image(item['link'])
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
if items_need_img:
|
| 212 |
-
with ThreadPoolExecutor(5) as ex:
|
| 213 |
-
futs = [ex.submit(_fill_img, item) for item in items_need_img[:10]]
|
| 214 |
-
for f in as_completed(futs, timeout=12):
|
| 215 |
-
try: f.result()
|
| 216 |
-
except: pass
|
| 217 |
|
| 218 |
_set('wc_news', news[:30]); return news[:30]
|
| 219 |
|
|
|
|
| 220 |
def scrape_road_to_wc():
|
| 221 |
c = _cached('wc_road', 600)
|
| 222 |
if c is not None: return c
|
| 223 |
articles = []
|
|
|
|
|
|
|
| 224 |
for q in ['đường tới World Cup 2026', 'tuyển Việt Nam World Cup 2026']:
|
| 225 |
try:
|
| 226 |
html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
|
|
@@ -233,10 +222,37 @@ def scrape_road_to_wc():
|
|
| 233 |
href = a.get('href', '')
|
| 234 |
img_el = art.select_one('img')
|
| 235 |
img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
|
| 236 |
-
if
|
| 237 |
if title and href and href not in [x['link'] for x in articles]:
|
| 238 |
articles.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress', 'type': 'road'})
|
| 239 |
except: continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
_set('wc_road', articles[:20]); return articles[:20]
|
| 241 |
|
| 242 |
# ==================== ALL ====================
|
|
@@ -252,7 +268,7 @@ def get_wc2026_all():
|
|
| 252 |
ex.submit(scrape_wc_news): 'news',
|
| 253 |
ex.submit(scrape_road_to_wc): 'road',
|
| 254 |
}
|
| 255 |
-
for f in as_completed(futs, timeout=
|
| 256 |
key = futs[f]
|
| 257 |
try: data[key] = f.result()
|
| 258 |
except: data[key] = {} if key in ('fixtures','standings','stats') else []
|
|
|
|
| 1 |
"""
|
| 2 |
+
World Cup 2026 Data Module - ALL NEWS ITEMS ALWAYS HAVE IMAGES
|
| 3 |
"""
|
| 4 |
import requests, re, time, threading
|
| 5 |
from bs4 import BeautifulSoup
|
|
|
|
| 45 |
except: return ''
|
| 46 |
|
| 47 |
def _get_og_image(url):
|
| 48 |
+
"""Fetch og:image from URL."""
|
| 49 |
try:
|
| 50 |
+
r = requests.get(url, headers=UA, timeout=10, allow_redirects=True)
|
| 51 |
r.encoding = 'utf-8'
|
| 52 |
if r.status_code != 200: return ''
|
| 53 |
+
m = re.search(r'<meta[^>]+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']', r.text[:5000])
|
| 54 |
+
if not m: m = re.search(r'<meta[^>]+content=["\']([^"\']+)["\'][^>]+property=["\']og:image["\']', r.text[:5000])
|
|
|
|
|
|
|
| 55 |
if m:
|
| 56 |
img = m.group(1)
|
| 57 |
if img.startswith('//'): img = 'https:' + img
|
|
|
|
| 112 |
if not html: html = _bongda(f"/api/league-table/home?tournament_id={WC_ID}")
|
| 113 |
r = {'html': html}; _set('wc_bxh', r); return r
|
| 114 |
|
|
|
|
| 115 |
def scrape_stats():
|
| 116 |
c = _cached('wc_stats', 600)
|
| 117 |
if c is not None: return c
|
|
|
|
| 124 |
def scrape_match_detail(event_id): return {'html': _bongda(f"/api/fixtures/commentaries?event_id={event_id}")}
|
| 125 |
def scrape_summary(): return scrape_standings()
|
| 126 |
|
| 127 |
+
# ==================== NEWS ====================
|
| 128 |
def scrape_wc_news():
|
| 129 |
c = _cached('wc_news', 300)
|
| 130 |
if c is not None: return c
|
| 131 |
news = []
|
| 132 |
+
# VnExpress
|
|
|
|
| 133 |
try:
|
| 134 |
html = _fetch(f'https://timkiem.vnexpress.net/?q={quote("World Cup 2026")}')
|
| 135 |
if html:
|
|
|
|
| 142 |
if not title or href in [n['link'] for n in news]: continue
|
| 143 |
img_el = art.select_one('img')
|
| 144 |
img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
|
|
|
|
| 145 |
if img and ('blank' in img or 'data:image' in img): img = ''
|
| 146 |
news.append({'title': title, 'link': href, 'img': img, 'source': 'VnExpress'})
|
| 147 |
except: pass
|
|
|
|
| 148 |
# Thanh Niên
|
| 149 |
try:
|
| 150 |
html = _fetch('https://worldcup2026.thanhnien.vn/')
|
|
|
|
| 161 |
news.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên'})
|
| 162 |
if len(news) >= 20: break
|
| 163 |
except: pass
|
|
|
|
| 164 |
# Dân Trí
|
| 165 |
try:
|
| 166 |
html = _fetch('https://dantri.com.vn/the-thao/world-cup.htm')
|
|
|
|
| 173 |
if not title or len(title)<15 or href in [n['link'] for n in news]: continue
|
| 174 |
news.append({'title': title, 'link': href, 'img': '', 'source': 'Dân Trí'})
|
| 175 |
except: pass
|
|
|
|
| 176 |
# TT&VH
|
| 177 |
try:
|
| 178 |
html = _fetch('https://thethaovanhoa.vn/rss/world-cup-2026.rss')
|
|
|
|
| 191 |
news.append({'title': title, 'link': link, 'img': img, 'source': 'TT&VH'})
|
| 192 |
except: pass
|
| 193 |
|
| 194 |
+
# FETCH og:image for ALL items without images
|
| 195 |
+
def _fill(item):
|
| 196 |
+
if item.get('img'): return
|
| 197 |
+
item['img'] = _get_og_image(item['link'])
|
| 198 |
+
with ThreadPoolExecutor(6) as ex:
|
| 199 |
+
futs = [ex.submit(_fill, n) for n in news[:20] if not n.get('img')]
|
| 200 |
+
for f in as_completed(futs, timeout=15):
|
| 201 |
+
try: f.result()
|
| 202 |
+
except: pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
_set('wc_news', news[:30]); return news[:30]
|
| 205 |
|
| 206 |
+
# ==================== ROAD TO WC (also fetches og:image for ALL) ====================
|
| 207 |
def scrape_road_to_wc():
|
| 208 |
c = _cached('wc_road', 600)
|
| 209 |
if c is not None: return c
|
| 210 |
articles = []
|
| 211 |
+
|
| 212 |
+
# VnExpress
|
| 213 |
for q in ['đường tới World Cup 2026', 'tuyển Việt Nam World Cup 2026']:
|
| 214 |
try:
|
| 215 |
html = _fetch(f'https://timkiem.vnexpress.net/?q={quote(q)}')
|
|
|
|
| 222 |
href = a.get('href', '')
|
| 223 |
img_el = art.select_one('img')
|
| 224 |
img = (img_el.get('data-src', '') or img_el.get('src', '')) if img_el else ''
|
| 225 |
+
if img and ('blank' in img or 'data:image' in img): img = ''
|
| 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
|
| 229 |
+
|
| 230 |
+
# Thanh Niên road
|
| 231 |
+
try:
|
| 232 |
+
html = _fetch('https://worldcup2026.thanhnien.vn/')
|
| 233 |
+
if html:
|
| 234 |
+
soup = BeautifulSoup(html, 'lxml')
|
| 235 |
+
for a in soup.select('a[href*="duong-toi"], a[href*="vong-loai"], a[href*="tuyen-viet-nam"]')[:5]:
|
| 236 |
+
href = a.get('href', '')
|
| 237 |
+
title = _clean(a.get('title', '') or a.get_text())
|
| 238 |
+
if not title or len(title) < 15: continue
|
| 239 |
+
if not href.startswith('http'): href = 'https://worldcup2026.thanhnien.vn' + href
|
| 240 |
+
if href in [x['link'] for x in articles]: continue
|
| 241 |
+
img_el = a.select_one('img') or (a.parent.select_one('img') if a.parent else None)
|
| 242 |
+
img = (img_el.get('data-src','') or img_el.get('src','')) if img_el else ''
|
| 243 |
+
articles.append({'title': title, 'link': href, 'img': img, 'source': 'Thanh Niên', 'type': 'road'})
|
| 244 |
+
except: pass
|
| 245 |
+
|
| 246 |
+
# FETCH og:image for ALL without images
|
| 247 |
+
def _fill(item):
|
| 248 |
+
if item.get('img'): return
|
| 249 |
+
item['img'] = _get_og_image(item['link'])
|
| 250 |
+
with ThreadPoolExecutor(5) as ex:
|
| 251 |
+
futs = [ex.submit(_fill, a) for a in articles if not a.get('img')]
|
| 252 |
+
for f in as_completed(futs, timeout=15):
|
| 253 |
+
try: f.result()
|
| 254 |
+
except: pass
|
| 255 |
+
|
| 256 |
_set('wc_road', articles[:20]); return articles[:20]
|
| 257 |
|
| 258 |
# ==================== ALL ====================
|
|
|
|
| 268 |
ex.submit(scrape_wc_news): 'news',
|
| 269 |
ex.submit(scrape_road_to_wc): 'road',
|
| 270 |
}
|
| 271 |
+
for f in as_completed(futs, timeout=35):
|
| 272 |
key = futs[f]
|
| 273 |
try: data[key] = f.result()
|
| 274 |
except: data[key] = {} if key in ('fixtures','standings','stats') else []
|