Spaces:
Running
Running
Add AI-specific scrapers: GenK AI (ai.chn), Tinhte, VatvoStudio, KhoaHoc.tv for AI Thế Giới / AI Việt Nam topics - sorted by newest first
Browse files- app_v2_entry.py +169 -4
app_v2_entry.py
CHANGED
|
@@ -219,6 +219,165 @@ def _s_date(el):
|
|
| 219 |
if dt: return _clean(dt.get_text(strip=True))[:20]
|
| 220 |
return ''
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
def _s_vnexpress(topic,limit=8):
|
| 223 |
items=[]
|
| 224 |
try:
|
|
@@ -301,7 +460,7 @@ def _s_tuoitre(topic,limit=6):
|
|
| 301 |
try:
|
| 302 |
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')
|
| 303 |
for a in soup.select('h3 a[href], .box-title-text a')[:limit*2]:
|
| 304 |
-
t=_clean(a.
|
| 305 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 306 |
if not href.startswith('http'):href='https://tuoitre.vn'+href
|
| 307 |
items.append({'title':t,'url':href,'via':'Tuổi Trẻ','date':_s_date(a.parent or a)})
|
|
@@ -538,7 +697,13 @@ async def serve_index():
|
|
| 538 |
return HTMLResponse('<h1>VNEWS</h1>')
|
| 539 |
@app.get('/api/hashtag/sources')
|
| 540 |
def _ht(topic:str=Query(...),page:int=Query(default=0)):
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
|
| 543 |
@app.get('/api/categories')
|
| 544 |
def _cat():return JSONResponse([])
|
|
@@ -1603,7 +1768,7 @@ async def _auto_rewrite_one(topic, slot_label, used_urls=None, post_index=0):
|
|
| 1603 |
if not items or post_index >= len(items):
|
| 1604 |
return False
|
| 1605 |
|
| 1606 |
-
# Get article at post_index (0,1,2 for multiple
|
| 1607 |
item = items[post_index] # post_index allows multiple articles per topic
|
| 1608 |
url = item.get('url', '')
|
| 1609 |
title = item.get('title', topic)
|
|
@@ -1904,4 +2069,4 @@ def shorts_rss():
|
|
| 1904 |
|
| 1905 |
return {"shorts": shorts, "count": len(shorts)}
|
| 1906 |
|
| 1907 |
-
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|
|
|
|
| 219 |
if dt: return _clean(dt.get_text(strip=True))[:20]
|
| 220 |
return ''
|
| 221 |
|
| 222 |
+
# ===== AI-specific scrapers (GenK AI, Tinhte, VatvoStudio, KhoaHoc.tv) =====
|
| 223 |
+
def _s_genk_ai(limit=10):
|
| 224 |
+
"""Scrape GenK AI section (ai.chn) for the latest AI articles."""
|
| 225 |
+
items=[]
|
| 226 |
+
try:
|
| 227 |
+
r=req.get("https://genk.vn/ai.chn", headers={'User-Agent':'Mozilla/5.0'}, timeout=10)
|
| 228 |
+
if r.status_code!=200: return items
|
| 229 |
+
r.encoding="utf-8"; soup=BeautifulSoup(r.text,'lxml')
|
| 230 |
+
for a in soup.find_all('a', href=True)[:limit*3]:
|
| 231 |
+
href=a.get('href','')
|
| 232 |
+
if not href.endswith('.chn') or href=='/ai.chn': continue
|
| 233 |
+
if href.startswith('/'): href='https://genk.vn'+href
|
| 234 |
+
t=_clean(a.get('title','') or a.get_text(strip=True))
|
| 235 |
+
if not t or len(t)<15: continue
|
| 236 |
+
# Extract date from article page
|
| 237 |
+
date_str = ''
|
| 238 |
+
img_src = ''
|
| 239 |
+
# Get date from parent block
|
| 240 |
+
date_el = a.parent
|
| 241 |
+
for _ in range(5):
|
| 242 |
+
if date_el is None: break
|
| 243 |
+
dt=date_el.find('time') or date_el.select_one('.date, .time, span.date, .meta-date')
|
| 244 |
+
if dt:
|
| 245 |
+
date_str=_clean(dt.get_text(strip=True))[:20]
|
| 246 |
+
break
|
| 247 |
+
date_el=date_el.parent
|
| 248 |
+
# Get image
|
| 249 |
+
container=a.parent
|
| 250 |
+
for _ in range(6):
|
| 251 |
+
if container is None: break
|
| 252 |
+
for img in container.find_all('img'):
|
| 253 |
+
s=img.get('data-src','') or img.get('src','')
|
| 254 |
+
if s and 'mediacdn' in s and 'avatar' not in s: img_src=s; break
|
| 255 |
+
if img_src: break; container=container.parent
|
| 256 |
+
if not img_src:
|
| 257 |
+
try:
|
| 258 |
+
og_r=req.get(href, headers={'User-Agent':'Mozilla/5.0'}, timeout=5)
|
| 259 |
+
og_r.encoding='utf-8'
|
| 260 |
+
og_s=BeautifulSoup(og_r.text,'lxml')
|
| 261 |
+
og_tag=og_s.find('meta', property='og:image')
|
| 262 |
+
if og_tag: img_src=og_tag.get('content','')
|
| 263 |
+
meta_date=og_s.find('meta', property='article:published_time')
|
| 264 |
+
if meta_date and meta_date.get('content'): date_str=meta_date.get('content')[:19]
|
| 265 |
+
except: pass
|
| 266 |
+
items.append({'title':t[:200], 'url':href, 'via':'GenK AI', 'img':img_src, 'date': date_str})
|
| 267 |
+
if len(items)>=limit: break
|
| 268 |
+
except: pass
|
| 269 |
+
return items
|
| 270 |
+
|
| 271 |
+
def _s_tinhte_ai(limit=10):
|
| 272 |
+
"""Scrape Tinhte.vn for AI articles (tag: tri-tue-nhan-tao, ai)."""
|
| 273 |
+
items=[]
|
| 274 |
+
seen_urls=set()
|
| 275 |
+
for tag in ['tri-tue-nhan-tao', 'ai']:
|
| 276 |
+
try:
|
| 277 |
+
r=req.get(f"https://tinhte.vn/tags/{tag}/", headers={'User-Agent':'Mozilla/5.0'}, timeout=10)
|
| 278 |
+
if r.status_code!=200: continue
|
| 279 |
+
r.encoding='utf-8'; soup=BeautifulSoup(r.text,'lxml')
|
| 280 |
+
for a in soup.find_all('a', href=True)[:limit*2]:
|
| 281 |
+
href=a.get('href','')
|
| 282 |
+
if 'tinhte.vn' not in href and not href.startswith('/threads/'): continue
|
| 283 |
+
if href.startswith('/'): href='https://tinhte.vn'+href
|
| 284 |
+
if href in seen_urls: continue
|
| 285 |
+
t=_clean(a.get('title','') or a.get_text(strip=True))
|
| 286 |
+
if not t or len(t)<15: continue
|
| 287 |
+
seen_urls.add(href)
|
| 288 |
+
img_src=''
|
| 289 |
+
img=a.find('img') or (a.parent.find('img') if a.parent else None)
|
| 290 |
+
if img: img_src=img.get('data-src','') or img.get('src','') or ''
|
| 291 |
+
date_str=_s_date(a.parent or a)
|
| 292 |
+
items.append({'title':t[:200], 'url':href, 'via':'Tinhte', 'img':img_src, 'date':date_str})
|
| 293 |
+
if len(items)>=limit: break
|
| 294 |
+
except: pass
|
| 295 |
+
return items[:limit]
|
| 296 |
+
|
| 297 |
+
def _s_vatvostudio_ai(limit=10):
|
| 298 |
+
"""Scrape VatvoStudio for AI articles."""
|
| 299 |
+
items=[]
|
| 300 |
+
try:
|
| 301 |
+
r=req.get("https://vatvostudio.vn/", headers={'User-Agent':'Mozilla/5.0'}, timeout=10)
|
| 302 |
+
if r.status_code!=200: return items
|
| 303 |
+
r.encoding='utf-8'; soup=BeautifulSoup(r.text,'lxml')
|
| 304 |
+
for a in soup.find_all('a', href=True)[:limit*4]:
|
| 305 |
+
href=a.get('href','')
|
| 306 |
+
if 'vatvostudio.vn' not in href and not href.startswith('/'): continue
|
| 307 |
+
t=_clean(a.get('title','') or a.get_text(strip=True))
|
| 308 |
+
if not t or len(t)<15: continue
|
| 309 |
+
ai_kw = ['ai', 'trí tuệ', 'machine learning', 'deep learning', 'chatgpt', 'gemini', 'claude', 'llm', 'openai', 'neural', 'robot', 'thông minh']
|
| 310 |
+
t_lower = t.lower()
|
| 311 |
+
if not any(kw in t_lower for kw in ai_kw): continue
|
| 312 |
+
if not href.startswith('http'): href='https://vatvostudio.vn'+href
|
| 313 |
+
img_src=''
|
| 314 |
+
img=a.find('img') or (a.parent.find('img') if a.parent else None)
|
| 315 |
+
if img: img_src=img.get('data-src','') or img.get('src','') or ''
|
| 316 |
+
date_str=_s_date(a.parent or a)
|
| 317 |
+
items.append({'title':t[:200], 'url':href, 'via':'VatvoStudio', 'img':img_src, 'date':date_str})
|
| 318 |
+
if len(items)>=limit: break
|
| 319 |
+
except: pass
|
| 320 |
+
return items
|
| 321 |
+
|
| 322 |
+
def _s_khoahoc_tv_ai(limit=5):
|
| 323 |
+
"""Scrape khoahoc.tv for AI articles."""
|
| 324 |
+
items=[]
|
| 325 |
+
try:
|
| 326 |
+
r=req.get("https://khoahoc.tv/tin-tuc/cong-nghe", headers={'User-Agent':'Mozilla/5.0'}, timeout=10)
|
| 327 |
+
if r.status_code!=200: return items
|
| 328 |
+
r.encoding='utf-8'; soup=BeautifulSoup(r.text,'lxml')
|
| 329 |
+
for a in soup.find_all('a', href=True)[:limit*3]:
|
| 330 |
+
href=a.get('href','')
|
| 331 |
+
t=_clean(a.get('title','') or a.get_text(strip=True))
|
| 332 |
+
if not t or len(t)<15: continue
|
| 333 |
+
ai_kw = ['ai', 'trí tuệ', 'machine learning', 'chatgpt', 'gemini', 'claude', 'openai', 'robot']
|
| 334 |
+
if not any(kw in t.lower() for kw in ai_kw): continue
|
| 335 |
+
if href.startswith('/'): href='https://khoahoc.tv'+href
|
| 336 |
+
img_src=''
|
| 337 |
+
img=a.find('img') or (a.parent.find('img') if a.parent else None)
|
| 338 |
+
if img: img_src=img.get('data-src','') or img.get('src','') or ''
|
| 339 |
+
date_str=_s_date(a.parent or a)
|
| 340 |
+
items.append({'title':t[:200], 'url':href, 'via':'KhoaHoc.tv', 'img':img_src, 'date':date_str})
|
| 341 |
+
if len(items)>=limit: break
|
| 342 |
+
except: pass
|
| 343 |
+
return items
|
| 344 |
+
|
| 345 |
+
def _search_ai(topic, limit=36):
|
| 346 |
+
"""Search AI-specific sources, sorted by newest first."""
|
| 347 |
+
results={}
|
| 348 |
+
with ThreadPoolExecutor(4) as ex:
|
| 349 |
+
futs={
|
| 350 |
+
ex.submit(_s_genk_ai, 12): 'genk',
|
| 351 |
+
ex.submit(_s_tinhte_ai, 10): 'tinhte',
|
| 352 |
+
ex.submit(_s_vatvostudio_ai, 10): 'vatvo',
|
| 353 |
+
ex.submit(_s_khoahoc_tv_ai, 6): 'khtv',
|
| 354 |
+
}
|
| 355 |
+
for f in as_completed(futs, timeout=18):
|
| 356 |
+
try: results[futs[f]] = f.result()
|
| 357 |
+
except: results[futs[f]] = []
|
| 358 |
+
# Deduplicate
|
| 359 |
+
out=[]; seen=set()
|
| 360 |
+
for key in ['genk', 'tinhte', 'vatvo', 'khtv']:
|
| 361 |
+
for s in results.get(key, []):
|
| 362 |
+
if s.get('url') and s['url'] not in seen:
|
| 363 |
+
seen.add(s['url']); out.append(s)
|
| 364 |
+
# Sort by date (newest first)
|
| 365 |
+
def _date_sort_key(s):
|
| 366 |
+
d = s.get('date', '')
|
| 367 |
+
if not d: return '0000'
|
| 368 |
+
# "yyyy-mm-ddTHH:MM:SS" or "yyyy-mm-dd HH:MM:SS"
|
| 369 |
+
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', d)
|
| 370 |
+
if m: return m.group(1)+m.group(2)+m.group(3)
|
| 371 |
+
# "dd/mm/yyyy"
|
| 372 |
+
m = re.search(r'(\d{2})/(\d{2})/(\d{4})', d)
|
| 373 |
+
if m: return m.group(3)+m.group(2)+m.group(1)
|
| 374 |
+
# "X phút trước", "X giờ trước" - very recent
|
| 375 |
+
if 'phút' in d or 'giờ' in d or 'Vừa' in d: return '9999'
|
| 376 |
+
if 'Hôm qua' in d: return '9998'
|
| 377 |
+
return '0000'
|
| 378 |
+
out.sort(key=_date_sort_key, reverse=True)
|
| 379 |
+
return out[:limit]
|
| 380 |
+
|
| 381 |
def _s_vnexpress(topic,limit=8):
|
| 382 |
items=[]
|
| 383 |
try:
|
|
|
|
| 460 |
try:
|
| 461 |
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')
|
| 462 |
for a in soup.select('h3 a[href], .box-title-text a')[:limit*2]:
|
| 463 |
+
t=_clean(a.get_text(strip=True));href=a.get('href','')
|
| 464 |
if t and len(t)>15 and _has_kw(topic,t):
|
| 465 |
if not href.startswith('http'):href='https://tuoitre.vn'+href
|
| 466 |
items.append({'title':t,'url':href,'via':'Tuổi Trẻ','date':_s_date(a.parent or a)})
|
|
|
|
| 697 |
return HTMLResponse('<h1>VNEWS</h1>')
|
| 698 |
@app.get('/api/hashtag/sources')
|
| 699 |
def _ht(topic:str=Query(...),page:int=Query(default=0)):
|
| 700 |
+
ai_kw = ['ai việt nam', 'ai thế giới', 'công nghệ ai', 'trí tuệ nhân tạo', 'ai', 'trí tuệ']
|
| 701 |
+
is_ai = any(kw in topic.lower() for kw in ai_kw)
|
| 702 |
+
if is_ai:
|
| 703 |
+
items=_search_ai(topic,36)
|
| 704 |
+
else:
|
| 705 |
+
items=_search_all(topic,36)
|
| 706 |
+
per_page=8;start=page*per_page;end=start+per_page
|
| 707 |
return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
|
| 708 |
@app.get('/api/categories')
|
| 709 |
def _cat():return JSONResponse([])
|
|
|
|
| 1768 |
if not items or post_index >= len(items):
|
| 1769 |
return False
|
| 1770 |
|
| 1771 |
+
# Get article at post_index (0,1,2 for multiple articles)
|
| 1772 |
item = items[post_index] # post_index allows multiple articles per topic
|
| 1773 |
url = item.get('url', '')
|
| 1774 |
title = item.get('title', topic)
|
|
|
|
| 2069 |
|
| 2070 |
return {"shorts": shorts, "count": len(shorts)}
|
| 2071 |
|
| 2072 |
+
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|