bep40 commited on
Commit
92b2e1c
·
verified ·
1 Parent(s): 13ecfa2

fix: rate limiter + session pooling + cache for all scrape functions

Browse files
Files changed (1) hide show
  1. main.py +45 -17
main.py CHANGED
@@ -25,6 +25,20 @@ _cache_ttl = 300
25
  _cache_ttl_live = 60
26
  _cache_ttl_yt = 1800
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # ===== VTV NAM BO SHORTS FALLBACK =====
29
  SHORTS_FALLBACK = [
30
  {"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
@@ -97,20 +111,21 @@ class _RateLimiter:
97
  time.sleep(wait_time)
98
  self._last[domain] = time.time()
99
 
100
- _rl_bongda = _RateLimiter(min_interval=2.0)
101
- _rl_news = _RateLimiter(min_interval=0.5)
102
 
103
  def _rl_get(url, headers=None, timeout=10, domain=None):
104
  from urllib.parse import urlparse
105
  if domain is None:
106
  domain = urlparse(url).netloc
107
  rl = _rl_bongda if "bongda.com.vn" in domain else _rl_news
 
108
  for attempt in range(3):
109
  rl.wait(domain)
110
  try:
111
- r = requests.get(url, headers=headers, timeout=timeout)
112
  if r.status_code == 429:
113
- retry_after = int(r.headers.get("Retry-After", 5))
114
  print(f"[RateLimit] 429 from {domain}, waiting {retry_after}s")
115
  time.sleep(retry_after)
116
  continue
@@ -146,7 +161,7 @@ def _cached(key, fn, ttl=None):
146
  except: data=_cache.get(key,{}).get("d",[])
147
  _cache[key]={"d":data,"t":now}; return data
148
  def _get(url, headers=None):
149
- h=headers or HEADERS; r=requests.get(url, headers=h, timeout=15); r.encoding="utf-8"
150
  return BeautifulSoup(r.text,"lxml")
151
  def fetch_bongda_api(endpoint):
152
  try:
@@ -517,8 +532,8 @@ def scrape_vne(cat_url):
517
 
518
  def scrape_genk_ai():
519
  try:
520
- r=requests.get("https://genk.vn/ai.chn",headers=HEADERS,timeout=15)
521
- if r.status_code!=200: return []
522
  r.encoding="utf-8"; soup=BeautifulSoup(r.text,"lxml"); articles=[]; seen=set()
523
  for a in soup.find_all("a",href=True):
524
  href=a.get("href","")
@@ -538,9 +553,11 @@ def scrape_genk_ai():
538
  seen.add(href)
539
  if not img_src:
540
  try:
541
- og_r=requests.get(href,headers=HEADERS,timeout=8); og_r.encoding="utf-8"
542
- og_soup=BeautifulSoup(og_r.text,"lxml"); og_tag=og_soup.find("meta",property="og:image")
543
- if og_tag: img_src=og_tag.get("content","")
 
 
544
  except: pass
545
  articles.append({"title":title,"link":href,"img":img_src,"source":"genk"})
546
  if len(articles)>=30: break
@@ -551,14 +568,17 @@ def scrape_genk_ai():
551
  def api_homepage():
552
  def _f():
553
  articles=[]
554
- with ThreadPoolExecutor(12) as ex:
 
555
  futs={ex.submit(scrape_vne,VNE_CATS[k][0]):VNE_CATS[k][1] for k in["thoi-su","the-gioi","kinh-doanh","the-thao","giai-tri","phap-luat","giao-duc","du-lich","doi-song"]}
556
  for f in as_completed(futs):
557
  try:
558
  for a in f.result(): a["group"]=futs[f]; articles.append(a)
559
  except: pass
560
  return articles
561
- return JSONResponse(_cached("homepage",_f))
 
 
562
 
563
  @app.get("/api/category/{cat_id}")
564
  def api_category(cat_id:str):
@@ -569,7 +589,9 @@ def api_category(cat_id:str):
569
  [a.update({"group":VNE_CATS[cat_id][1]}) for a in arts]
570
  return arts
571
  return []
572
- return JSONResponse(_cached(f"cat_{cat_id}",_f))
 
 
573
 
574
  @app.get("/api/categories")
575
  def api_categories():
@@ -581,8 +603,11 @@ def api_categories():
581
  # ===== Article scraping functions for /api/article =====
582
  def _scrape_generic(url):
583
  """Generic article scraper fallback."""
 
584
  try:
585
- r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True)
 
 
586
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
587
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
588
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
@@ -622,7 +647,8 @@ def scrape_vne_article(url):
622
  return data
623
  # If generic didn't work well, try specific VnExpress selectors
624
  try:
625
- r=requests.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)
 
626
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
627
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.banner-ads','iframe','.fb-comments','.fb-root']):tag.decompose()
628
  article=soup.select_one('.fck_detail,.sidebar-1')
@@ -668,7 +694,8 @@ def scrape_dantri_article(url):
668
  """Scrape Dân Trí article."""
669
  if 'dantri.com.vn' not in url:return _scrape_generic(url)
670
  try:
671
- r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15)
 
672
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
673
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.banner-ads','iframe']):tag.decompose()
674
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
@@ -712,7 +739,8 @@ def scrape_genk_article(url):
712
  """Scrape GenK article."""
713
  if 'genk.vn' not in url:return _scrape_generic(url)
714
  try:
715
- r=requests.get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15)
 
716
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
717
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.ads','iframe']):tag.decompose()
718
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
 
25
  _cache_ttl_live = 60
26
  _cache_ttl_yt = 1800
27
 
28
+ # ===== Connection Pool Session (reuse TCP connections) =====
29
+ _session_pool = threading.local()
30
+ def _get_session():
31
+ """Get a thread-local requests.Session with connection pooling."""
32
+ s = getattr(_session_pool, "sess", None)
33
+ if s is None:
34
+ s = requests.Session()
35
+ s.headers.update(HEADERS)
36
+ adapter = requests.adapters.HTTPAdapter(pool_connections=6, pool_maxsize=6, max_retries=1)
37
+ s.mount("http://", adapter)
38
+ s.mount("https://", adapter)
39
+ _session_pool.sess = s
40
+ return s
41
+
42
  # ===== VTV NAM BO SHORTS FALLBACK =====
43
  SHORTS_FALLBACK = [
44
  {"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
 
111
  time.sleep(wait_time)
112
  self._last[domain] = time.time()
113
 
114
+ _rl_bongda = _RateLimiter(min_interval=3.0)
115
+ _rl_news = _RateLimiter(min_interval=2.0)
116
 
117
  def _rl_get(url, headers=None, timeout=10, domain=None):
118
  from urllib.parse import urlparse
119
  if domain is None:
120
  domain = urlparse(url).netloc
121
  rl = _rl_bongda if "bongda.com.vn" in domain else _rl_news
122
+ sess = _get_session()
123
  for attempt in range(3):
124
  rl.wait(domain)
125
  try:
126
+ r = sess.get(url, headers=headers, timeout=timeout)
127
  if r.status_code == 429:
128
+ retry_after = int(r.headers.get("Retry-After", 10))
129
  print(f"[RateLimit] 429 from {domain}, waiting {retry_after}s")
130
  time.sleep(retry_after)
131
  continue
 
161
  except: data=_cache.get(key,{}).get("d",[])
162
  _cache[key]={"d":data,"t":now}; return data
163
  def _get(url, headers=None):
164
+ h=headers or HEADERS; r=_get_session().get(url, headers=h, timeout=15); r.encoding="utf-8"
165
  return BeautifulSoup(r.text,"lxml")
166
  def fetch_bongda_api(endpoint):
167
  try:
 
532
 
533
  def scrape_genk_ai():
534
  try:
535
+ r=_rl_get("https://genk.vn/ai.chn", headers=HEADERS, timeout=15, domain="genk.vn")
536
+ if not r or r.status_code!=200: return []
537
  r.encoding="utf-8"; soup=BeautifulSoup(r.text,"lxml"); articles=[]; seen=set()
538
  for a in soup.find_all("a",href=True):
539
  href=a.get("href","")
 
553
  seen.add(href)
554
  if not img_src:
555
  try:
556
+ og_r=_rl_get(href, headers=HEADERS, timeout=8, domain="genk.vn")
557
+ if og_r and og_r.status_code==200:
558
+ og_r.encoding="utf-8"
559
+ og_soup=BeautifulSoup(og_r.text,"lxml"); og_tag=og_soup.find("meta",property="og:image")
560
+ if og_tag: img_src=og_tag.get("content","")
561
  except: pass
562
  articles.append({"title":title,"link":href,"img":img_src,"source":"genk"})
563
  if len(articles)>=30: break
 
568
  def api_homepage():
569
  def _f():
570
  articles=[]
571
+ # Reduced concurrency to avoid HF outbound rate limits
572
+ with ThreadPoolExecutor(4) as ex:
573
  futs={ex.submit(scrape_vne,VNE_CATS[k][0]):VNE_CATS[k][1] for k in["thoi-su","the-gioi","kinh-doanh","the-thao","giai-tri","phap-luat","giao-duc","du-lich","doi-song"]}
574
  for f in as_completed(futs):
575
  try:
576
  for a in f.result(): a["group"]=futs[f]; articles.append(a)
577
  except: pass
578
  return articles
579
+ resp = JSONResponse(_cached("homepage",_f))
580
+ resp.headers["Cache-Control"] = "public, max-age=120"
581
+ return resp
582
 
583
  @app.get("/api/category/{cat_id}")
584
  def api_category(cat_id:str):
 
589
  [a.update({"group":VNE_CATS[cat_id][1]}) for a in arts]
590
  return arts
591
  return []
592
+ resp = JSONResponse(_cached(f"cat_{cat_id}",_f))
593
+ resp.headers["Cache-Control"] = "public, max-age=120"
594
+ return resp
595
 
596
  @app.get("/api/categories")
597
  def api_categories():
 
603
  # ===== Article scraping functions for /api/article =====
604
  def _scrape_generic(url):
605
  """Generic article scraper fallback."""
606
+ from urllib.parse import urlparse
607
  try:
608
+ domain=urlparse(url).netloc
609
+ r=_rl_get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,domain=domain)
610
+ if not r: return None
611
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
612
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
613
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
 
647
  return data
648
  # If generic didn't work well, try specific VnExpress selectors
649
  try:
650
+ r=_rl_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,domain="vnexpress.net")
651
+ if not r: return data
652
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
653
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.banner-ads','iframe','.fb-comments','.fb-root']):tag.decompose()
654
  article=soup.select_one('.fck_detail,.sidebar-1')
 
694
  """Scrape Dân Trí article."""
695
  if 'dantri.com.vn' not in url:return _scrape_generic(url)
696
  try:
697
+ r=_rl_get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,domain="dantri.com.vn")
698
+ if not r: return _scrape_generic(url)
699
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
700
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.banner-ads','iframe']):tag.decompose()
701
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
 
739
  """Scrape GenK article."""
740
  if 'genk.vn' not in url:return _scrape_generic(url)
741
  try:
742
+ r=_rl_get(url,headers={'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,domain="genk.vn")
743
+ if not r: return _scrape_generic(url)
744
  r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
745
  for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','.ads','iframe']):tag.decompose()
746
  h1=soup.find('h1');ogt=soup.find('meta',property='og:title')