diff --git a/Makefile b/Makefile index a7f7ce2..9eaaeb4 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,9 @@ scrape_folha: scrape_jornaldaparaiba: docker compose exec scraper python scrape_no_openai.py --platform jornaldaparaiba.com.br +scrape_brasildefato: + docker compose exec scraper python scrape_no_openai.py --platform brasildefato.com.br + # Crawler para todos os portais ou específicos crawl: @@ -121,9 +124,13 @@ crawl_ig: crawl_folha: docker compose run scraper python crawl.py folhaspider +crawl_brasildefato: + docker compose run scraper python crawl.py brasildefato + crawl_jornaldaparaiba: docker compose run scraper python crawl.py jornaldaparaiba + # Workflow completo de coleta de URLs crawl_all_working: @echo "Executando crawl de todos os portais funcionais..." @@ -135,6 +142,7 @@ crawl_all_working: @make crawl_aliadosBrasil @make crawl_ig @make crawl_folha + @make crawl_brasildefato @make crawl_jornaldaparaiba @echo "Crawl de todos os portais concluído!" @@ -149,6 +157,7 @@ scrape_all_working: @make scrape_r7 @make scrape_uol @make scrape_folha + @make scrape_brasildefato @make scrape_jornaldaparaiba @echo "Scraping de todos os portais concluído!" @@ -197,6 +206,7 @@ help: @echo " make crawl_aliadosBrasil - Coleta URLs do portal AliadosBrasil" @echo " make crawl_ig - Coleta URLs do portal IG" @echo " make crawl_folha - Coleta URLs do portal Folha" + @echo " make crawl_brasildefato - Coleta URLs do portal Brasil de Fato" @echo "" @echo "=== COMANDOS DE SCRAPING (Extração de Anúncios) ===" @echo " make scrape_all_working - Executa scraping de todos os portais funcionais" @@ -208,6 +218,7 @@ help: @echo " make scrape_r7 - Scraping do portal R7" @echo " make scrape_uol - Scraping do portal UOL" @echo " make scrape_folha - Scraping do portal Folha" + @echo " make scrape_brasildefato - Scraping do portal Brasil de Fato" @echo "" @echo "=== WORKFLOWS COMPLETOS ===" @echo " make pipeline_complete - Executa crawl + scraping de todos os portais" diff --git a/README.md b/README.md index 1c50248..0b2e25e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Check-up + ![cover](https://raw.githubusercontent.com/aosfatos/check-up/refs/heads/develop/cover.png) O Check-up é um projeto de **extração de conteúdo de notícias** de portais brasileiros para análise de desinformação. Este repositório contém ferramentas para coletar e extrair o texto completo de notícias de diversos portais de notícia do Brasil. @@ -15,6 +16,7 @@ Este projeto foi adaptado a partir de uma ferramenta original do [**Aos Fatos**] ### 🏗️ Arquitetura A ferramenta possui dois módulos principais: + - **Crawler (Spiders)**: Coleta URLs de notícias das páginas iniciais dos portais - **Extrator (Plays)**: Acessa cada notícia e extrai o conteúdo completo @@ -25,16 +27,20 @@ O código pode ser facilmente adaptado para novos portais de notícia, seguindo ## 🚀 Como Rodar o Projeto ### 📋 Pré-requisitos + - Docker e Docker Compose instalados - Make (disponível por padrão no macOS e Linux) ### 🔧 Configuração Inicial **1. Setup completo do ambiente:** + ```bash make setup ``` + Este comando faz toda a configuração inicial: + - Cria o arquivo `.env` a partir do `env.example` - Inicia os serviços Docker (PostgreSQL, MinIO, scraper) - Aguarda o banco de dados ficar pronto @@ -48,30 +54,35 @@ O processo de extração acontece em duas etapas principais: **🕷️ Etapa 1: Crawling (Coleta de URLs de Notícias)** Colete URLs de notícias de todos os portais funcionais: + ```bash make crawl_all_working ``` Ou execute individualmente: + ```bash make crawl_metropoles # Portal Metrópoles -make crawl_veja # Portal Veja +make crawl_veja # Portal Veja make crawl_r7 # Portal R7 make crawl_uol # Portal UOL make crawl_maisgoias # Portal MaisGoiás make crawl_aliadosBrasil # Portal AliadosBrasil make crawl_ig # Portal IG make crawl_folha # Portal Folha +make crawl_brasildefato # Portal Brasil de Fato ``` **📰 Etapa 2: Scraping (Extração de Conteúdo das Notícias)** Execute a extração de conteúdo de todos os portais: + ```bash make scrape_all_working ``` Ou execute por portal específico: + ```bash make scrape_metropoles # Extração do Metrópoles make scrape_maisgoias # Extração do MaisGoiás @@ -81,11 +92,13 @@ make scrape_veja # Extração da Veja make scrape_r7 # Extração do R7 make scrape_uol # Extração do UOL make scrape_folha # Extração da Folha +make scrape_brasildefato # Extração do Brasil de Fato ``` ### 📋 Conteúdo Extraído Para cada notícia, o sistema extrai: + - **Título**: Título principal da notícia - **Descrição**: Subtítulo ou resumo da notícia - **Corpo**: Texto completo da matéria @@ -94,11 +107,13 @@ Para cada notícia, o sistema extrai: ### ⚡ Workflows Automatizados **Pipeline completo (crawl + scraping):** + ```bash make pipeline_complete ``` **Coleta otimizada:** + ```bash make collect_working ``` @@ -106,21 +121,25 @@ make collect_working ### 🛠️ Comandos Úteis **Acessar o container:** + ```bash make bash ``` **Ver logs dos serviços:** + ```bash docker compose logs -f ``` **Parar serviços:** + ```bash make stop ``` **Ver todos os comandos disponíveis:** + ```bash make help ``` @@ -128,13 +147,14 @@ make help ### � Onde os Dados São Salvos - **URLs coletadas**: Salvos no banco PostgreSQL (tabela `URLQueue`) -- **Notícias extraídas**: Salvos no banco PostgreSQL (tabela `Entry`) com título, descrição, corpo e tags +- **Notícias extraídas**: Salvos no banco PostgreSQL (tabela `Entry`) com título, descrição, corpo e tags - **Arquivos temporários**: Salvos no MinIO (acessível em `http://localhost:9001`) - **Logs**: Disponíveis via `docker compose logs` ### � Configurações Importantes **Variáveis de ambiente (arquivo `.env`):** + - Credenciais dos portais (se necessário) - Configurações do banco PostgreSQL - Configurações do MinIO para armazenamento @@ -147,16 +167,19 @@ make help Este projeto coleta dados dos seguintes portais brasileiros: ### ✅ **Portais Funcionais** (Totalmente operacionais) + - **[Metrópoles](https://www.metropoles.com)** - `make crawl_metropoles` / `make scrape_metropoles` -- **[IG](https://www.ig.com.br)** - `make crawl_ig` / `make scrape_ig` +- **[IG](https://www.ig.com.br)** - `make crawl_ig` / `make scrape_ig` - **[MaisGoiás](https://www.maisgoias.com.br)** - `make crawl_maisgoias` / `make scrape_maisgoias` - **[AliadosBrasil](https://www.aliadosbrasiloficial.com.br)** - `make crawl_aliadosBrasil` / `make scrape_aliadosbrasil` - **[Veja](https://veja.abril.com.br)** - `make crawl_veja` / `make scrape_veja` - **[R7](https://www.r7.com)** - `make crawl_r7` / `make scrape_r7` - **[UOL](https://www.uol.com.br)** - `make crawl_uol` / `make scrape_uol` - **[Folha](https://www.folha.uol.com.br)** - `make crawl_folha` / `make scrape_folha` +- **[Brasil de Fato](https://www.brasildefato.com.br)** - `make crawl_brasildefato` / `make scrape_brasildefato` ### 🔧 **Em Desenvolvimento** + - **[Estadão](https://www.estadao.com.br)** - Necessita ajustes nos seletores CSS - **[Globo](https://oglobo.globo.com/)** - Requer configurações específicas de autenticação - **[RBS](https://www.clicrbs.com.br)** - Spider em desenvolvimento @@ -175,19 +198,48 @@ As seguintes tabelas são criadas automaticamente durante o setup: ## 🧑‍💻 Desenvolvimento e Contribuição -Quer contribuir para o projeto? Consulte nosso **[Guia de Contribuição](./.github/CONTRIBUTING.md)** completo que inclui: +### 🐳 Build Docker e Troubleshooting + +- O `Dockerfile` usa multi-stage build com instalação automática dos browsers do Playwright +- Para build manual: `docker build -t check-up:latest .` +- Para acessar o container: `make bash` + +### 🧪 Estrutura de Spiders e Plays -- 🔄 Fluxo de contribuição -- 🛠️ Como criar novos scrapers ou adaptar existentes -- 🐛 Como reportar bugs e sugerir melhorias -- 📚 Guias de estilo (commits e PRs) -- 📋 Templates de issues e labels +- Cada portal tem um spider Scrapy (`spiders/`) e um script Playwright (`plays/`) +- Para adicionar novo portal, siga o modelo dos arquivos existentes +- O scraping aceita argumentos de timeout e plataforma via CLI + +### 📚 Guias de Contribuição + +**⚠️ IMPORTANTE**: Antes de contribuir, leia os tutoriais apropriados: + +- **[TUTORIAL_CRIACAO_DO_ZERO.md](./TUTORIAL_CRIACAO_DO_ZERO.md)** - Para criar scrapers completamente novos: + + - Guia passo-a-passo desde a análise do portal + - Exemplos práticos com código completo + - Troubleshooting e problemas comuns + - Testes e validação + +- **[TUTORIAL_SPIDERS_PLAYS.md](./TUTORIAL_SPIDERS_PLAYS.md)** - Para adaptar scrapers existentes: + - Guia para adaptar spiders e plays existentes + - Checklist de validação para garantir qualidade + - Exemplos práticos de boas práticas + - Dicas para identificar seletores atualizados + +### 📝 Fluxo de Contribuição + +- Siga as políticas de branches e commits do projeto +- Use Conventional Commits e Git Flow +- Sempre rode os testes antes de abrir PR +- Nunca faça push direto para `main` ou `develop` ## 🚨 Troubleshooting ### Problemas Comuns **1. Erro "Playwright browsers not installed"** + ```bash # Se o IG spider falhar, reconstrua a imagem Docker: docker compose down @@ -196,6 +248,7 @@ make start ``` **2. Container não conecta ao banco** + ```bash # Aguarde o banco ficar pronto: make wait-for-db @@ -204,6 +257,7 @@ make stop && make start ``` **3. MinIO não acessível** + ```bash # Verifique se o MinIO está rodando: docker compose ps @@ -211,6 +265,7 @@ docker compose ps ``` **4. Containers órfãos** + ```bash # Limpe containers órfãos: docker compose down --remove-orphans @@ -220,6 +275,7 @@ make prune ### 💡 Exemplos Práticos **Coleta rápida de um portal específico:** + ```bash # Setup inicial (só uma vez) make setup @@ -230,21 +286,24 @@ make scrape_metropoles ``` **Pipeline completo para produção:** + ```bash # Coleta de todos os portais funcionais make pipeline_complete ``` **Monitoramento em tempo real:** + ```bash # Terminal 1 - Execute a coleta make crawl_all_working -# Terminal 2 - Monitore os logs +# Terminal 2 - Monitore os logs docker compose logs -f scraper ``` **Acesso aos dados coletados:** + ```bash # Via container make bash @@ -259,6 +318,7 @@ open http://localhost:9001 ``` ### 2- Executar as migrações do banco de dados + Para executar as migrações do banco de dados, utilize: `make migrate_db` @@ -266,6 +326,7 @@ Para executar as migrações do banco de dados, utilize: Este comando irá aplicar todas as migrações pendentes no banco de dados. ### 3- Coletar URL de notícias + O primeiro passo é coletar URLs de notícias nas páginas iniciais dos portais. Cada portal possui um "spider" implementado com a biblioteca Scrapy, localizado no diretório `spiders/`. Exemplo de script para a [Folha]("https://www.folha.uol.com.br"): `spiders/folha.py`. @@ -285,6 +346,7 @@ Para executar a extração de todas as notícias: ### 5- Adicionar um Novo Portal Para adicionar um novo portal, consulte o **[TUTORIAL_SPIDERS_PLAYS.md](./TUTORIAL_SPIDERS_PLAYS.md)** que contém instruções detalhadas sobre: + - Como criar spiders para coleta de URLs - Como criar plays para extração de conteúdo - Exemplos práticos e boas práticas @@ -295,15 +357,15 @@ Para adicionar um novo portal, consulte o **[TUTORIAL_SPIDERS_PLAYS.md](./TUTORI Os dados extraídos são armazenados no banco PostgreSQL e arquivos temporários no MinIO. Para configurações específicas de armazenamento, consulte o arquivo `.env`. ## Importante -Os scripts dependem da estrutura HTML dos portais e podem precisar de ajustes após atualizações nos sites. +Os scripts dependem da estrutura HTML dos portais e podem precisar de ajustes após atualizações nos sites. ## Executando um spider específico Para executar apenas um spider específico, você pode passar o nome do spider como argumento: ``` - docker compose run scraper python crawl.py metropolesspider + docker compose run scraper python crawl.py metropolesspider ``` Para verificar o banco de dados, você pode executar o seguinte comando: @@ -335,4 +397,4 @@ make clean # Ver todos os comandos disponíveis make help -``` \ No newline at end of file +``` diff --git a/create_db.py b/create_db.py index 7ce5701..2d571c8 100644 --- a/create_db.py +++ b/create_db.py @@ -43,6 +43,7 @@ ("Mais Goiás", "https://www.maisgoias.com.br/", "maisgoias"), ("Aliados Brasil", "https://www.aliadosbrasiloficial.com.br/", "aliadosbrasil"), ("Jornal da Paraíba", "https://jornaldaparaiba.com.br/", "jornaldaparaiba"), + ("Brasil de Fato", "https://www.brasildefato.com.br/", "brasildefato"), ] portals_to_add = [] for portal in portals: diff --git a/plays/__init__.py b/plays/__init__.py index 8189816..357390a 100644 --- a/plays/__init__.py +++ b/plays/__init__.py @@ -1,6 +1,7 @@ from plays.estadao import EstadaoPlay from plays.folha import FolhaPlay from plays.globo import GloboPlay +from plays.brasilDeFato import BrasilDeFatoPlay from plays.ig import IGPlay from plays.metropoles import MetropolesPlay from plays.r7 import R7Play @@ -14,6 +15,7 @@ from plays.jornaldaparaiba import JornalDaParaibaPlay __all__ = [ + BrasilDeFatoPlay, ClicRBSPlay, EstadaoPlay, FolhaPlay, diff --git a/plays/brasilDeFato.py b/plays/brasilDeFato.py new file mode 100644 index 0000000..c095d96 --- /dev/null +++ b/plays/brasilDeFato.py @@ -0,0 +1,221 @@ +import time +from playwright.sync_api import sync_playwright +from plays.base import BasePlay +from plays.items import EntryItem +from plog import logger + + +class BrasilDeFatoPlay(BasePlay): + name = "brasildefato" + + @classmethod + def match(cls, url): + """Identifica se a URL é do Brasil de Fato""" + return "brasildefato.com.br" in url + + def pre_run(self): + """Configurações antes da execução""" + pass + + def run(self) -> EntryItem: + """Extrai conteúdo da notícia do Brasil de Fato""" + with sync_playwright() as p: + browser = self.launch_browser(p, viewport={"width": 1920, "height": 1080}) + page = browser.new_page() + + logger.info(f"[{self.name}] Opening URL '{self.url}'...") + page.goto(self.url, timeout=180_000) + + # Aguardar carregamento do conteúdo principal + try: + page.wait_for_selector("h1", timeout=30000) + except Exception as e: + logger.warning(f"[{self.name}] Timeout waiting for h1: {str(e)}") + + # Extrair todos os campos + title = self._extract_title(page) + description = self._extract_description(page) + body = self._extract_body(page) + tags = self._extract_tags(page) + + return EntryItem( + title=title, + url=self.url, + description=description, + body=body, + tags=tags, + ) + + def _extract_title(self, page): + """Extrai o título da notícia""" + selectors = [ + "h1", + ".post-title h1", + ".article-title h1", + ".entry-title h1" + ] + + for selector in selectors: + try: + element = page.locator(selector) + if element.count() > 0: + title = element.first.inner_text().strip() + if title: + logger.info(f"[{self.name}] Successfully extracted title using: {selector}") + return title + except Exception as e: + logger.warning(f"[{self.name}] Failed to extract title with {selector}: {str(e)}") + continue + + logger.error(f"[{self.name}] Failed to extract title") + return "" + + def _extract_description(self, page): + """Extrai a descrição/subtítulo da notícia""" + selectors = [ + "h1 + h2", # H2 que vem logo após o H1 + "h2", # Qualquer H2 como subtítulo + ".subtitle", + ".lead", + ".summary", + ".article-subtitle", + ".post-subtitle", + ".excerpt" + ] + + for selector in selectors: + try: + element = page.locator(selector) + if element.count() > 0: + description = element.first.inner_text().strip() + if description and len(description) > 20: # Evitar textos muito curtos + logger.info(f"[{self.name}] Successfully extracted description using: {selector}") + return description + except Exception as e: + logger.warning(f"[{self.name}] Failed to extract description with {selector}: {str(e)}") + continue + + logger.info(f"[{self.name}] No description found") + return "" + + def _extract_body(self, page): + """Extrai o corpo da notícia""" + selectors = [ + ".post-content p", # Parágrafos do conteúdo + ".article-content p", + ".entry-content p", + ".content p", + "article p", + ".text p", + ".article-body p" + ] + + for selector in selectors: + try: + elements = page.locator(selector) + if elements.count() > 0: + # Extrair todos os parágrafos + paragraphs = [] + for i in range(elements.count()): + paragraph = elements.nth(i).inner_text().strip() + if paragraph and len(paragraph) > 10: # Evitar parágrafos muito curtos + paragraphs.append(paragraph) + + if paragraphs: + body = '\n\n'.join(paragraphs) + logger.info(f"[{self.name}] Successfully extracted body using: {selector}") + return body + except Exception as e: + logger.warning(f"[{self.name}] Failed to extract body with {selector}: {str(e)}") + continue + + # Fallback: tentar extrair todo o texto após os títulos + try: + # Extrair texto do corpo principal, excluindo header/footer + all_text = page.locator("body").inner_text() + lines = all_text.split('\n') + + # Encontrar onde começa o conteúdo real (após a data/autor) + content_lines = [] + start_content = False + + for line in lines: + line = line.strip() + if not line: + continue + + # Identificar início do conteúdo (geralmente após informações de data/autor) + if ('27.SET.2025' in line or 'REDAÇÃO' in line or 'Editado por:' in line): + start_content = True + continue + + if start_content and len(line) > 50: # Linhas substanciais + content_lines.append(line) + + if content_lines: + body = '\n\n'.join(content_lines[:10]) # Primeiros 10 parágrafos + logger.info(f"[{self.name}] Extracted body using fallback method") + return body + + except Exception as e: + logger.warning(f"[{self.name}] Fallback body extraction failed: {str(e)}") + + logger.error(f"[{self.name}] Failed to extract body") + return "" + + def _extract_tags(self, page): + """Extrai as tags/categorias da notícia""" + selectors = [ + ".tags a", + ".categories a", + ".post-tags a", + ".article-tags a", + "[rel='tag']", + ".tag-list a", + ".category-list a" + ] + + tags = [] + + for selector in selectors: + try: + elements = page.locator(selector) + if elements.count() > 0: + for i in range(elements.count()): + tag_text = elements.nth(i).inner_text().strip() + if tag_text and tag_text not in tags: + tags.append(tag_text) + if tags: + logger.info(f"[{self.name}] Successfully extracted {len(tags)} tags using: {selector}") + break + except Exception as e: + logger.warning(f"[{self.name}] Failed to extract tags with {selector}: {str(e)}") + continue + + # Fallback: extrair seção da notícia (ex: "SUL GLOBAL", "POLÍTICA", etc.) + if not tags: + try: + # Procurar pela seção que aparece antes do título + section_selectors = [ + "h1 + *:contains('GLOBAL')", + "h1 + *:contains('POLÍTICA')", + "h1 + *:contains('INTERNACIONAL')", + ".section-name", + ".category-name" + ] + + # Método alternativo: extrair da URL + url_parts = self.url.split('/') + if len(url_parts) >= 4: + # Pode haver uma seção na URL + potential_section = url_parts[-2] if url_parts[-1] == '' else None + if potential_section and len(potential_section) > 3: + tags.append(potential_section.replace('-', ' ').title()) + + except Exception as e: + logger.warning(f"[{self.name}] Failed to extract fallback tags: {str(e)}") + + if not tags: + logger.info(f"[{self.name}] No tags found") + + return tags \ No newline at end of file diff --git a/spiders/__init__.py b/spiders/__init__.py index d2bb555..67f48e6 100644 --- a/spiders/__init__.py +++ b/spiders/__init__.py @@ -1,4 +1,5 @@ from spiders.base import BaseSpider +from spiders.brasilDeFato import BrasilDeFatoSpider from spiders.estadao import EstadaoSpider from spiders.folha import FolhaSpider from spiders.globo import GloboSpider @@ -16,6 +17,7 @@ __all__ = [ BaseSpider, + BrasilDeFatoSpider, EstadaoSpider, FolhaSpider, GloboSpider, diff --git a/spiders/brasilDeFato.py b/spiders/brasilDeFato.py new file mode 100644 index 0000000..03b2064 --- /dev/null +++ b/spiders/brasilDeFato.py @@ -0,0 +1,116 @@ +import scrapy +from urllib.parse import urlparse +from spiders.base import BaseSpider +from spiders.items import URLItem + + +class BrasilDeFatoSpider(BaseSpider): + name = "brasildefato" + start_urls = ["https://www.brasildefato.com.br/"] + allowed_domains = ["brasildefato.com.br"] + + custom_settings = { + **BaseSpider.custom_settings, + "COOKIES_ENABLED": True, + "DOWNLOAD_DELAY": 2, + "DEFAULT_REQUEST_HEADERS": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + } + } + + def allow_url(self, url: str) -> bool: + p = urlparse(url) + path = p.path.rstrip('/') + + # 1) blacklist sections that are not news + blacklist = { + "/quem-somos", "/publicidade", "/contato", "/politica-privacidade", + "/feed", "/tv-brasil-de-fato", "/radioagencia", "/artes", "/colunistas", + "/programas", "/podcast", "/apoie" + } + + if "/podcast/" in path: + return False + + for blocked in blacklist: + if path.startswith(blocked): + return False + + # 2) require date pattern: YYYY/MM/DD/slug + segments = [seg for seg in path.split('/') if seg] + + if len(segments) >= 5: + # regional pattern: /state/YYYY/MM/DD/slug + try: + estado, year, month, day = segments[0], segments[1], segments[2], segments[3] + if (len(year) == 4 and year.isdigit() and + len(month) == 2 and month.isdigit() and + len(day) == 2 and day.isdigit() and + len(segments) >= 5): + slug = segments[4] + if slug.count('-') >= 1 and len(slug) > 10: + return True + except (IndexError, ValueError): + pass + + if len(segments) >= 4: + # national pattern: YYYY/MM/DD/slug + try: + year, month, day = segments[0], segments[1], segments[2] + if (len(year) == 4 and year.isdigit() and + len(month) == 2 and month.isdigit() and + len(day) == 2 and day.isdigit()): + slug = segments[3] + if slug.count('-') >= 1 and len(slug) > 10: + return True + except (IndexError, ValueError): + pass + + return False + + def start_requests(self): + for url in self.start_urls: + yield scrapy.Request( + url, + callback=self.parse, + dont_filter=True, + ) + + def parse(self, response): + import re + + # Handle XML/RSS feeds + if response.url.endswith('.xml') or 'xml' in response.headers.get('content-type', b'').decode(): + links = response.xpath('//loc/text()').getall() + links.extend(response.xpath('//url/text()').getall()) + links.extend(response.xpath('//link/text()').getall()) + for link in links: + if link and self.allow_url(link): + yield URLItem(url=link) + return + + # Extract URLs from HTML content + urls = response.css('a::attr(href)').getall() + for url in urls: + if url: + full_url = response.urljoin(url).split('#')[0].split('?')[0] + if self.allow_url(full_url): + yield URLItem(url=full_url) + + # Extract URLs using regex patterns + patterns = [ + r'"(https?://(?:www\.)?brasildefato\.com\.br/20\d{2}/\d{2}/\d{2}/[^"]+)"', + r'"(/20\d{2}/\d{2}/\d{2}/[^"]+)"', + r'href="(/20\d{2}/\d{2}/\d{2}/[^"]+)"', + r'href="(https?://(?:www\.)?brasildefato\.com\.br/20\d{2}/\d{2}/\d{2}/[^"]+)"' + ] + + for pattern in patterns: + matches = re.findall(pattern, response.text) + for match in matches: + full_url = response.urljoin(match) if match.startswith('/') else match + full_url = full_url.split('#')[0].split('?')[0] + if self.allow_url(full_url): + yield URLItem(url=full_url) \ No newline at end of file diff --git a/tests/plays/test_base.py b/tests/plays/test_base.py index f0741ab..808929d 100644 --- a/tests/plays/test_base.py +++ b/tests/plays/test_base.py @@ -3,6 +3,7 @@ from plays.base import BasePlay from plays import ( + BrasilDeFatoPlay, ClicRBSPlay, EstadaoPlay, FolhaPlay, @@ -98,6 +99,14 @@ def test_return_correct_scraper_r7(self): assert isinstance(scraper, R7Play) assert scraper.url == url + def test_return_correct_scraper_brasil_de_fato(self): + url = "https://www.brasildefato.com.br/2025/09/27/entry-slug" + + scraper = BasePlay.get_scraper(url) + + assert isinstance(scraper, BrasilDeFatoPlay) + assert scraper.url == url + def test_raise_error_if_no_scraper_is_found(self): url = "https://any.com.br"