Spaces:
Running
Running
Fix: import HL_LEAGUES + add friendly path for highlights"
Browse files- app_v2_entry.py +33 -78
app_v2_entry.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
| 1 |
-
"""
|
| 2 |
-
VNEWS v2 Entry Point
|
| 3 |
-
Hashtag: 8 sources
|
| 4 |
-
Hot topics: SORTED by frequency (most mentioned across feeds = hottest = auto-loaded first)
|
| 5 |
-
"""
|
| 6 |
import sys, os
|
| 7 |
-
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api
|
| 8 |
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
from starlette.routing import Mount
|
|
@@ -15,34 +11,32 @@ from bs4 import BeautifulSoup
|
|
| 15 |
import re, html as html_lib, json, threading, time
|
| 16 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 17 |
|
|
|
|
|
|
|
|
|
|
| 18 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
| 19 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
| 20 |
app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
|
| 21 |
app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
| 22 |
-
|
| 23 |
def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 24 |
_STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
|
| 25 |
-
|
| 26 |
def _has_kw(topic,title):
|
| 27 |
tl=topic.lower();tt=(title or'').lower()
|
| 28 |
if tl in tt:return True
|
| 29 |
words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',tl) if len(w)>2 and w not in _STOP]
|
| 30 |
if not words:return True
|
| 31 |
return any(w in tt for w in words)
|
| 32 |
-
|
| 33 |
def _s_vnexpress(topic,limit=8):
|
| 34 |
-
items=[]
|
| 35 |
try:
|
| 36 |
r=req.get(f"https://timkiem.vnexpress.net/?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 37 |
for art in soup.select('article.item-news')[:limit]:
|
| 38 |
a=art.select_one('h2 a, h3 a')
|
| 39 |
-
if a and a.get('href'):
|
| 40 |
-
t=_clean(a.get('title','') or a.get_text(strip=True))
|
| 41 |
-
if _has_kw(topic,t):items.append({'title':t,'url':a['href'],'via':'VnExpress'})
|
| 42 |
except:pass
|
| 43 |
return items
|
| 44 |
def _s_dantri(topic,limit=8):
|
| 45 |
-
items=[]
|
| 46 |
try:
|
| 47 |
r=req.get(f"https://dantri.com.vn/tim-kiem/{quote(topic)}.htm",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 48 |
for a in soup.select('h3 a[href], .article-title a[href]')[:limit*2]:
|
|
@@ -54,10 +48,10 @@ def _s_dantri(topic,limit=8):
|
|
| 54 |
except:pass
|
| 55 |
return items
|
| 56 |
def _s_vietnamnet(topic,limit=6):
|
| 57 |
-
items=[]
|
| 58 |
try:
|
| 59 |
r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 60 |
-
for a in soup.select('h3 a[href], .vnn-title a
|
| 61 |
t=_clean(a.get_text(strip=True));href=a.get('href','')
|
| 62 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 63 |
if not href.startswith('http'):href='https://vietnamnet.vn'+href
|
|
@@ -66,10 +60,10 @@ def _s_vietnamnet(topic,limit=6):
|
|
| 66 |
except:pass
|
| 67 |
return items
|
| 68 |
def _s_bongda(topic,limit=5):
|
| 69 |
-
items=[]
|
| 70 |
try:
|
| 71 |
r=req.get(f"https://bongda.com.vn/tim-kiem.html?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 72 |
-
for a in soup.select('h3 a[href], .title a[href]
|
| 73 |
t=_clean(a.get_text(strip=True));href=a.get('href','')
|
| 74 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 75 |
if not href.startswith('http'):href='https://bongda.com.vn'+href
|
|
@@ -78,7 +72,7 @@ def _s_bongda(topic,limit=5):
|
|
| 78 |
except:pass
|
| 79 |
return items
|
| 80 |
def _s_genk(topic,limit=5):
|
| 81 |
-
items=[]
|
| 82 |
try:
|
| 83 |
r=req.get(f"https://genk.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 84 |
for a in soup.select('a[href$=".chn"]')[:limit*3]:
|
|
@@ -90,10 +84,10 @@ def _s_genk(topic,limit=5):
|
|
| 90 |
except:pass
|
| 91 |
return items
|
| 92 |
def _s_thanhnien(topic,limit=6):
|
| 93 |
-
items=[]
|
| 94 |
try:
|
| 95 |
r=req.get(f"https://thanhnien.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 96 |
-
for a in soup.select('h3 a[href], .box-title a
|
| 97 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 98 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 99 |
if not href.startswith('http'):href='https://thanhnien.vn'+href
|
|
@@ -102,10 +96,10 @@ def _s_thanhnien(topic,limit=6):
|
|
| 102 |
except:pass
|
| 103 |
return items
|
| 104 |
def _s_tuoitre(topic,limit=6):
|
| 105 |
-
items=[]
|
| 106 |
try:
|
| 107 |
r=req.get(f"https://tuoitre.vn/tim-kiem.htm?keywords={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 108 |
-
for a in soup.select('h3 a[href], .box-title-text a
|
| 109 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 110 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 111 |
if not href.startswith('http'):href='https://tuoitre.vn'+href
|
|
@@ -114,10 +108,10 @@ def _s_tuoitre(topic,limit=6):
|
|
| 114 |
except:pass
|
| 115 |
return items
|
| 116 |
def _s_thethaovanhoa(topic,limit=5):
|
| 117 |
-
items=[]
|
| 118 |
try:
|
| 119 |
r=req.get(f"https://thethaovanhoa.vn/tim-kiem.htm?keyword={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 120 |
-
for a in soup.select('h3 a[href], .title a[href]
|
| 121 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 122 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 123 |
if not href.startswith('http'):href='https://thethaovanhoa.vn'+href
|
|
@@ -125,35 +119,27 @@ def _s_thethaovanhoa(topic,limit=5):
|
|
| 125 |
if len(items)>=limit:break
|
| 126 |
except:pass
|
| 127 |
return items
|
| 128 |
-
|
| 129 |
-
def _search_all(topic, limit=36):
|
| 130 |
results={}
|
| 131 |
with ThreadPoolExecutor(8) as ex:
|
| 132 |
futs={ex.submit(_s_vnexpress,topic,8):'vne',ex.submit(_s_dantri,topic,8):'dt',ex.submit(_s_vietnamnet,topic,6):'vnn',ex.submit(_s_bongda,topic,5):'bd',ex.submit(_s_genk,topic,5):'gk',ex.submit(_s_thanhnien,topic,6):'tn',ex.submit(_s_tuoitre,topic,6):'tt',ex.submit(_s_thethaovanhoa,topic,5):'tvh'}
|
| 133 |
for f in as_completed(futs,timeout=14):
|
| 134 |
try:results[futs[f]]=f.result()
|
| 135 |
except:results[futs[f]]=[]
|
| 136 |
-
srcs=list(results.values());
|
| 137 |
-
mx=max((len(s) for s in srcs),default=0)
|
| 138 |
for i in range(mx):
|
| 139 |
for s in srcs:
|
| 140 |
-
if i<len(s):
|
| 141 |
-
|
| 142 |
-
if url and url not in seen:seen.add(url);interleaved.append(s[i])
|
| 143 |
-
return interleaved[:limit]
|
| 144 |
-
|
| 145 |
-
# === ARTICLE ===
|
| 146 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
|
| 147 |
def _scrape_generic(url):
|
| 148 |
try:
|
| 149 |
-
r=req.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True)
|
| 150 |
-
r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
|
| 151 |
for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
|
| 152 |
h1=soup.find('h1');ogt=soup.find('meta',property='og:title');title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
|
| 153 |
ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
|
| 154 |
ogi=soup.find('meta',property='og:image');og_img=ogi.get('content','') if ogi else ''
|
| 155 |
if og_img and og_img.startswith('//'):og_img='https:'+og_img
|
| 156 |
-
if og_img and 'cdnphoto.dantri' in og_img:og_img='/api/proxy/img?url='+quote(og_img,safe='')
|
| 157 |
block=None
|
| 158 |
for sel in['article','.singular-content','.detail-content','.fck_detail','.content-detail','.knc-content','main','.cms-body','.article__body']:
|
| 159 |
el=soup.select_one(sel)
|
|
@@ -165,13 +151,10 @@ def _scrape_generic(url):
|
|
| 165 |
elif el.name in('h2','h3'):t=el.get_text(strip=True);(body.append({'type':'heading','text':t}) if t else None)
|
| 166 |
elif el.name in('figure','img'):
|
| 167 |
im=el if el.name=='img' else el.find('img')
|
| 168 |
-
if im:
|
| 169 |
-
src=im.get('data-src') or im.get('src') or im.get('data-original') or''
|
| 170 |
-
if src and'base64' not in src:body.append({'type':'img','src':'https:'+src if src.startswith('//') else src})
|
| 171 |
if not body and summary:body=[{'type':'p','text':summary}]
|
| 172 |
return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
|
| 173 |
except:return None
|
| 174 |
-
|
| 175 |
@app.get('/api/article')
|
| 176 |
def api_article_v2(url:str=Query(...)):
|
| 177 |
from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
|
|
@@ -184,56 +167,35 @@ def api_article_v2(url:str=Query(...)):
|
|
| 184 |
if data and data.get('body'):return JSONResponse(data)
|
| 185 |
data=_scrape_generic(url)
|
| 186 |
return JSONResponse(data if data else{'error':'Không đọc được','url':url})
|
| 187 |
-
|
| 188 |
-
# === HOT TOPICS — SORTED BY FREQUENCY (most mentioned = hottest = first) ===
|
| 189 |
_hot_cache={'t':0,'d':[]}
|
| 190 |
def _get_hot_topics():
|
| 191 |
now=time.time()
|
| 192 |
if _hot_cache['d'] and now-_hot_cache['t']<600:return _hot_cache['d']
|
| 193 |
-
|
| 194 |
-
# Count how many times each phrase appears across ALL feeds
|
| 195 |
freq={};display={}
|
| 196 |
feeds=['https://vnexpress.net/rss/tin-moi-nhat.rss','https://dantri.com.vn/rss/home.rss','https://vietnamnet.vn/rss/tin-moi-nhat.rss','https://thanhnien.vn/rss/home.rss','https://tuoitre.vn/rss/tin-moi-nhat.rss','https://genk.vn/rss','https://vnexpress.net/rss/the-thao.rss','https://thethaovanhoa.vn/rss/tin-nong.rss']
|
| 197 |
-
|
| 198 |
for feed_url in feeds:
|
| 199 |
try:
|
| 200 |
r=req.get(feed_url,headers={'User-Agent':'Mozilla/5.0'},timeout=6);r.encoding='utf-8';soup=BeautifulSoup(r.text,'xml')
|
| 201 |
for item in soup.find_all('item')[:12]:
|
| 202 |
title=_clean(item.find('title').get_text() if item.find('title') else '')
|
| 203 |
if not title:continue
|
| 204 |
-
title=re.sub(r'\s*[-|].*$','',title)
|
| 205 |
-
words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title) if len(w)>2 and w.lower() not in _STOP]
|
| 206 |
if len(words)<2:continue
|
| 207 |
-
# Generate phrases and count
|
| 208 |
for n in(3,4,2):
|
| 209 |
for i in range(max(0,len(words)-n+1)):
|
| 210 |
phrase=' '.join(words[i:i+n])
|
| 211 |
-
if
|
| 212 |
-
key=phrase.lower()
|
| 213 |
-
freq[key]=freq.get(key,0)+1
|
| 214 |
-
display[key]=phrase
|
| 215 |
except:continue
|
| 216 |
-
|
| 217 |
-
# Sort by frequency (most mentioned = hottest)
|
| 218 |
-
ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True)
|
| 219 |
-
|
| 220 |
-
topics=[];seen=set()
|
| 221 |
for key,count in ranked:
|
| 222 |
-
# Skip duplicates (overlapping phrases)
|
| 223 |
is_dup=any(len(set(e.split())&set(key.split()))/max(len(set(e.split())),len(set(key.split())),1)>0.6 for e in seen)
|
| 224 |
if is_dup:continue
|
| 225 |
-
seen.add(key)
|
| 226 |
-
phrase=display[key]
|
| 227 |
-
topics.append({'label':'#'+re.sub(r'\s+','',phrase.title()),'topic':phrase,'count':count})
|
| 228 |
if len(topics)>=20:break
|
| 229 |
-
|
| 230 |
-
# Add fallbacks
|
| 231 |
-
for kw in['World Cup 2026','Kinh tế Việt Nam','Bóng đá châu Âu','Công nghệ AI','Giá vàng','Thời tiết','An ninh mạng']:
|
| 232 |
if len(topics)>=24:break
|
| 233 |
if not any(kw.lower() in s for s in seen):topics.append({'label':'#'+re.sub(r'\s+','',kw.title()),'topic':kw,'count':0})
|
| 234 |
-
|
| 235 |
_hot_cache.update({'t':now,'d':topics[:24]});return topics[:24]
|
| 236 |
-
|
| 237 |
@app.get('/api/hot_topics')
|
| 238 |
def api_hot_topics():return JSONResponse({'topics':_get_hot_topics()})
|
| 239 |
@app.get('/')
|
|
@@ -243,21 +205,16 @@ async def serve_index():
|
|
| 243 |
return HTMLResponse('<h1>VNEWS</h1>')
|
| 244 |
@app.get('/api/hashtag/sources')
|
| 245 |
def _ht(topic:str=Query(...),page:int=Query(default=0)):
|
| 246 |
-
items=_search_all(topic,36)
|
| 247 |
-
per_page=8;start=page*per_page;end=start+per_page
|
| 248 |
return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
|
| 249 |
@app.get('/api/categories')
|
| 250 |
def _cat():return JSONResponse([])
|
| 251 |
@app.get('/api/storage_status')
|
| 252 |
def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
|
| 253 |
@app.get('/s')
|
| 254 |
-
async def _sh(url:str='',title:str='',img:str=''):
|
| 255 |
-
return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body>Redirecting...</body></html>')
|
| 256 |
-
|
| 257 |
-
# Interactions
|
| 258 |
DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
|
| 259 |
-
os.makedirs(DATA_DIR,exist_ok=True)
|
| 260 |
-
IF=os.path.join(DATA_DIR,'interactions_v2.json');CF=os.path.join(DATA_DIR,'comments_v2.json')
|
| 261 |
_il=threading.Lock();_cl=threading.Lock()
|
| 262 |
def _lj(p):
|
| 263 |
try:
|
|
@@ -286,8 +243,6 @@ async def _pc(request:Request):
|
|
| 286 |
with _cl:db=_lj(CF);db.setdefault(v,[]);db[v].append(c);db[v]=db[v][-200:];_sj(CF,db);cms=db[v]
|
| 287 |
with _il:idb=_lj(IF);idb.setdefault(v,{'views':0,'likes':0,'comments':0});idb[v]['comments']=len(cms);_sj(IF,idb)
|
| 288 |
return JSONResponse({'comments':cms})
|
| 289 |
-
|
| 290 |
-
# WC2026
|
| 291 |
from wc2026_scraper import(scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail)
|
| 292 |
@app.get('/api/wc2026')
|
| 293 |
def _w():return JSONResponse(get_wc2026_all())
|
|
|
|
| 1 |
+
"""VNEWS v2 Entry Point - with friendly highlights"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import sys, os
|
| 3 |
+
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
|
| 4 |
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
| 5 |
from fastapi.staticfiles import StaticFiles
|
| 6 |
from starlette.routing import Mount
|
|
|
|
| 11 |
import re, html as html_lib, json, threading, time
|
| 12 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 13 |
|
| 14 |
+
# Add friendly league for xemlaibongda.top highlights
|
| 15 |
+
HL_LEAGUES['friendly'] = {"path": "giao-huu/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 16 |
+
|
| 17 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
| 18 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
| 19 |
app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
|
| 20 |
app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
|
|
|
| 21 |
def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 22 |
_STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
|
|
|
|
| 23 |
def _has_kw(topic,title):
|
| 24 |
tl=topic.lower();tt=(title or'').lower()
|
| 25 |
if tl in tt:return True
|
| 26 |
words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',tl) if len(w)>2 and w not in _STOP]
|
| 27 |
if not words:return True
|
| 28 |
return any(w in tt for w in words)
|
|
|
|
| 29 |
def _s_vnexpress(topic,limit=8):
|
| 30 |
+
items=[];
|
| 31 |
try:
|
| 32 |
r=req.get(f"https://timkiem.vnexpress.net/?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 33 |
for art in soup.select('article.item-news')[:limit]:
|
| 34 |
a=art.select_one('h2 a, h3 a')
|
| 35 |
+
if a and a.get('href'):t=_clean(a.get('title','') or a.get_text(strip=True));items.append({'title':t,'url':a['href'],'via':'VnExpress'}) if _has_kw(topic,t) else None
|
|
|
|
|
|
|
| 36 |
except:pass
|
| 37 |
return items
|
| 38 |
def _s_dantri(topic,limit=8):
|
| 39 |
+
items=[];
|
| 40 |
try:
|
| 41 |
r=req.get(f"https://dantri.com.vn/tim-kiem/{quote(topic)}.htm",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 42 |
for a in soup.select('h3 a[href], .article-title a[href]')[:limit*2]:
|
|
|
|
| 48 |
except:pass
|
| 49 |
return items
|
| 50 |
def _s_vietnamnet(topic,limit=6):
|
| 51 |
+
items=[];
|
| 52 |
try:
|
| 53 |
r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 54 |
+
for a in soup.select('h3 a[href], .vnn-title a')[:limit*2]:
|
| 55 |
t=_clean(a.get_text(strip=True));href=a.get('href','')
|
| 56 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 57 |
if not href.startswith('http'):href='https://vietnamnet.vn'+href
|
|
|
|
| 60 |
except:pass
|
| 61 |
return items
|
| 62 |
def _s_bongda(topic,limit=5):
|
| 63 |
+
items=[];
|
| 64 |
try:
|
| 65 |
r=req.get(f"https://bongda.com.vn/tim-kiem.html?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 66 |
+
for a in soup.select('h3 a[href], .title a[href]')[:limit*2]:
|
| 67 |
t=_clean(a.get_text(strip=True));href=a.get('href','')
|
| 68 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 69 |
if not href.startswith('http'):href='https://bongda.com.vn'+href
|
|
|
|
| 72 |
except:pass
|
| 73 |
return items
|
| 74 |
def _s_genk(topic,limit=5):
|
| 75 |
+
items=[];
|
| 76 |
try:
|
| 77 |
r=req.get(f"https://genk.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 78 |
for a in soup.select('a[href$=".chn"]')[:limit*3]:
|
|
|
|
| 84 |
except:pass
|
| 85 |
return items
|
| 86 |
def _s_thanhnien(topic,limit=6):
|
| 87 |
+
items=[];
|
| 88 |
try:
|
| 89 |
r=req.get(f"https://thanhnien.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 90 |
+
for a in soup.select('h3 a[href], .box-title a')[:limit*2]:
|
| 91 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 92 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 93 |
if not href.startswith('http'):href='https://thanhnien.vn'+href
|
|
|
|
| 96 |
except:pass
|
| 97 |
return items
|
| 98 |
def _s_tuoitre(topic,limit=6):
|
| 99 |
+
items=[];
|
| 100 |
try:
|
| 101 |
r=req.get(f"https://tuoitre.vn/tim-kiem.htm?keywords={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
|
| 102 |
+
for a in soup.select('h3 a[href], .box-title-text a')[:limit*2]:
|
| 103 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 104 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 105 |
if not href.startswith('http'):href='https://tuoitre.vn'+href
|
|
|
|
| 108 |
except:pass
|
| 109 |
return items
|
| 110 |
def _s_thethaovanhoa(topic,limit=5):
|
| 111 |
+
items=[];
|
| 112 |
try:
|
| 113 |
r=req.get(f"https://thethaovanhoa.vn/tim-kiem.htm?keyword={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
|
| 114 |
+
for a in soup.select('h3 a[href], .title a[href]')[:limit*2]:
|
| 115 |
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 116 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 117 |
if not href.startswith('http'):href='https://thethaovanhoa.vn'+href
|
|
|
|
| 119 |
if len(items)>=limit:break
|
| 120 |
except:pass
|
| 121 |
return items
|
| 122 |
+
def _search_all(topic,limit=36):
|
|
|
|
| 123 |
results={}
|
| 124 |
with ThreadPoolExecutor(8) as ex:
|
| 125 |
futs={ex.submit(_s_vnexpress,topic,8):'vne',ex.submit(_s_dantri,topic,8):'dt',ex.submit(_s_vietnamnet,topic,6):'vnn',ex.submit(_s_bongda,topic,5):'bd',ex.submit(_s_genk,topic,5):'gk',ex.submit(_s_thanhnien,topic,6):'tn',ex.submit(_s_tuoitre,topic,6):'tt',ex.submit(_s_thethaovanhoa,topic,5):'tvh'}
|
| 126 |
for f in as_completed(futs,timeout=14):
|
| 127 |
try:results[futs[f]]=f.result()
|
| 128 |
except:results[futs[f]]=[]
|
| 129 |
+
srcs=list(results.values());out=[];seen=set();mx=max((len(s) for s in srcs),default=0)
|
|
|
|
| 130 |
for i in range(mx):
|
| 131 |
for s in srcs:
|
| 132 |
+
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
|
| 133 |
+
return out[:limit]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
|
| 135 |
def _scrape_generic(url):
|
| 136 |
try:
|
| 137 |
+
r=req.get(url,headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True);r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
|
|
|
|
| 138 |
for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
|
| 139 |
h1=soup.find('h1');ogt=soup.find('meta',property='og:title');title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
|
| 140 |
ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
|
| 141 |
ogi=soup.find('meta',property='og:image');og_img=ogi.get('content','') if ogi else ''
|
| 142 |
if og_img and og_img.startswith('//'):og_img='https:'+og_img
|
|
|
|
| 143 |
block=None
|
| 144 |
for sel in['article','.singular-content','.detail-content','.fck_detail','.content-detail','.knc-content','main','.cms-body','.article__body']:
|
| 145 |
el=soup.select_one(sel)
|
|
|
|
| 151 |
elif el.name in('h2','h3'):t=el.get_text(strip=True);(body.append({'type':'heading','text':t}) if t else None)
|
| 152 |
elif el.name in('figure','img'):
|
| 153 |
im=el if el.name=='img' else el.find('img')
|
| 154 |
+
if im:src=im.get('data-src') or im.get('src') or'';(body.append({'type':'img','src':'https:'+src if src.startswith('//') else src}) if src and'base64' not in src else None)
|
|
|
|
|
|
|
| 155 |
if not body and summary:body=[{'type':'p','text':summary}]
|
| 156 |
return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
|
| 157 |
except:return None
|
|
|
|
| 158 |
@app.get('/api/article')
|
| 159 |
def api_article_v2(url:str=Query(...)):
|
| 160 |
from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
|
|
|
|
| 167 |
if data and data.get('body'):return JSONResponse(data)
|
| 168 |
data=_scrape_generic(url)
|
| 169 |
return JSONResponse(data if data else{'error':'Không đọc được','url':url})
|
|
|
|
|
|
|
| 170 |
_hot_cache={'t':0,'d':[]}
|
| 171 |
def _get_hot_topics():
|
| 172 |
now=time.time()
|
| 173 |
if _hot_cache['d'] and now-_hot_cache['t']<600:return _hot_cache['d']
|
|
|
|
|
|
|
| 174 |
freq={};display={}
|
| 175 |
feeds=['https://vnexpress.net/rss/tin-moi-nhat.rss','https://dantri.com.vn/rss/home.rss','https://vietnamnet.vn/rss/tin-moi-nhat.rss','https://thanhnien.vn/rss/home.rss','https://tuoitre.vn/rss/tin-moi-nhat.rss','https://genk.vn/rss','https://vnexpress.net/rss/the-thao.rss','https://thethaovanhoa.vn/rss/tin-nong.rss']
|
|
|
|
| 176 |
for feed_url in feeds:
|
| 177 |
try:
|
| 178 |
r=req.get(feed_url,headers={'User-Agent':'Mozilla/5.0'},timeout=6);r.encoding='utf-8';soup=BeautifulSoup(r.text,'xml')
|
| 179 |
for item in soup.find_all('item')[:12]:
|
| 180 |
title=_clean(item.find('title').get_text() if item.find('title') else '')
|
| 181 |
if not title:continue
|
| 182 |
+
title=re.sub(r'\s*[-|].*$','',title);words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title) if len(w)>2 and w.lower() not in _STOP]
|
|
|
|
| 183 |
if len(words)<2:continue
|
|
|
|
| 184 |
for n in(3,4,2):
|
| 185 |
for i in range(max(0,len(words)-n+1)):
|
| 186 |
phrase=' '.join(words[i:i+n])
|
| 187 |
+
if 8<=len(phrase)<=45:key=phrase.lower();freq[key]=freq.get(key,0)+1;display[key]=phrase
|
|
|
|
|
|
|
|
|
|
| 188 |
except:continue
|
| 189 |
+
ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True);topics=[];seen=set()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
for key,count in ranked:
|
|
|
|
| 191 |
is_dup=any(len(set(e.split())&set(key.split()))/max(len(set(e.split())),len(set(key.split())),1)>0.6 for e in seen)
|
| 192 |
if is_dup:continue
|
| 193 |
+
seen.add(key);topics.append({'label':'#'+re.sub(r'\s+','',display[key].title()),'topic':display[key],'count':count})
|
|
|
|
|
|
|
| 194 |
if len(topics)>=20:break
|
| 195 |
+
for kw in['World Cup 2026','Kinh tế Việt Nam','Bóng đá châu Âu','Công nghệ AI','Giá vàng','Thời tiết']:
|
|
|
|
|
|
|
| 196 |
if len(topics)>=24:break
|
| 197 |
if not any(kw.lower() in s for s in seen):topics.append({'label':'#'+re.sub(r'\s+','',kw.title()),'topic':kw,'count':0})
|
|
|
|
| 198 |
_hot_cache.update({'t':now,'d':topics[:24]});return topics[:24]
|
|
|
|
| 199 |
@app.get('/api/hot_topics')
|
| 200 |
def api_hot_topics():return JSONResponse({'topics':_get_hot_topics()})
|
| 201 |
@app.get('/')
|
|
|
|
| 205 |
return HTMLResponse('<h1>VNEWS</h1>')
|
| 206 |
@app.get('/api/hashtag/sources')
|
| 207 |
def _ht(topic:str=Query(...),page:int=Query(default=0)):
|
| 208 |
+
items=_search_all(topic,36);per_page=8;start=page*per_page;end=start+per_page
|
|
|
|
| 209 |
return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
|
| 210 |
@app.get('/api/categories')
|
| 211 |
def _cat():return JSONResponse([])
|
| 212 |
@app.get('/api/storage_status')
|
| 213 |
def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
|
| 214 |
@app.get('/s')
|
| 215 |
+
async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
|
|
|
|
|
|
|
|
|
|
| 216 |
DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
|
| 217 |
+
os.makedirs(DATA_DIR,exist_ok=True);IF=os.path.join(DATA_DIR,'interactions_v2.json');CF=os.path.join(DATA_DIR,'comments_v2.json')
|
|
|
|
| 218 |
_il=threading.Lock();_cl=threading.Lock()
|
| 219 |
def _lj(p):
|
| 220 |
try:
|
|
|
|
| 243 |
with _cl:db=_lj(CF);db.setdefault(v,[]);db[v].append(c);db[v]=db[v][-200:];_sj(CF,db);cms=db[v]
|
| 244 |
with _il:idb=_lj(IF);idb.setdefault(v,{'views':0,'likes':0,'comments':0});idb[v]['comments']=len(cms);_sj(IF,idb)
|
| 245 |
return JSONResponse({'comments':cms})
|
|
|
|
|
|
|
| 246 |
from wc2026_scraper import(scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail)
|
| 247 |
@app.get('/api/wc2026')
|
| 248 |
def _w():return JSONResponse(get_wc2026_all())
|