@@ -68,9 +68,13 @@ def is_recent(date_str):
6868
6969 try :
7070 clean = date_str .split ('T' )[0 ].strip ()
71- for fmt in ('%Y-%m-%d' , '%d/%m/%Y' , '%Y/%m/%d' ):
71+ # Intentar formatos comunes + formato RSS (RFC 822)
72+ for fmt in ('%Y-%m-%d' , '%d/%m/%Y' , '%Y/%m/%d' , '%a, %d %b %Y %H:%M:%S %z' ):
7273 try :
73- dt = datetime .strptime (clean , fmt )
74+ dt = datetime .strptime (clean if fmt != '%a, %d %b %Y %H:%M:%S %z' else date_str , fmt )
75+ # Normalizar a offset-naive para la comparación si es necesario
76+ if dt .tzinfo :
77+ dt = dt .replace (tzinfo = None )
7478 return datetime .now () - dt < timedelta (days = 2 )
7579 except :
7680 continue
@@ -247,6 +251,39 @@ def send_to_telegram(message):
247251 except Exception as e :
248252 logger .error (f"Error Telegram: { e } " )
249253
254+ # ── RSS Scraper ───────────────────────────────────────────────────────────────
255+
256+ def scrape_rss_feed (url , source_name ):
257+ try :
258+ r = requests .get (url , headers = HEADERS , timeout = 15 )
259+ r .raise_for_status ()
260+ soup = BeautifulSoup (r .text , 'xml' )
261+
262+ items = []
263+ for entry in soup .find_all ('item' , limit = 10 ):
264+ title = entry .title .text .strip () if entry .title else ""
265+ link = entry .link .text .strip () if entry .link else ""
266+ pub_date = entry .pubDate .text .strip () if entry .pubDate else ""
267+
268+ if not title or not link :
269+ continue
270+
271+ if pub_date and not is_recent (pub_date ):
272+ continue
273+
274+ items .append ({
275+ 'title' : title ,
276+ 'link' : link ,
277+ 'source' : source_name
278+ })
279+ # Solo retornamos la primera noticia válida para mantener consistencia con el flujo actual
280+ if items :
281+ return items
282+
283+ except Exception as e :
284+ logger .error (f"RSS Error ({ source_name } ): { e } " )
285+ return []
286+
250287# ── Scrapers ──────────────────────────────────────────────────────────────────
251288
252289BLOCKED_URLS = {
@@ -261,107 +298,19 @@ def send_to_telegram(message):
261298}
262299
263300def scrape_cybersecurity_news ():
264- try :
265- r = requests .get (
266- "https://cybersecuritynews.es/category/actualidad/inteligencia-artificial/" ,
267- headers = HEADERS , timeout = 15
268- )
269- soup = BeautifulSoup (r .text , 'html.parser' )
270-
271- # Palabras clave para saltar noticias "sticky" o promocionales viejas
272- bad_keywords = ["Insurance Day" , "Puertas Abiertas" , "CyberCoffee" , "CyberWebinar" ]
273-
274- for article in soup .find_all ('article' , limit = 15 ):
275- title_tag = article .find (['h1' , 'h2' , 'h3' ])
276- link_tag = title_tag .find ('a' ) if title_tag else article .find ('a' , href = True )
277- if not link_tag :
278- continue
279- href , title = link_tag ['href' ], link_tag .text .strip ()
280- title = title .replace ("AntAnterior" , "" ).replace ("Siguiente" , "" ).strip ()
281-
282- # Filtros
283- if href in BLOCKED_URLS :
284- continue
285- if len (title ) <= 25 :
286- continue
287- if any (k .lower () in title .lower () for k in bad_keywords ):
288- continue
289-
290- return [{'title' : title , 'link' : href , 'source' : 'CyberSecurity News' }]
291- except Exception as e :
292- logger .error (f"CSN error: { e } " )
293- return []
301+ # RSS de CSN (generalmente incluye IA si es el feed principal o de categoría)
302+ return scrape_rss_feed ("https://cybersecuritynews.es/feed/" , "CyberSecurity News" )
294303
295304def scrape_welivesecurity ():
296- try :
297- r = requests .get ("https://www.welivesecurity.com/la-es/" , headers = HEADERS , timeout = 15 )
298- soup = BeautifulSoup (r .text , 'html.parser' )
299- for article in soup .find_all ('div' , class_ = 'article-list-card' , limit = 5 ):
300- # Nuevo selector de fecha para WLS
301- info_tag = article .find ('div' , class_ = 'article-title-info' )
302- date_text = ""
303- if info_tag :
304- spans = info_tag .find_all ('span' )
305- if spans :
306- date_text = spans [- 1 ].text .strip ()
307-
308- if not date_text :
309- time_tag = article .find ('time' )
310- date_text = time_tag .text .strip () if time_tag else ""
311-
312- if date_text and not is_recent (date_text ):
313- continue
314-
315- link_tag = article .find ('a' , href = True )
316- title_tag = article .find ('p' , class_ = 'title' ) or article .find (['h2' , 'h3' ])
317- title = title_tag .text .strip () if title_tag else ""
318- if title and link_tag :
319- href = link_tag ['href' ]
320- return [{'title' : title ,
321- 'link' : href if href .startswith ('http' ) else f"https://www.welivesecurity.com{ href } " ,
322- 'source' : 'WeLiveSecurity' }]
323- except Exception as e :
324- logger .error (f"WLS error: { e } " )
325- return []
305+ return scrape_rss_feed ("https://www.welivesecurity.com/la-es/feed/" , "WeLiveSecurity" )
326306
327307def scrape_xataka ():
328- try :
329- r = requests .get ("https://www.xataka.com/categoria/robotica-e-ia" , headers = HEADERS , timeout = 15 )
330- soup = BeautifulSoup (r .text , 'html.parser' )
331- for article in soup .find_all ('article' , class_ = 'abstract-article' , limit = 5 ):
332- time_tag = article .find ('time' )
333- date_val = time_tag ['datetime' ] if time_tag and time_tag .has_attr ('datetime' ) else None
334- if date_val and not is_recent (date_val ):
335- continue
336-
337- title_tag = article .find ('h2' )
338- link_tag = title_tag .find ('a' ) if title_tag else None
339- if title_tag and link_tag :
340- return [{'title' : title_tag .text .strip (), 'link' : link_tag ['href' ], 'source' : 'Xataka' }]
341- except Exception as e :
342- logger .error (f"Xataka error: { e } " )
343- return []
308+ # Feed verificado de Xataka
309+ return scrape_rss_feed ("https://www.xataka.com/index.xml" , "Xataka" )
344310
345311def scrape_wired_espanol ():
346- try :
347- r = requests .get ("https://es.wired.com/tag/inteligencia-artificial" , headers = HEADERS , timeout = 15 )
348- soup = BeautifulSoup (r .text , 'html.parser' )
349- for article in soup .find_all ('div' , class_ = lambda x : x and 'SummaryItemContent' in x , limit = 5 ):
350- time_tag = article .find ('time' )
351- date_val = time_tag .text .strip () if time_tag else None
352- if date_val and not is_recent (date_val ):
353- continue
354- link_tag = article .find ('a' )
355- if link_tag :
356- title = link_tag .text .strip ()
357- if len (title ) > 20 :
358- href = link_tag ['href' ]
359- return [{'title' : title ,
360- 'link' : href if href .startswith ('http' ) else f"https://es.wired.com{ href } " ,
361- 'source' : 'WIRED en Español' }]
362- except Exception as e :
363- logger .error (f"WIRED error: { e } " )
364- return []
312+ # Feed de Wired España (estándar, a veces requiere el trailing slash)
313+ return scrape_rss_feed ("https://es.wired.com/feed/rss" , "WIRED en Español" )
365314
366315# ── Main ──────────────────────────────────────────────────────────────────────
367316
0 commit comments