- Módulo de Ciência da Computação
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.
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
- Faça o clone do projeto:
git clone [email protected]:flaviojoaofelix/trybe-project-tech-news.git
- Crie e ative o ambiente virtual
python3 -m venv .venv && source .venv/bin/activate
- Instalar as dependências
python3 -m pip install -r dev-requirements.txt
- Subir o container Docker do MongoDB
docker-compose up -d mongodb
- Comando para rodar a aplicação
tech-news-analyzer
🧱 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.
Lista de Requisitos do Projeto
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çãorequests.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 retornarNone
; - 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
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
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
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 formatodd/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.
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!
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 asamount
notícias mais recentes do site. - Utilize as funções
fetch
,scrape_updates
,scrape_next_page_link
escrape_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
dotech_news/database.py
foi chamada corretamente -
A função retorna a quantidade correta de notícias
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:
readable
: notícias que podem ser lidas em atéX
minutosunreadable
: 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
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 comandopython3 -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.
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 mensagemData 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 comandopython3 -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 mensagemData inválida
.
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
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
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.
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çãoget_tech_news
deve ser importada; - Caso a opção
1
seja selecionada, a funçãosearch_by_title
deve ser importada e seu resultado deve ser impresso em tela; - Caso a opção
2
seja selecionada, a funçãosearch_by_date
deve ser importada e seu resultado deve ser impresso em tela; - Caso a opção
3
seja selecionada, a funçãosearch_by_category
deve ser importada e seu resultado deve ser impresso em tela; - Caso a opção
4
seja selecionada, a funçãotop_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