bep40 commited on
Commit
caaf5af
·
verified ·
1 Parent(s): 4721f3c

Fix _get_hot_topics: count frequency across feeds, sort by hotness (most mentioned first)"

Browse files
Files changed (1) hide show
  1. app_v2_entry.py +42 -60
app_v2_entry.py CHANGED
@@ -1,7 +1,7 @@
1
  """
2
  VNEWS v2 Entry Point
3
- Hashtag: 8 sources (vnexpress, dantri, vietnamnet, bongda, genk, thanhnien, tuoitre, thethaovanhoa)
4
- Hot topics: from 8 RSS feeds diverse sources
5
  """
6
  import sys, os
7
  from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api
@@ -30,7 +30,6 @@ def _has_kw(topic,title):
30
  if not words:return True
31
  return any(w in tt for w in words)
32
 
33
- # === 8 SEARCH SOURCES ===
34
  def _s_vnexpress(topic,limit=8):
35
  items=[]
36
  try:
@@ -42,7 +41,6 @@ def _s_vnexpress(topic,limit=8):
42
  if _has_kw(topic,t):items.append({'title':t,'url':a['href'],'via':'VnExpress'})
43
  except:pass
44
  return items
45
-
46
  def _s_dantri(topic,limit=8):
47
  items=[]
48
  try:
@@ -55,7 +53,6 @@ def _s_dantri(topic,limit=8):
55
  if len(items)>=limit:break
56
  except:pass
57
  return items
58
-
59
  def _s_vietnamnet(topic,limit=6):
60
  items=[]
61
  try:
@@ -68,7 +65,6 @@ def _s_vietnamnet(topic,limit=6):
68
  if len(items)>=limit:break
69
  except:pass
70
  return items
71
-
72
  def _s_bongda(topic,limit=5):
73
  items=[]
74
  try:
@@ -81,7 +77,6 @@ def _s_bongda(topic,limit=5):
81
  if len(items)>=limit:break
82
  except:pass
83
  return items
84
-
85
  def _s_genk(topic,limit=5):
86
  items=[]
87
  try:
@@ -94,7 +89,6 @@ def _s_genk(topic,limit=5):
94
  if len(items)>=limit:break
95
  except:pass
96
  return items
97
-
98
  def _s_thanhnien(topic,limit=6):
99
  items=[]
100
  try:
@@ -107,7 +101,6 @@ def _s_thanhnien(topic,limit=6):
107
  if len(items)>=limit:break
108
  except:pass
109
  return items
110
-
111
  def _s_tuoitre(topic,limit=6):
112
  items=[]
113
  try:
@@ -120,7 +113,6 @@ def _s_tuoitre(topic,limit=6):
120
  if len(items)>=limit:break
121
  except:pass
122
  return items
123
-
124
  def _s_thethaovanhoa(topic,limit=5):
125
  items=[]
126
  try:
@@ -135,23 +127,12 @@ def _s_thethaovanhoa(topic,limit=5):
135
  return items
136
 
137
  def _search_all(topic, limit=36):
138
- """Search 8 sources in parallel, interleave results."""
139
  results={}
140
  with ThreadPoolExecutor(8) as ex:
141
- futs={
142
- ex.submit(_s_vnexpress,topic,8):'vne',
143
- ex.submit(_s_dantri,topic,8):'dt',
144
- ex.submit(_s_vietnamnet,topic,6):'vnn',
145
- ex.submit(_s_bongda,topic,5):'bd',
146
- ex.submit(_s_genk,topic,5):'gk',
147
- ex.submit(_s_thanhnien,topic,6):'tn',
148
- ex.submit(_s_tuoitre,topic,6):'tt',
149
- ex.submit(_s_thethaovanhoa,topic,5):'tvh',
150
- }
151
  for f in as_completed(futs,timeout=14):
152
  try:results[futs[f]]=f.result()
153
  except:results[futs[f]]=[]
154
- # Interleave: take 1 from each source in round-robin
155
  srcs=list(results.values());interleaved=[];seen=set()
156
  mx=max((len(s) for s in srcs),default=0)
157
  for i in range(mx):
@@ -161,40 +142,32 @@ def _search_all(topic, limit=36):
161
  if url and url not in seen:seen.add(url);interleaved.append(s[i])
162
  return interleaved[:limit]
163
 
164
- # === ARTICLE SCRAPER ===
165
  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()))]
166
  def _scrape_generic(url):
167
  try:
168
  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)
169
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
170
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
171
- h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
172
- title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
173
  ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
174
  ogi=soup.find('meta',property='og:image');og_img=ogi.get('content','') if ogi else ''
175
  if og_img and og_img.startswith('//'):og_img='https:'+og_img
176
  if og_img and 'cdnphoto.dantri' in og_img:og_img='/api/proxy/img?url='+quote(og_img,safe='')
177
- selectors=['article','.singular-content','.detail-content','.fck_detail','.content-detail','.knc-content','main','.cms-body','.article__body','.detail__content']
178
  block=None
179
- for sel in selectors:
180
  el=soup.select_one(sel)
181
  if el and len(el.find_all('p'))>=2:block=el;break
182
  if not block:block=soup.body or soup
183
  body=[]
184
  for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
185
- if el.name=='p':
186
- t=el.get_text(strip=True)
187
- if t and len(t)>30:body.append({'type':'p','text':t})
188
- elif el.name in('h2','h3'):
189
- t=el.get_text(strip=True)
190
- if t:body.append({'type':'heading','text':t})
191
  elif el.name in('figure','img'):
192
  im=el if el.name=='img' else el.find('img')
193
  if im:
194
  src=im.get('data-src') or im.get('src') or im.get('data-original') or''
195
- if src and'base64' not in src:
196
- if src.startswith('//'):src='https:'+src
197
- body.append({'type':'img','src':src})
198
  if not body and summary:body=[{'type':'p','text':summary}]
199
  return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
200
  except:return None
@@ -212,44 +185,53 @@ def api_article_v2(url:str=Query(...)):
212
  data=_scrape_generic(url)
213
  return JSONResponse(data if data else{'error':'Không đọc được','url':url})
214
 
215
- # === HOT TOPICS (from 8 diverse RSS feeds) ===
216
  _hot_cache={'t':0,'d':[]}
217
  def _get_hot_topics():
218
  now=time.time()
219
  if _hot_cache['d'] and now-_hot_cache['t']<600:return _hot_cache['d']
220
- topics=[];seen=set()
221
- feeds=[
222
- 'https://vnexpress.net/rss/tin-moi-nhat.rss',
223
- 'https://dantri.com.vn/rss/home.rss',
224
- 'https://vietnamnet.vn/rss/tin-moi-nhat.rss',
225
- 'https://thanhnien.vn/rss/home.rss',
226
- 'https://tuoitre.vn/rss/tin-moi-nhat.rss',
227
- 'https://genk.vn/rss',
228
- 'https://vnexpress.net/rss/the-thao.rss',
229
- 'https://thethaovanhoa.vn/rss/tin-nong.rss',
230
- ]
231
  for feed_url in feeds:
232
  try:
233
  r=req.get(feed_url,headers={'User-Agent':'Mozilla/5.0'},timeout=6);r.encoding='utf-8';soup=BeautifulSoup(r.text,'xml')
234
- for item in soup.find_all('item')[:8]:
235
  title=_clean(item.find('title').get_text() if item.find('title') else '')
236
  if not title:continue
237
  title=re.sub(r'\s*[-|].*$','',title)
238
  words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title) if len(w)>2 and w.lower() not in _STOP]
239
  if len(words)<2:continue
240
- phrase=' '.join(words[:4])
241
- if len(phrase)<8 or len(phrase)>45:continue
242
- key=phrase.lower()
243
- if key in seen:continue
244
- 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)
245
- if is_dup:continue
246
- seen.add(key);topics.append({'label':'#'+re.sub(r'\s+','',phrase.title()),'topic':phrase})
247
- if len(topics)>=20:break
248
  except:continue
 
 
 
 
 
 
 
 
 
 
 
 
249
  if len(topics)>=20:break
250
- 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','Xe điện']:
 
 
251
  if len(topics)>=24:break
252
- if not any(kw.lower() in s for s in seen):topics.append({'label':'#'+re.sub(r'\s+','',kw.title()),'topic':kw})
 
253
  _hot_cache.update({'t':now,'d':topics[:24]});return topics[:24]
254
 
255
  @app.get('/api/hot_topics')
@@ -262,7 +244,7 @@ async def serve_index():
262
  @app.get('/api/hashtag/sources')
263
  def _ht(topic:str=Query(...),page:int=Query(default=0)):
264
  items=_search_all(topic,36)
265
- per_page=6;start=page*per_page;end=start+per_page
266
  return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
267
  @app.get('/api/categories')
268
  def _cat():return JSONResponse([])
 
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
 
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:
 
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:
 
53
  if len(items)>=limit:break
54
  except:pass
55
  return items
 
56
  def _s_vietnamnet(topic,limit=6):
57
  items=[]
58
  try:
 
65
  if len(items)>=limit:break
66
  except:pass
67
  return items
 
68
  def _s_bongda(topic,limit=5):
69
  items=[]
70
  try:
 
77
  if len(items)>=limit:break
78
  except:pass
79
  return items
 
80
  def _s_genk(topic,limit=5):
81
  items=[]
82
  try:
 
89
  if len(items)>=limit:break
90
  except:pass
91
  return items
 
92
  def _s_thanhnien(topic,limit=6):
93
  items=[]
94
  try:
 
101
  if len(items)>=limit:break
102
  except:pass
103
  return items
 
104
  def _s_tuoitre(topic,limit=6):
105
  items=[]
106
  try:
 
113
  if len(items)>=limit:break
114
  except:pass
115
  return items
 
116
  def _s_thethaovanhoa(topic,limit=5):
117
  items=[]
118
  try:
 
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());interleaved=[];seen=set()
137
  mx=max((len(s) for s in srcs),default=0)
138
  for i in range(mx):
 
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)
160
  if el and len(el.find_all('p'))>=2:block=el;break
161
  if not block:block=soup.body or soup
162
  body=[]
163
  for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
164
+ if el.name=='p':t=el.get_text(strip=True);(body.append({'type':'p','text':t}) if t and len(t)>30 else None)
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
 
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 len(phrase)<8 or len(phrase)>45:continue
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')
 
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([])