bep40 commited on
Commit
4532efb
·
verified ·
1 Parent(s): 8e10e01

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
Files changed (1) hide show
  1. 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.get('title','') or a.get_text(strip=True));href=a.get('href','')
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
- items=_search_all(topic,36);per_page=8;start=page*per_page;end=start+per_page
 
 
 
 
 
 
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 posts)
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')