Spaces:
Running
Running
Fix VnEgo (use /video page), DanTri always OG image, frontend DanTri in-app read
Browse files
main.py
CHANGED
|
@@ -25,8 +25,8 @@ def _get(url,headers=None):
|
|
| 25 |
h=headers or HEADERS;r=requests.get(url,headers=h,timeout=15);r.encoding="utf-8"
|
| 26 |
return BeautifulSoup(r.text,"lxml")
|
| 27 |
|
| 28 |
-
# ===== DANTRI =====
|
| 29 |
def scrape_dantri_hot():
|
|
|
|
| 30 |
try:
|
| 31 |
soup=_get("https://dantri.com.vn/tin-nong.htm")
|
| 32 |
arts=[];seen=set()
|
|
@@ -37,36 +37,27 @@ def scrape_dantri_hot():
|
|
| 37 |
if href in seen or not href.endswith(".htm"):continue
|
| 38 |
if href=="https://dantri.com.vn/tin-nong.htm":continue
|
| 39 |
seen.add(href)
|
| 40 |
-
|
| 41 |
-
if
|
| 42 |
-
|
| 43 |
-
if img:img_src=img.get("data-src") or img.get("src","")
|
| 44 |
-
if img_src and("base64" in img_src or len(img_src)<10):img_src=""
|
| 45 |
-
arts.append({"title":title,"link":href,"img":img_src,"source":"dantri"})
|
| 46 |
-
if len(arts)>=15:break
|
| 47 |
def _og(art):
|
| 48 |
-
if art["img"]:return
|
| 49 |
try:
|
| 50 |
r2=requests.get(art["link"],headers=HEADERS,timeout=8);r2.encoding="utf-8"
|
| 51 |
og=BeautifulSoup(r2.text,"lxml").find("meta",property="og:image")
|
| 52 |
art["img"]=og.get("content","") if og else ""
|
| 53 |
except:pass
|
| 54 |
-
|
| 55 |
-
if
|
| 56 |
-
with ThreadPoolExecutor(3) as ex:list(ex.map(_og,need))
|
| 57 |
-
return arts[:12]
|
| 58 |
except:return[]
|
| 59 |
|
| 60 |
def scrape_dantri_article(url):
|
| 61 |
-
"""Parse dantri.com.vn article for in-app reading."""
|
| 62 |
try:
|
| 63 |
r=requests.get(url,headers=HEADERS,timeout=15);r.encoding="utf-8"
|
| 64 |
soup=BeautifulSoup(r.text,"lxml")
|
| 65 |
for tag in soup.find_all(["script","style","nav","footer","aside"]):tag.decompose()
|
| 66 |
h1=soup.find("h1")
|
| 67 |
og=soup.find("meta",property="og:image");og_img=og.get("content","") if og else ""
|
| 68 |
-
|
| 69 |
-
content=soup.select_one("div.singular-content") or soup.select_one("div.e-magazine__body") or soup.select_one("article.e-magazine") or soup.select_one("div.detail-content")
|
| 70 |
body=[]
|
| 71 |
if content:
|
| 72 |
for el in content.find_all(["p","h2","h3","figure","img"],recursive=True):
|
|
@@ -81,7 +72,6 @@ def scrape_dantri_article(url):
|
|
| 81 |
if im:
|
| 82 |
s=im.get("data-src") or im.get("src","")
|
| 83 |
if s and"base64" not in s and len(s)>10:body.append({"type":"img","src":s,"alt":im.get("alt","")})
|
| 84 |
-
# Fallback: find all p in page if content block empty
|
| 85 |
if not body:
|
| 86 |
for p in soup.find_all("p"):
|
| 87 |
t=p.get_text(strip=True)
|
|
@@ -93,32 +83,50 @@ def scrape_dantri_article(url):
|
|
| 93 |
return{"title":h1.get_text(strip=True) if h1 else "","summary":desc,"og_image":og_img,"body":body,"source":"dantri","url":url}
|
| 94 |
except:return None
|
| 95 |
|
| 96 |
-
# ===== VNEXPRESS VIDEO (VnEgo) =====
|
| 97 |
def scrape_vne_video():
|
| 98 |
-
"""Scrape VnExpress
|
| 99 |
try:
|
| 100 |
-
soup=_get("https://vnexpress.net/
|
| 101 |
arts=[];seen=set()
|
| 102 |
-
for
|
|
|
|
|
|
|
| 103 |
href=a.get("href","");title=a.get("title","") or a.get_text(strip=True)
|
| 104 |
-
if not title or len(title)<8 or
|
| 105 |
-
if not href.startswith("http"):continue
|
| 106 |
-
if href in seen:continue
|
| 107 |
-
if"/video/" not in href and"/vne-go/" not in href:continue
|
| 108 |
seen.add(href)
|
| 109 |
-
|
| 110 |
-
if
|
| 111 |
-
|
| 112 |
-
if parent:img=parent.find("img")
|
| 113 |
-
img_src=""
|
| 114 |
-
if img:
|
| 115 |
-
img_src=img.get("data-src") or img.get("src","")
|
| 116 |
if img_src and"blank" in img_src:
|
| 117 |
-
source=
|
| 118 |
if source:img_src=source.get("srcset","").split(",")[0].strip().split(" ")[0]
|
| 119 |
-
if not img_src or len(img_src)<10:img_src=""
|
| 120 |
arts.append({"title":title,"link":href,"img":img_src,"source":"vne-video"})
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
return arts[:12]
|
| 123 |
except:return[]
|
| 124 |
|
|
@@ -280,7 +288,6 @@ def extract_bdp_video(url):
|
|
| 280 |
return{"src":source.get("src","") if source else "","poster":video.get("poster","")}
|
| 281 |
except:return None
|
| 282 |
|
| 283 |
-
# All VnExpress categories
|
| 284 |
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"cong-nghe":("https://vnexpress.net/so-hoa","Công Nghệ"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-tri","Giải Trí"),"suc-khoe":("https://vnexpress.net/suc-khoe","Sức Khỏe"),"giao-duc":("https://vnexpress.net/giao-duc","Giáo Dục"),"phap-luat":("https://vnexpress.net/phap-luat","Pháp Luật"),"du-lich":("https://vnexpress.net/du-lich","Du Lịch"),"xe":("https://vnexpress.net/oto-xe-may","Xe"),"doi-song":("https://vnexpress.net/doi-song","Đời Sống")}
|
| 285 |
|
| 286 |
@app.get("/api/homepage")
|
|
@@ -288,7 +295,6 @@ def api_homepage():
|
|
| 288 |
def _f():
|
| 289 |
articles=[]
|
| 290 |
with ThreadPoolExecutor(8) as ex:
|
| 291 |
-
# Load ALL VNE categories for homepage
|
| 292 |
futs={ex.submit(scrape_vne,VNE_CATS[k][0]):VNE_CATS[k][1] for k in VNE_CATS}
|
| 293 |
futs[ex.submit(scrape_bbc_vietnamese)]="BBC"
|
| 294 |
for f in as_completed(futs):
|
|
|
|
| 25 |
h=headers or HEADERS;r=requests.get(url,headers=h,timeout=15);r.encoding="utf-8"
|
| 26 |
return BeautifulSoup(r.text,"lxml")
|
| 27 |
|
|
|
|
| 28 |
def scrape_dantri_hot():
|
| 29 |
+
"""Scrape DanTri tin nong - always fetch OG image for each article."""
|
| 30 |
try:
|
| 31 |
soup=_get("https://dantri.com.vn/tin-nong.htm")
|
| 32 |
arts=[];seen=set()
|
|
|
|
| 37 |
if href in seen or not href.endswith(".htm"):continue
|
| 38 |
if href=="https://dantri.com.vn/tin-nong.htm":continue
|
| 39 |
seen.add(href)
|
| 40 |
+
arts.append({"title":title,"link":href,"img":"","source":"dantri"})
|
| 41 |
+
if len(arts)>=12:break
|
| 42 |
+
# Always fetch OG image for ALL DanTri articles
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def _og(art):
|
|
|
|
| 44 |
try:
|
| 45 |
r2=requests.get(art["link"],headers=HEADERS,timeout=8);r2.encoding="utf-8"
|
| 46 |
og=BeautifulSoup(r2.text,"lxml").find("meta",property="og:image")
|
| 47 |
art["img"]=og.get("content","") if og else ""
|
| 48 |
except:pass
|
| 49 |
+
with ThreadPoolExecutor(4) as ex:list(ex.map(_og,arts[:10]))
|
| 50 |
+
return [a for a in arts if a["img"]][:10]
|
|
|
|
|
|
|
| 51 |
except:return[]
|
| 52 |
|
| 53 |
def scrape_dantri_article(url):
|
|
|
|
| 54 |
try:
|
| 55 |
r=requests.get(url,headers=HEADERS,timeout=15);r.encoding="utf-8"
|
| 56 |
soup=BeautifulSoup(r.text,"lxml")
|
| 57 |
for tag in soup.find_all(["script","style","nav","footer","aside"]):tag.decompose()
|
| 58 |
h1=soup.find("h1")
|
| 59 |
og=soup.find("meta",property="og:image");og_img=og.get("content","") if og else ""
|
| 60 |
+
content=soup.select_one("div.singular-content") or soup.select_one("div.e-magazine__body") or soup.select_one("article")
|
|
|
|
| 61 |
body=[]
|
| 62 |
if content:
|
| 63 |
for el in content.find_all(["p","h2","h3","figure","img"],recursive=True):
|
|
|
|
| 72 |
if im:
|
| 73 |
s=im.get("data-src") or im.get("src","")
|
| 74 |
if s and"base64" not in s and len(s)>10:body.append({"type":"img","src":s,"alt":im.get("alt","")})
|
|
|
|
| 75 |
if not body:
|
| 76 |
for p in soup.find_all("p"):
|
| 77 |
t=p.get_text(strip=True)
|
|
|
|
| 83 |
return{"title":h1.get_text(strip=True) if h1 else "","summary":desc,"og_image":og_img,"body":body,"source":"dantri","url":url}
|
| 84 |
except:return None
|
| 85 |
|
|
|
|
| 86 |
def scrape_vne_video():
|
| 87 |
+
"""Scrape VnExpress video page (server-rendered, not SPA like /vne-go)."""
|
| 88 |
try:
|
| 89 |
+
soup=_get("https://vnexpress.net/video")
|
| 90 |
arts=[];seen=set()
|
| 91 |
+
for it in soup.select("article.item-news")[:15]:
|
| 92 |
+
a=it.select_one("a[href]")
|
| 93 |
+
if not a:continue
|
| 94 |
href=a.get("href","");title=a.get("title","") or a.get_text(strip=True)
|
| 95 |
+
if not title or len(title)<8 or href in seen:continue
|
|
|
|
|
|
|
|
|
|
| 96 |
seen.add(href)
|
| 97 |
+
im=it.find("img");img_src=""
|
| 98 |
+
if im:
|
| 99 |
+
img_src=im.get("data-src") or im.get("src","")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if img_src and"blank" in img_src:
|
| 101 |
+
source=it.find("source")
|
| 102 |
if source:img_src=source.get("srcset","").split(",")[0].strip().split(" ")[0]
|
|
|
|
| 103 |
arts.append({"title":title,"link":href,"img":img_src,"source":"vne-video"})
|
| 104 |
+
# Fallback: if article.item-news not found, try generic links
|
| 105 |
+
if not arts:
|
| 106 |
+
for a in soup.find_all("a",href=True):
|
| 107 |
+
href=a.get("href","");title=a.get("title","") or a.get_text(strip=True)
|
| 108 |
+
if not title or len(title)<10:continue
|
| 109 |
+
if not href.startswith("http") or href in seen:continue
|
| 110 |
+
if"/video/" not in href:continue
|
| 111 |
+
seen.add(href)
|
| 112 |
+
img=a.find("img")
|
| 113 |
+
if not img and a.parent:img=a.parent.find("img")
|
| 114 |
+
img_src=""
|
| 115 |
+
if img:img_src=img.get("data-src") or img.get("src","")
|
| 116 |
+
if img_src and("blank" in img_src or"base64" in img_src):img_src=""
|
| 117 |
+
arts.append({"title":title,"link":href,"img":img_src,"source":"vne-video"})
|
| 118 |
+
if len(arts)>=12:break
|
| 119 |
+
# Fetch OG for missing images
|
| 120 |
+
def _og(art):
|
| 121 |
+
if art["img"]:return
|
| 122 |
+
try:
|
| 123 |
+
r2=requests.get(art["link"],headers=HEADERS,timeout=8);r2.encoding="utf-8"
|
| 124 |
+
og=BeautifulSoup(r2.text,"lxml").find("meta",property="og:image")
|
| 125 |
+
art["img"]=og.get("content","") if og else ""
|
| 126 |
+
except:pass
|
| 127 |
+
need=[a for a in arts if not a["img"]][:5]
|
| 128 |
+
if need:
|
| 129 |
+
with ThreadPoolExecutor(3) as ex:list(ex.map(_og,need))
|
| 130 |
return arts[:12]
|
| 131 |
except:return[]
|
| 132 |
|
|
|
|
| 288 |
return{"src":source.get("src","") if source else "","poster":video.get("poster","")}
|
| 289 |
except:return None
|
| 290 |
|
|
|
|
| 291 |
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"cong-nghe":("https://vnexpress.net/so-hoa","Công Nghệ"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-tri","Giải Trí"),"suc-khoe":("https://vnexpress.net/suc-khoe","Sức Khỏe"),"giao-duc":("https://vnexpress.net/giao-duc","Giáo Dục"),"phap-luat":("https://vnexpress.net/phap-luat","Pháp Luật"),"du-lich":("https://vnexpress.net/du-lich","Du Lịch"),"xe":("https://vnexpress.net/oto-xe-may","Xe"),"doi-song":("https://vnexpress.net/doi-song","Đời Sống")}
|
| 292 |
|
| 293 |
@app.get("/api/homepage")
|
|
|
|
| 295 |
def _f():
|
| 296 |
articles=[]
|
| 297 |
with ThreadPoolExecutor(8) as ex:
|
|
|
|
| 298 |
futs={ex.submit(scrape_vne,VNE_CATS[k][0]):VNE_CATS[k][1] for k in VNE_CATS}
|
| 299 |
futs[ex.submit(scrape_bbc_vietnamese)]="BBC"
|
| 300 |
for f in as_completed(futs):
|