Extractors
This commit is contained in:
		
							
								
								
									
										78
									
								
								src/extractors/BaseNewspaperExtractor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/extractors/BaseNewspaperExtractor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import { WebScraper } from '../utils/WebScraper';
 | 
				
			||||||
 | 
					import { IFeed, NewsSource } from '../types/Feed';
 | 
				
			||||||
 | 
					import { NewspaperConfig } from '../types/NewspaperTypes';
 | 
				
			||||||
 | 
					import { Logger } from '../utils/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Clase abstracta base para extractores de periódicos
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export abstract class BaseNewspaperExtractor {
 | 
				
			||||||
 | 
					  protected webScraper: WebScraper;
 | 
				
			||||||
 | 
					  protected config: NewspaperConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(config: NewspaperConfig) {
 | 
				
			||||||
 | 
					    this.webScraper = new WebScraper();
 | 
				
			||||||
 | 
					    this.config = config;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Método abstracto que debe implementar cada extractor específico
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  abstract extractFrontPageUrls(): Promise<string[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Extrae noticias de las URLs de portada
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  async extractNews(): Promise<Omit<IFeed, '_id' | 'createdAt' | 'updatedAt'>[]> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      Logger.info(`Extracting front page URLs for ${this.config.name}`);
 | 
				
			||||||
 | 
					      const urls = await this.extractFrontPageUrls();
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (urls.length === 0) {
 | 
				
			||||||
 | 
					        Logger.warn(`No URLs found for ${this.config.name}`);
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      Logger.info(`Found ${urls.length} articles for ${this.config.name}`);
 | 
				
			||||||
 | 
					      const newsItems: Omit<IFeed, '_id' | 'createdAt' | 'updatedAt'>[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const url of urls) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          const scrapedData = await this.webScraper.scrapeUrl(url);
 | 
				
			||||||
 | 
					          if (scrapedData) {
 | 
				
			||||||
 | 
					            const feedItem = this.webScraper.convertToFeedData(scrapedData, this.config.source);
 | 
				
			||||||
 | 
					            newsItems.push(feedItem);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          Logger.error(`Error scraping article ${url}:`, error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return newsItems;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`Error extracting news for ${this.config.name}:`, error);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Verifica si el extractor está habilitado
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  isEnabled(): boolean {
 | 
				
			||||||
 | 
					    return this.config.enabled;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Obtiene el nombre del periódico
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getName(): string {
 | 
				
			||||||
 | 
					    return this.config.name;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Obtiene la fuente del periódico
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  getSource(): NewsSource {
 | 
				
			||||||
 | 
					    return this.config.source;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										78
									
								
								src/extractors/ElMundoExtractor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/extractors/ElMundoExtractor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import { BaseNewspaperExtractor } from './BaseNewspaperExtractor';
 | 
				
			||||||
 | 
					import { NewsSource } from '../types/Feed';
 | 
				
			||||||
 | 
					import { Logger } from '../utils/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extractor específico para El Mundo
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class ElMundoExtractor extends BaseNewspaperExtractor {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super({
 | 
				
			||||||
 | 
					      name: 'El Mundo',
 | 
				
			||||||
 | 
					      source: NewsSource.EL_MUNDO,
 | 
				
			||||||
 | 
					      baseUrl: 'https://elmundo.es',
 | 
				
			||||||
 | 
					      frontPageUrl: 'https://elmundo.es',
 | 
				
			||||||
 | 
					      selectors: {
 | 
				
			||||||
 | 
					        articleLinks: '.ue-c-cover-content__link, .ue-c-cover-content__headline-link, h2 a, h3 a',
 | 
				
			||||||
 | 
					        titleSelector: 'h1, .ue-c-article__headline',
 | 
				
			||||||
 | 
					        descriptionSelector: '.ue-c-article__standfirst, .ue-c-cover-content__standfirst',
 | 
				
			||||||
 | 
					        dateSelector: '.ue-c-article__publishdate, time',
 | 
				
			||||||
 | 
					        imageSelector: '.ue-c-article__image img'
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      enabled: true
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async extractFrontPageUrls(): Promise<string[]> {
 | 
				
			||||||
 | 
					    // Obtener HTML directamente usando fetch
 | 
				
			||||||
 | 
					    const response = await fetch(this.config.frontPageUrl, {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        'User-Agent': 'Mozilla/5.0 (compatible; DailyTrends/1.0)',
 | 
				
			||||||
 | 
					        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response.ok) {
 | 
				
			||||||
 | 
					      Logger.error(`Failed to fetch ${this.config.frontPageUrl}: ${response.status}`);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = await response.text();
 | 
				
			||||||
 | 
					    if (!html) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Extraer enlaces de artículos usando regex
 | 
				
			||||||
 | 
					      const linkRegex = /<a[^>]+href=["']([^"']*(?:elmundo\.es)?[^"']*)["'][^>]*>.*?<\/a>/gi;
 | 
				
			||||||
 | 
					      const urls: string[] = [];
 | 
				
			||||||
 | 
					      let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      while ((match = linkRegex.exec(html)) !== null) {
 | 
				
			||||||
 | 
					        let url = match[1];
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Filtrar solo URLs de artículos relevantes
 | 
				
			||||||
 | 
					        if (url.includes('/espana/') || 
 | 
				
			||||||
 | 
					            url.includes('/internacional/') || 
 | 
				
			||||||
 | 
					            url.includes('/economia/') ||
 | 
				
			||||||
 | 
					            url.includes('/sociedad/') ||
 | 
				
			||||||
 | 
					            url.includes('/politica/')) {
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Convertir URLs relativas a absolutas
 | 
				
			||||||
 | 
					          if (url.startsWith('/')) {
 | 
				
			||||||
 | 
					            url = this.config.baseUrl + url;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          if (!urls.includes(url) && urls.length < 20) {
 | 
				
			||||||
 | 
					            urls.push(url);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return urls;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`Error extracting El Mundo URLs:`, error);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										78
									
								
								src/extractors/ElPaisExtractor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/extractors/ElPaisExtractor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
				
			|||||||
 | 
					import { BaseNewspaperExtractor } from './BaseNewspaperExtractor';
 | 
				
			||||||
 | 
					import { NewsSource } from '../types/Feed';
 | 
				
			||||||
 | 
					import { Logger } from '../utils/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extractor específico para El País
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class ElPaisExtractor extends BaseNewspaperExtractor {
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super({
 | 
				
			||||||
 | 
					      name: 'El País',
 | 
				
			||||||
 | 
					      source: NewsSource.EL_PAIS,
 | 
				
			||||||
 | 
					      baseUrl: 'https://elpais.com',
 | 
				
			||||||
 | 
					      frontPageUrl: 'https://elpais.com',
 | 
				
			||||||
 | 
					      selectors: {
 | 
				
			||||||
 | 
					        articleLinks: 'article h2 a, .c_t a, .articulo-titulo a, h2.articulo-titulo a',
 | 
				
			||||||
 | 
					        titleSelector: 'h1, .articulo-titulo',
 | 
				
			||||||
 | 
					        descriptionSelector: '.articulo-entradilla, .entradilla, .subtitulo',
 | 
				
			||||||
 | 
					        dateSelector: '.articulo-fecha, time',
 | 
				
			||||||
 | 
					        imageSelector: '.articulo-foto img, .foto img'
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      enabled: true
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async extractFrontPageUrls(): Promise<string[]> {
 | 
				
			||||||
 | 
					    // Obtener HTML directamente usando fetch
 | 
				
			||||||
 | 
					    const response = await fetch(this.config.frontPageUrl, {
 | 
				
			||||||
 | 
					      headers: {
 | 
				
			||||||
 | 
					        'User-Agent': 'Mozilla/5.0 (compatible; DailyTrends/1.0)',
 | 
				
			||||||
 | 
					        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!response.ok) {
 | 
				
			||||||
 | 
					      Logger.error(`Failed to fetch ${this.config.frontPageUrl}: ${response.status}`);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const html = await response.text();
 | 
				
			||||||
 | 
					    if (!html) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      // Extraer enlaces de artículos usando regex
 | 
				
			||||||
 | 
					      const linkRegex = /<a[^>]+href=["']([^"']*(?:elpais\.com)?[^"']*)["'][^>]*>.*?<\/a>/gi;
 | 
				
			||||||
 | 
					      const urls: string[] = [];
 | 
				
			||||||
 | 
					      let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      while ((match = linkRegex.exec(html)) !== null) {
 | 
				
			||||||
 | 
					        let url = match[1];
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Filtrar solo URLs de artículos relevantes
 | 
				
			||||||
 | 
					        if (url.includes('/politica/') || 
 | 
				
			||||||
 | 
					            url.includes('/economia/') || 
 | 
				
			||||||
 | 
					            url.includes('/sociedad/') ||
 | 
				
			||||||
 | 
					            url.includes('/internacional/') ||
 | 
				
			||||||
 | 
					            url.includes('/espana/')) {
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          // Convertir URLs relativas a absolutas
 | 
				
			||||||
 | 
					          if (url.startsWith('/')) {
 | 
				
			||||||
 | 
					            url = this.config.baseUrl + url;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          if (!urls.includes(url) && urls.length < 20) {
 | 
				
			||||||
 | 
					            urls.push(url);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return urls;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      Logger.error(`Error extracting El País URLs:`, error);
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/extractors/NewspaperExtractorFactory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/extractors/NewspaperExtractorFactory.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					import { BaseNewspaperExtractor } from './BaseNewspaperExtractor';
 | 
				
			||||||
 | 
					import { ElPaisExtractor } from './ElPaisExtractor';
 | 
				
			||||||
 | 
					import { ElMundoExtractor } from './ElMundoExtractor';
 | 
				
			||||||
 | 
					import { NewsSource } from '../types/Feed';
 | 
				
			||||||
 | 
					import { Logger } from '../utils/logger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Factory para crear extractores de periódicos
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export class NewspaperExtractorFactory {
 | 
				
			||||||
 | 
					  static createExtractor(source: NewsSource): BaseNewspaperExtractor | null {
 | 
				
			||||||
 | 
					    switch (source) {
 | 
				
			||||||
 | 
					      case NewsSource.EL_PAIS:
 | 
				
			||||||
 | 
					        return new ElPaisExtractor();
 | 
				
			||||||
 | 
					      case NewsSource.EL_MUNDO:
 | 
				
			||||||
 | 
					        return new ElMundoExtractor();
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        Logger.warn(`No extractor available for source: ${source}`);
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static getAllAvailableExtractors(): BaseNewspaperExtractor[] {
 | 
				
			||||||
 | 
					    const extractors: BaseNewspaperExtractor[] = [];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    for (const source of Object.values(NewsSource)) {
 | 
				
			||||||
 | 
					      if (source !== NewsSource.MANUAL) {
 | 
				
			||||||
 | 
					        const extractor = this.createExtractor(source);
 | 
				
			||||||
 | 
					        if (extractor) {
 | 
				
			||||||
 | 
					          extractors.push(extractor);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return extractors;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user