Skip to content

Projeto desenvolvido em Python para raspagem de dados de notícias utilizando Parsel e MongoDB.

Notifications You must be signed in to change notification settings

flaviojoaofelix/trybe-project-tech-news

Repository files navigation

Trybe

Curso de Desenvolvimento Web Full-Stack

  • Módulo de Ciência da Computação

Projeto: Tech News

Neste projeto desenvolvido em Python, foi criado uma aplicação com o intuito de raspar os dados das notícias do Blog da Trybe utilizando a biblioteca Parsel e guarda-las em um banco de dados MongoDB.

Tecnologias Utilizadas no Projeto

1. Python1

  • Como a linguagem principal do projeto.

2. Requests2

  • Para fazer a requisição HTTP para o site.

2. Parsel3

  • Para extração dos dados do HTML utilizando seletores XPath e CSS.

3. Rate Limit4

  • Para limitar o número de requisições feitas para o site.

4. PyMongo5

  • Para trabalhar com o banco de dados MongoDB.

5. PyTest6

  • Para testar a aplicação.

6. MongoDB7

  • Para armazenamento dos dados.

7. Docker8

  • Para criar uma máquina virtual com o Banco de Dados MongoDB

Como Utilizar

Configurar Localmente

Requisitos

Passos para Instalação/Configuração

  1. Faça o clone do projeto:
git clone git@github.com:flaviojoaofelix/trybe-project-tech-news.git
  1. Crie e ative o ambiente virtual
python3 -m venv .venv && source .venv/bin/activate
  1. Instalar as dependências
python3 -m pip install -r dev-requirements.txt
  1. Subir o container Docker do MongoDB
docker-compose up -d mongodb
  1. Comando para rodar a aplicação
tech-news-analyzer

ESTRUTURA DO PROJETO

🧱 Estrutura do Projeto
Este repositório já contém um template com a estrutura de diretórios e arquivos, tanto de código quanto de teste criados. Veja abaixo:
Legenda:
🔸Arquivos que não podem ser alterados
🔹Arquivos a serem alterados para realizar os requisitos.
.
├── tech_news
│   ├── analyzer
│   │   ├── 🔹ratings.py
│   │   ├── 🔸reading_plan.py
│   │   └── 🔹search_engine.py
│   ├── 🔸database.py
│   └── 🔹menu.py
│   └── 🔹scraper.py
├── tests
│   ├── reading_plan
│   │   ├── 🔸__init__.py
│   │   ├── 🔸conftest.py
│   │   ├── 🔸mocks.py
│   │   └── 🔹test_reading_plan.py
│   ├── 🔸assets/*
│   ├── 🔸__init__.py
│   ├── 🔸marker.py
│   ├── 🔸test_menu.py
│   ├── 🔸test_ratings.py
│   ├── 🔸test_scraper.py
│   └── 🔸test_search_engine.py
├── 🔸dev-requirements.txt
├── 🔸docker-compose.yml
├── 🔸Dockerfile
├── 🔸pyproject.toml
├── 🔸README.md
├── 🔸requirements.txt
├── 🔸setup.cfg
├── 🔸setup.py
├── 🔸trybe-filter-repo.sh
└── 🔸trybe.yml

Apesar do projeto já possuir uma estrutura base, você perceberá que possui arquivos vazios, ou seja, neles você quem deve implementar as classes. Novos arquivos e funções podem ser criados conforme a necessidade da sua implementação, porém não remova arquivos já existentes.

REQUISITOS DO PROJETO

Lista de Requisitos do Projeto

Requisitos obrigatórios

1 - Crie a função fetch

local: tech_news/scraper.py

Antes de fazer scrape, precisamos de uma página! Esta função será responsável por fazer a requisição HTTP ao site e obter o conteúdo HTML. Alguns cuidados deverão ser tomados: como a nossa função poderá ser utilizada várias vezes em sucessão, na nossa implementação devemos nos assegurar que um Rate Limit será respeitado.

  • A função deve receber uma URL
  • A função deve fazer uma requisição HTTP get para esta URL utilizando a função requests.get
  • A função deve retornar o conteúdo HTML da resposta.
  • A função deve respeitar um Rate Limit de 1 requisição por segundo; Ou seja, caso chamada múltiplas vezes, ela deve aguardar 1 segundo entre cada requisição que fizer. Dica: Uma forma simples de garantir que cada requisição seja feita com um intervalo mínimo de um segundo é utilizar time.sleep(1) antes de cada requisição. (Existem outras formas mais eficientes.)
  • Caso a requisição seja bem sucedida com Status Code 200: OK, deve ser retornado seu conteúdo de texto;
  • Caso a resposta tenha o código de status diferente de 200, deve-se retornar None;
  • Caso a requisição não receba resposta em até 3 segundos, ela deve ser abandonada (este caso é conhecido como "Timeout") e a função deve retornar None.

📌 Você vai precisar definir o header user-agent para que a raspagem do blog da Trybe funcione corretamente. Para isso, preencha com o valor "Fake user-agent" conforme exemplo abaixo:

{ "user-agent": "Fake user-agent" }
✍️ Teste manual

Abra um terminal Python importando estas funções através do comando:

python3 -i tech_news/scraper.py

Agora invoque as funções utilizando diferentes parâmetros. Exemplo:

html = fetch(url_da_noticia)
scrape_news(html)
🤖 O que será verificado pelo avaliador
  • A função utiliza o método get() da biblioteca requests

  • A função executada com uma URL correta retorna o conteúdo html

  • A função, sofrendo timeout, retorna None

  • A função retorna None quando recebe uma resposta com código diferente de 200

  • A função respeita o rate limit

2 - Crie a função scrape_updates

local: tech_news/scraper.py

Para conseguirmos fazer o scrape da página de uma notícia, primeiro precisamos de links para várias páginas de notícias. Estes links estão contidos na página inicial do blog da Trybe (https://blog.betrybe.com).

Esta função fará o scrape da página Novidades para obter as URLs das páginas de notícias. Vamos utilizar as ferramentas que aprendemos no curso, como a biblioteca Parsel, para obter os dados que queremos de cada página.

  • A função deve receber uma string com o conteúdo HTML da página inicial do blog
  • A função deve fazer o scrape do conteúdo recebido para obter uma lista contendo as URLs das notícias listadas.
    • ⚠️ Atenção: NÃO inclua a notícia em destaque da primeira página, apenas as notícias dos cards.
  • A função deve retornar esta lista.
  • Caso não encontre nenhuma URL de notícia, a função deve retornar uma lista vazia.
✍️ Teste manual

Abra um terminal Python importando estas funções através do comando:

python3 -i tech_news/scraper.py

Agora invoque as funções utilizando diferentes parâmetros. Exemplo:

html = fetch(url_da_noticia)
scrape_updates(html)
🤖 O que será verificado pelo avaliador
  • A função retorna os dados esperados quando chamada com os parâmetros corretos

  • A função retorna uma lista vazia quando chamada com parâmetros incorretos

3 - Crie a função scrape_next_page_link

local: tech_news/scraper.py

Para buscar mais notícias, precisaremos fazer a paginação, e para isto, vamos precisar do link da próxima página. Esta função será responsável por fazer o scrape deste link.

  • A função deve receber como parâmetro uma string contendo o conteúdo HTML da página de novidades (https://blog.betrybe.com)
  • A função deve fazer o scrape deste HTML para obter a URL da próxima página.
  • A função deve retornar a URL obtida.
  • Caso não encontre o link da próxima página, a função deve retornar None
🤖 O que será verificado pelo avaliador
  • A função retorna os dados esperados quando chamada com os parâmetros corretos

  • A função retorna None quando chamada com os parâmetros incorretos

4 - Crie a função scrape_news

local: tech_news/scraper.py

Agora que sabemos pegar páginas HTML, e descobrir o link de notícias, é hora de fazer o scrape dos dados que procuramos!

  • A função deve receber como parâmetro o conteúdo HTML da página de uma única notícia

  • A função deve, no conteúdo recebido, buscar as informações das notícias para preencher um dicionário com os seguintes atributos:

    • url - link para acesso da notícia.
    • title - título da notícia.
    • timestamp - data da notícia, no formato dd/mm/AAAA.
    • writer - nome da pessoa autora da notícia.
    • reading_time - número de minutos necessários para leitura.
    • summary - o primeiro parágrafo da notícia.
    • category - categoria da notícia.
  • Exemplo de um retorno da função com uma notícia fictícia:

{
  "url": "https://blog.betrybe.com/novidades/noticia-bacana",
  "title": "Notícia bacana",
  "timestamp": "04/04/2021",
  "writer": "Eu",
  "reading_time": 4,
  "summary": "Algo muito bacana aconteceu",
  "category": "Ferramentas",
}

📌 Muita atenção aos tipos dos campos, por exemplo, category é uma string enquanto reading_time é numérico.

📌 Os textos coletados em title e summary podem conter alguns caracteres vazios ao final. O teste espera que esses caracteres sejam removidos.

📌 É bom saber que ao fazer scraping na vida real, você está sempre "refém" de quem construiu o site. Por exemplo, pode ser que nem toda notícia tenha exatamente o mesmo HTML/CSS e você precise de criatividade para contornar isso.

📌 Caso uma tag possua outras tags aninhadas, você pode usar o seletor * para obter informações da tag ancestral e também de suas tags descendentes.

Veja um exemplo:
<p>
  Recentemente, a Alemanha fez a
  <a
    href="https://www.tecmundo.com.br/mobilidade-urbana-smart-cities/155000-musk-tesla-carros-totalmente-autonomos.htm"
    rel="noopener noreferrer"
    target="_blank"
    >Tesla</a
  >
  “pisar no freio” quanto ao uso de termos comerciais relacionados a carros
  autônomos, mas quem pensa que esse é um sinal de resistência à introdução de
  novas tecnologias se engana. Isso porque, de acordo o
  <em>Automotive News Europe</em>, o país está se preparando para se tornar o
  primeiro do mundo a criar uma ampla estrutura para regulamentar tais
  veículos de nível 4.
</p>

Repare que dentro da tag p encontram-se duas outras tags. Esse é um caso onde a tag p é uma ancestral e as tags a e em são as descendentes. Assim, podemos usar o seletor * para fazer refrência à todas essas tags simultaneamente.

Você pode encontrar mais informações sobre esse seletor aqui

🤖 O que será verificado pelo avaliador
  • Será verificado se a função retorna o conteúdo correto e no formato correto, dada uma página de notícia exemplo.

👍 Terminou o requisito 4?

Parabéns! Este é o requisito mais longo do projeto, e também a funcionalidade central do nosso tech-news. Faça um break, tome uma água, e #vamoquevamo para os próximos requisitos!


5 - Crie a função get_tech_news para obter as notícias

local: tech_news/scraper.py

Agora, chegou a hora de aplicar todas as funções que você acabou de fazer. Com estas ferramentas prontas, podemos fazer nosso scraper mais robusto com a paginação.

  • A função deve receber como parâmetro um número inteiro amount e buscar as amount notícias mais recentes do site.
  • Utilize as funções fetch, scrape_updates, scrape_next_page_link e scrape_news para buscar as notícias e processar seu conteúdo.
  • As notícias buscadas devem ser inseridas no MongoDB; Para acessar o banco de dados, importe e utilize as funções que já temos prontas em tech_news/database.py
  • Após inserir as notícias no banco, a função deve retornar estas mesmas notícias.

📌 De aqui em diante, usaremos o MongoDB.

Rodar MongoDB via Docker: docker-compose up -d mongodb no terminal. Para mais detalhes acerca do mongo com o docker, olhe o arquivo docker-compose.yml

Caso queira instalar e rodar o servidor MongoDB nativo na máquina, siga as instruções no tutorial oficial: Ubuntu: https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/
MacOS: https://docs.mongodb.com/guides/server/install/

Com o banco de dados rodando, o nosso módulo conseguirá acessá-lo sem problemas. Importe o módulo tech_news/database.py e chame as funções contidas nele. Não altere as funções deste módulo; elas serão utilizadas nos testes.

🤖 O que será verificado pelo avaliador
  • A função create_news do tech_news/database.py foi chamada corretamente

  • A função retorna a quantidade correta de notícias

6 - Teste a classe ReadingPlanService

local: tests/reading_plan/test_reading_plan.py

Agora que temos meios de popular nosso banco de dados com notícias, podemos fazer uso de uma funcionalidade implementada por outro time!

O serviço de planejamento de leituras, implementado no arquivo tech_news/analyzer/reading_plan.py, coleta as notícias do banco de dados e as divide em 2 agrupamentos:

  1. readable: notícias que podem ser lidas em até X minutos
  2. unreadable: notícias que não podem ser lidas em até X minutos

Além disso, as notícias readable são organizadas em sub-grupos cuja soma dos tempos de leitura seja menor que X. Assim, a pessoa leitora pode ler mais do que 1 notícia sem ultrapassar o tempo disponível!

O valor de X, que é o tempo de leitura que uma pessoa tem disponível, é passado por parâmetro no método group_news_for_available_time, que é um método de classe.

📌 Você deve implementar o teste test_reading_plan_group_news para garantir o funcionamento correto deste método que está explicado abaixo. Ah, não se preocupe em testar a chamada dos outros métodos da classe!

📌 O método group_news_for_available_time utiliza a função find_news do módulo tech_news.database para coletar as notícias no banco de dados. Pode ser importante mockar essa função para que o resultado do teste não dependa do banco de dados.

👀 Entenda o retorno do método group_news_for_available_time

Um exemplo de resultado da chamada ReadingPlanService.group_news_for_available_time(10) pode ser:

{
    "readable": [  # Grupos de notícias que tem 'reading_time' menor ou igual ao parâmetro
        {
            "unfilled_time": 3,  # tempo disponível restante (não preenchido pelas notícias escolhidas)
            "chosen_news": [  # Lista de notícias escolhidas
                (
                    "Não deixe para depois: Python é a linguagem mais quente do momento",  # 'title' da notícia
                    4,  # 'reading_time' da notícia
                ),
                (
                    "Selenium, BeautifulSoup ou Parsel? Entenda as diferenças",
                    3,
                ),
            ],
        },
        {
            "unfilled_time": 0,
            "chosen_news": [
                (
                    "Pytest + Faker: a combinação poderosa dos testes!",
                    10,
                )
            ],
        },
    ],
    "unreadable": [  # Lista de notícias que tem 'reading_time' maior que o parâmetro
        ("FastAPI e Flask: frameworks para APIs em Python", 15),
        ("A biblioteca Pandas e o sucesso da linguagem Python", 12),
    ],
}
🤖 O que será verificado pelo avaliador
  • Seu teste deve validar que uma exceção é levantada se o método é chamado com parâmetro de valor inválido
  • Seu teste deve validar que os valores 'unfilled_time' retornados estão corretos
  • Seu teste deve validar que os valores em 'readable' retornados estão corretos
  • Seu teste deve validar que os valores em 'unreadable' estão corretos

7 - Crie a função search_by_title

local: tech_news/analyzer/search_engine.py

Além de testar funcionalidades que acessam o banco, podemos fazer as nossas próprias funcionalidades! Esta função irá fazer buscas por título.

  • A função deve receber uma string com um título de notícia
  • A função deve buscar as notícias do banco de dados por título
  • A função deve retornar uma lista de tuplas com as notícias encontradas nesta busca. Exemplo:
[
  ("Título1_aqui", "url1_aqui"),
  ("Título2_aqui", "url2_aqui"),
  ("Título3_aqui", "url3_aqui"),
]
  • A busca deve ser case insensitive

  • Caso nenhuma notícia seja encontrada, deve-se retornar uma lista vazia.

📌 Lembre-se; para acesso ao banco de dados importe db definido no módulo tech_news/database.py.

✍️ Teste manual Abra um terminal Python importando esta função através do comando

python3 -i tech_news/analyzer/search_engine.py

Agora invoque a função utilizando diferentes parâmetros. Exemplo:

search_by_title("Algoritmos").

🤖 O que será verificado pelo avaliador
  • Será validado que é possível buscar uma notícia pelo título com sucesso

  • Será validado que ao buscar por um título que não existe, o retorno deve ser uma lista vazia

  • Será validado que é possível buscar uma notícia com sucesso, tanto pelo título em maiúsculas como em minúsculas.

8 - Crie a função search_by_date

local: tech_news/analyzer/search_engine.py

Esta função irá buscar as notícias do banco de dados por data.

  • A função deve receber como parâmetro uma data no formato ISO AAAA-mm-dd
  • A função deve buscar as notícias do banco de dados por data.
  • A função deve ter retorno no mesmo formato do requisito anterior.
  • Caso a data seja inválida, ou esteja em outro formato, uma exceção ValueError deve ser lançada com a mensagem Data inválida.
  • Caso nenhuma notícia seja encontrada, deve-se retornar uma lista vazia.

📌 Lembre-se: A função recebe uma data no formato ISO AAAA-mm-dd, mas no banco a data está salva no formato dd/mm/AAAA. Dica: Lembrem-se de como trabalhamos com datas nos projetos anteriores.

✍️ Teste manual Abra um terminal Python importando esta função através do comando

python3 -i tech_news/analyzer/search_engine.py

Agora invoque a função utilizando diferentes parâmetros. Exemplo:

search_by_date("2021-04-04")

🤖 O que será verificado pelo avaliador
  • Será validado que é possível buscar uma notícia pela data com sucesso

  • Será validado que ao buscar por uma data que não existe, o retorno deve ser uma lista vazia

  • Sera validado que ao buscar por uma data com formato inválido, deve lançar um erro ValueError com a mensagem Data inválida.

9 - Crie a função search_by_category

local: tech_news/analyzer/search_engine.py

Esta função irá buscar as notícias por categoria.

  • A função deve receber como parâmetro o nome da categoria completo.
  • A função deve buscar as notícias do banco de dados por categoria.
  • A função deve ter retorno no mesmo formato do requisito anterior.
  • Caso nenhuma notícia seja encontrada, deve-se retornar uma lista vazia.
  • A busca deve ser case insensitive
✍️ Teste manual

Abra um terminal Python importando esta função através do comando:

python3 -i tech_news/analyzer/search_engine.py

Agora invoque a função utilizando diferentes parâmetros. Exemplo:

search_by_category("Ferramentas").

🤖 O que será verificado pelo avaliador
  • Será validado que é possível buscar uma notícia pela categoria com sucesso

  • Será validado que ao buscar por uma categoria que não existe, o retorno deve ser uma lista vazia

  • Será validado que é possível buscar uma notícia tanto pela categoria em maiúsculas como em minúsculas

10 - Crie a função top_5_categories

local: tech_news/analyzer/ratings.py

Esta função irá listar as cinco categorias com maior ocorrência no banco de dados.

  • A função deve buscar as categorias do banco de dados e calcular a sua "popularidade" com base no número de ocorrências;
  • As top 5 categorias da análise devem ser retornadas em uma lista no formato ["category1", "category2"];
  • A ordem das categorias retornadas deve ser da mais popular para a menos popular, ou seja, categorias que estão em mais notícias primeiro;
  • Em caso de empate, o desempate deve ser por ordem alfabética de categoria.
  • Caso haja menos de cinco categorias, no banco de dados, deve-se retornar todas as categorias existentes;
  • Caso não haja categorias disponíveis, deve-se retornar uma lista vazia.
✍️ Teste manual Abra um terminal Python importando esta função através do comando:

python3 -i tech_news/analyzer/ratings.py

Agora invoque a função utilizando diferentes parâmetros. Exemplo:

top_5_categories().

🤖 O que será verificado pelo avaliador
  • Será validado que é possível buscar as cinco top categorias

  • Será validado que é possível buscar as cinco top categorias e retornar vazio caso não tenha nenhuma notícia

  • Caso houver menos de 5 categorias, serão retornadas quantas houverem

---

Requisitos bônus

11 - Crie a função analyzer_menu

local: tech_news/menu.py

Esta função é o menu do nosso programa. Através dele poderemos operar as funcionalidades que criamos. Será um menu de opções, em que cada opção pede as informações necessárias para disparar uma ação.

  • O texto exibido pelo menu deve ser exatamente:
Selecione uma das opções a seguir:
 0 - Popular o banco com notícias;
 1 - Buscar notícias por título;
 2 - Buscar notícias por data;
 3 - Buscar notícias por categoria;
 4 - Listar top 5 categorias;
 5 - Sair.
  • Caso a opção 0 seja selecionada, seve-se exibir a mensagem "Digite quantas notícias serão buscadas:"
  • Caso a opção 1 seja selecionada, deve-se exibir a mensagem "Digite o título:";
  • Caso a opção 2 seja selecionada, deve-se exibir a mensagem "Digite a data no formato aaaa-mm-dd:";
  • Caso a opção 3 seja selecionada, deve-se exibir a mensagem "Digite a categoria:";
  • Caso a opção não exista, exiba a mensagem de erro "Opção inválida" na stderr.

📌 A função input deve ser utilizada para receber a entrada de dados da pessoa usuária.

✍️ Teste manual

Dentro de um ambiente virtual onde seu projeto foi configurado, para o menu ser exibido digite o comando

tech-news-analyzer

Isto acontece pois durante a configuração inicial do projeto já configuramos para que a função seja corretamente chamada quando este comando seja invocado.

12 - Implemente as funcionalidades do menu

local: tech_news/menu.py

  • Quando selecionada uma opção do menu, e inseridas as informações necessárias, a ação adequada deve ser realizada.
  • Caso a opção 0 seja selecionada, a função get_tech_news deve ser importada;
  • Caso a opção 1 seja selecionada, a função search_by_title deve ser importada e seu resultado deve ser impresso em tela;
  • Caso a opção 2 seja selecionada, a função search_by_date deve ser importada e seu resultado deve ser impresso em tela;
  • Caso a opção 3 seja selecionada, a função search_by_category deve ser importada e seu resultado deve ser impresso em tela;
  • Caso a opção 4 seja selecionada, a função top_5_categories deve ser importada e seu resultado deve ser impresso em tela;
  • Caso a opção 5 seja selecionada, deve-se encerrar a execução do script e exibir a mensagem "Encerrando script";
  • Caso alguma exceção seja lançada, a mesma deve ser capturada e sua mensagem deve ser exibida na saída padrão de erros (stderr).
✍️ Teste manual

Dentro de um ambiente virtual onde seu projeto foi configurado, para interagir com o menu digite o comando

tech-news-analyzer


Informações adicionais e Referências

🚧 README em construção 🚧

Footnotes

  1. Linguagem Python

  2. Biblioteca Requests

  3. Biblioteca Parsel

  4. Biblioteca Ratelimit

  5. Biblioteca PyMongo

  6. Pytest Framework

  7. Banco de Dados MongoDB

  8. Docker