Usando corrente de Markov para gerar notícias falsas – Gazeta Robótica Devlog 1

Compartilhar:

Autor

Pessoa mascarada e de capuz lê jornal em chamas

    Primeiro de tudo: feliz 2021!   

    Projeto no Github.

    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 Scrapping
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')

Bom, 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.
    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"httpS+", "", # 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á 🙂

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Posts Relacionados