Usando corrente de Markov para gerar notícias falsas - Gazeta Robótica Devlog 1
![]() |
Foto de Connor Danylenko no Pexels
|
Primeiro de tudo: feliz 2021!
Quando resolvi implementar a raspagem de notícias e a corrente de markov no bot, eu tinha dois grandes problemas: não sabia como implementar um processo de raspagem de notícias e nem como criar uma corrente de Markov. Daí tive que lembrar de um dos principais lemas da programação: "vamos por partes".
Por sinal, esse lance de gerar notícias falsas
com uma corrente de Markov já foi feito 256315 vezes lá na gringa e por isso
tem vários tutoriais sobre o tópico por aí. Não achei nenhuma iniciativa
parecida aqui no Brasil, então acho que tudo bem fazer.
O primeiro objetivo: criar um script que me permitisse coletar uma grande quantidade de notícias de vários portais de uma vez. No primeiro tutorial que encontrei sobre o tema, o autor utilizava BeuatifulSoup para fazer isso. Fui atrás para conhecer mais e trata-se de uma biblioteca (para python, claro) focada em raspar e analisar o conteúdo de sites. É uma excelente ferramenta para navegar pelo html de uma página e obter um dado específico (ou vários). Com a facilidade típica do Python, consegui criar um pequeno script para testar a viabilidade de fazer a raspagem com o BeautifulSoup.
#Bibliotecas de ScrappingBom, estava funcionando! Mas haviam alguns problemas, dentre eles: cada
portal de notícia precisava ser acompanhado da tag onde ficavam as chamadas e
cada um disponibilizava as notícias de formas diferentes. Além disso, era
comum você ter notícias em tags diferentes no mesmo site (por exemplo:
notícias em destaque em tags diferentes das demais). Essas variações realmente
me incomodavam, já que um dos maiores princípios da programação é a
padronização, o que era impossível de se obter com esse método. Para se ter
uma ideia, o script conseguia fácil raspar +80 notícias da Folha, mas penava
pra conseguir +20 do Estadão - isso tudo porque eles organizavam suas chamadas
de formas diferentes.
import requests
from bs4 import
BeautifulSoup
def pega_noticias(url, nome_id, tag, nome_class): #Teste para
scrapping do UOL
#Declaração de variáveis
lista_output = []
#Processamento
pagina = requests.get(url)
#print(pagina) #Permite saber se o request foi bem sucedido [200] ou não
[404]
sopa = BeautifulSoup(pagina.content,
'html.parser')
if(nome_id != 'null'):
body = sopa.find(id = nome_id)
lista_noticias_html = body.find_all(tag,
class_ = nome_class)
else:
lista_noticias_html = sopa.find_all(tag,
class_ = nome_class)
for noticia in
lista_noticias_html:
lista_output.append(noticia.text)
return
lista_output
def scrap_noticias():
scrap_output =
[]
#Grande mídia
scrap_output += pega_noticias(UOL, 'corpo', 'span', 'chamada cor2-hover
cor-transition')
Foi nessa hora que
me peguei pensando "caramba, se pelo menos todos esses portais fizessem algo
parecido com o "últimas notícias" da Folha, onde eles disponibilizam todas as
notícias em uma lista padronizada..."
Aí me toquei que eles já faziam isso, mas não
no próprio site e sim no twitter! Lá, cada portal disponibilizava suas
notícias de forma padronizada em uma grande lista, e como eu já estava usando
o tweepy para lidar com a API do twitter, utilizar a rede social para raspar
as notícias era matar dois coelhos numa cajadada só. Resolvi então dar uma
olhada na documentação da biblioteca tweepy para ver qual método me permitiria
obter vários tweets de uma conta (algo que eu já sabia ser possível porque é
demonstrado na introdução da própria API do twitter). Depois de uma lida
no StackOverflow na documentação da biblioteca, criei esse
script para testar essa forma de obter notícias:
def raspa_tweets(api_obj: object, conta: str, qntd: int) -> 'lista de
objetos':
'''
Retorna certa quantidade de tweets da conta desejada
Args:
api_obj -> Objeto com informações e autenticação da API do Twitter
conta -> Conta específica da qual se deseja raspar dados
qntd -> Quantos tweets devem ser obtidos
Returns:
lista de objetos
'''
# O tweet_mode serve pra evitar que o texto
dos tweets seja cortado pelo tweepy
return tweepy.Cursor(api_obj.user_timeline,
id=conta, tweet_mode="extended").items(qntd)
def main():
objeto_api = criar_objeto_API() # Objeto para
realizar operações na API do twitter
lista_chamadas = raspa_tweets(objeto_api,
'UOL', QNTD)
Novamente, deu certo, e
bem melhor do que antes - mas com alguns problemas. Enquanto esse método me
permitia obter exatamente x tweets de cada portal, não vinham só notícias aí.
Também são inclusos nos seus tweets RT's e Replys, logo eu acabava me
deparando com tweets tipo "olá @fulano, entre em contato com nosso suporte".
Eu precisava filtrar esses tipo de tweet e ficar apenas com os principais
feitos pela própria conta (as notícias em si).
Quando você raspa um tweet através do método tweepy.Cursor(api_obj.user_timeline)
no tweepy, ele te retorna um objeto que inclui várias informações do tweet
específico - incluindo se ele é uma reply ou não. Isso facilita demais o
trabalho de filtrar as replys, já que basta um simples teste para saber o
tweet é ou não. Infelizmente, o mesmo não ocorre com RT - o objeto retornado
pelo Tweepy não diz se o tweet foi retweetado, mas traz indicativos. Todo RT
feito sem comentários (ou seja, que não é um "quote retweet") tem seu texto
extraído como "RT" + texto do tweet. Então eu só precisava analisar o texto
de cada tweet para saber se ele possuía esse RT ou não - algo fácil de fazer
com a biblioteca re, que implementa o famoso RegEx no Python.
Retirando Replys:
def remove_replys(tweets: list or tuple) -> list:
'''Recebe uma lista de objetos tweets. Retorna
aqueles que não são replys.'''
# Utiliza propriedades do objeto para saber se
o tweet é uma reply
return [tweet for tweet in tweets if
bool(tweet.in_reply_to_status_id) is False]Retirando RT's:
def _removeRT(tweets: list or tuple) -> list:
"""Recebe uma lista de tweets e retorna uma
lista sem tweets que incluam 'RT'"""
return [tweet for tweet in tweets if
bool(re.search('RT', tweet)) is False]
"Limpando" o tweet:
def _regras_noticias(lista_tweets: list or tuple) -> list:
lista_saida = []
for tweet in lista_tweets:
lista_saida.append(
re.sub(r"http\S+", "", # Retira Links do tweet
(http ou https)
re.sub(r"#\S+", "", # Retira Hashtags
re.sub(r"RT", "", # Retira 'RT' (se sobrar
algum do remove_RT)
re.sub(r"@\S+", "@insensoblog", # Substitui
as @
re.sub(r">", "", # Retira esse
caractere que vem nos tweets do Estadão
tweet
)
)
)
)
)
)
return lista_saida
Com os devidos mecanismos de raspagem e filtragem implementados, era hora de lidar com a parte difícil do projeto: criar a corrente de Markov.
Se você não sabe o que é uma corrente de Markov, tudo bem - eu também não sabia até ler o primeiro tutorial. Você pode ver explicações muito boas sobre o conceito aqui e aqui (esse é assustador de tão bem feito), mas basicamente se trata de uma cadeia de elementos interligados com uma quantificação da "força" dessa ligação.
Aqui o lance de procurar vários tutoriais realmente salvou minha pele. Um deles me introduziu a uma biblioteca para python focada justamente em criar correntes de Markov - a incrível Markovify. Dando uma rápida olhada na página do github, aprendi que ela pode receber um arquivo e gerar a corrente, além de imediatamente gerar frases através dessa corrente. Para melhorar, ela também permite que a corrente seja armazenada e recuperada depois, ideal para lidar com entradas grandes, que tendem a gerar correntes de markov maiores ainda.
Ok, mas antes de passar o conteúdo
raspado para a Markovify, eu precisava armazenar ele - o que já trazia uma
série de implicações. A database de notícias não seria feita com uma única
raspagem, mas sim através de um processo contínuo de adição de chamadas
mais recentes. O problema é que a mesma notícia poderia vir a ser
adicionada duas vezes (ou até mais, dependendo da frequência de raspagem e
de quantas noticias o portal posta em seu twitter).
Quando ainda estava testando a raspagem diretamente dos sites, testei o
armazenamento das chamadas (em formato de string) em um bloco de notas e a
retirada de chamadas repetidas através de uma comparação direta, mas por
algum motivo certas chamadas repetidas simplesmente davam falso negativo e
passavam como originais. Essa experiência me fez abominar completamente
essa forma de armazenamento e entendi que precisava de algo mais
sofisticado, precisava de uma coisa que tinha medo - a biblioteca
Pandas.
Panda nada mais é do que uma biblioteca
voltada para criação, gerenciamento e manipulação de Dataframes. Para usar
ela eu teria que aprender bastante coisa, mas sabia que iria valer a pena
por que ela possuía uma ferramenta do qual eu estava realmente necessitado
- a função .drop_duplicates, basicamente uma forma automática de evitar aquele tormento do parágrafo
anterior.
Depois de mais um tempo olhando a documentação da biblioteca (e umas perguntas no StackOverflow), consegui criar um "protótipo". Nessa versão, o projeto já raspava notícias, filtrava elas e agora passariam o resultado para um script com funções da biblioteca Pandas para que as chamadas fossem armazenadas. Logo me veio à mente que seria legal saber de onde veio cada notícia, quem sabe até poderia fazer alguma coisa a mais em cima dessa database depois. Decidi então criar um dataframe com 2 colunas: a notícia em si e o portal da onde ela foi tirada. Depois acabei adicionando a coluna do Index para ter mais facilidade na hora de saber quantas notícias haviam no DataFrame.
A explicação exata de como rolou essa última parte + estruturação do projeto e processo de documentação estarão disponíveis no próximo Devlog.
Até lá :)

