Nesta aula, vamos consolidar os conhecimentos de Python construindo uma ferramenta de linha de comando (CLI) completa. Você aprenderá a criar um programa profissional que recebe argumentos, lê arquivos, processa o conteúdo e gera uma saída útil. Este projeto prático combina vários tópicos essenciais: manipulação de argumentos com argparse, leitura de arquivos, processamento de texto e formatação de saída.

Ao final, você terá uma ferramenta chamada textstats que analisa arquivos de texto e fornece estatísticas como contagem de palavras, linhas e caracteres, além de permitir opções como ignorar palavras vazias e gerar um relatório detalhado. Vamos desenvolver passo a passo, entendendo cada componente e como eles se integram.

argparse

O módulo argparse é a forma padrão de criar interfaces de linha de comando em Python. Ele permite definir argumentos posicionais e opcionais, gerar mensagens de ajuda automaticamente e tratar erros de forma consistente. Com argparse, você pode especificar tipos, valores padrão, ações e até mesmo subcomandos, tornando suas ferramentas muito mais robustas e amigáveis.

No nosso projeto, usaremos argparse para definir:

  • Um argumento posicional arquivo que indica o caminho do arquivo a ser processado.
  • Uma opção --palavras que, quando presente, exibe a lista de palavras mais frequentes.
  • Uma opção --min-length que define o comprimento mínimo para uma palavra ser considerada (padrão 1).
  • Uma opção --saida que permite especificar um arquivo de saída para o relatório.

Vamos ver um exemplo básico de configuração do parser:

import argparse

parser = argparse.ArgumentParser(description='Analisa um arquivo de texto e mostra estatísticas.')
parser.add_argument('arquivo', help='Caminho do arquivo de texto')
parser.add_argument('--palavras', action='store_true', help='Lista as palavras mais frequentes')
parser.add_argument('--min-length', type=int, default=1, help='Comprimento mínimo das palavras (padrão: 1)')
parser.add_argument('--saida', help='Arquivo de saída para o relatório (opcional)')

args = parser.parse_args()
print(f'Arquivo: {args.arquivo}')
print(f'Palavras? {args.palavras}')
print(f'Comprimento mínimo: {args.min_length}')
print(f'Saída: {args.saida}')

Observe como action='store_true' transforma a opção em um booleano: se a flag estiver presente, o valor será True; caso contrário, False. O parâmetro type=int garante que o valor seja convertido para inteiro, e default define o valor usado quando a opção não é fornecida.

Leitura de arquivos

Para ler arquivos em Python, utilizamos a função open() que retorna um objeto de arquivo. É fundamental usar o gerenciador de contexto with, que garante que o arquivo seja fechado corretamente, mesmo se ocorrer uma exceção. Podemos ler o conteúdo inteiro com read(), ou linha por linha com readline() ou iterando sobre o objeto.

No nosso projeto, vamos ler o arquivo de texto e dividir em linhas. Também precisamos lidar com possíveis erros, como arquivo não encontrado. Vamos usar try/except para capturar FileNotFoundError e outras exceções.

def ler_arquivo(caminho):
    """Lê o conteúdo do arquivo e retorna uma lista de linhas."""
    try:
        with open(caminho, 'r', encoding='utf-8') as arquivo:
            linhas = arquivo.readlines()
        return linhas
    except FileNotFoundError:
        print(f'Erro: arquivo "{caminho}" não encontrado.')
        return None
    except Exception as e:
        print(f'Erro ao ler o arquivo: {e}')
        return None

Observe que usamos encoding='utf-8' para garantir a leitura correta de caracteres acentuados. A função retorna None em caso de erro, permitindo que o chamador decida como prosseguir.

Processamento

O processamento é o coração da ferramenta. Precisamos analisar o texto para extrair estatísticas. Vamos contar linhas (considerando linhas vazias ou não), palavras (definidas como sequências de caracteres alfanuméricos), caracteres (incluindo espaços e quebras de linha) e também a frequência das palavras, respeitando o comprimento mínimo.

Para isso, usaremos expressões regulares (re) para encontrar palavras. Vamos criar uma função processar_texto que recebe as linhas e o comprimento mínimo e retorna um dicionário com as estatísticas.

import re
from collections import Counter

def processar_texto(linhas, min_length=1):
    """Processa as linhas e retorna estatísticas."""
    texto = ''.join(linhas)
    num_linhas = len(linhas)
    num_caracteres = len(texto)

    # Encontra todas as palavras (sequências de letras, números e underscore)
    palavras = re.findall(r'\b\w+\b', texto, flags=re.UNICODE)
    # Filtra por comprimento mínimo
    palavras_filtradas = [p.lower() for p in palavras if len(p) >= min_length]
    num_palavras = len(palavras_filtradas)

    # Contagem de frequência
    frequencia = Counter(palavras_filtradas)

    return {
        'linhas': num_linhas,
        'palavras': num_palavras,
        'caracteres': num_caracteres,
        'frequencia': frequencia
    }

Usamos re.findall com o padrão \b\w+\b para capturar palavras completas. A função Counter da biblioteca collections facilita a contagem de frequência. Também convertemos todas as palavras para minúsculas para evitar diferenças de capitalização.

É importante notar que a definição de "palavra" pode variar. Aqui consideramos sequências de letras, números e underscore. Se quiséssemos apenas letras, usaríamos [a-zA-Zá-úÁ-Ú]+ com suporte a acentos, mas \w já cobre acentos em Python com o flag UNICODE.

Saída

A saída da nossa ferramenta deve ser clara e útil. Vamos imprimir um resumo no terminal e, se o usuário solicitar, a lista de palavras mais frequentes. Também oferecemos a opção de salvar o relatório em um arquivo.

Para exibir as palavras mais frequentes, usamos most_common(n) do Counter. Vamos permitir que o usuário especifique quantas palavras exibir através de uma opção --top (padrão 10).

def gerar_relatorio(estats, top_n=10):
    """Gera uma string com o relatório formatado."""
    linhas = [
        '=== Relatório de Análise ===',
        f'Arquivo: {args.arquivo}',
        f'Linhas: {estats["linhas"]}',
        f'Palavras: {estats["palavras"]}',
        f'Caracteres: {estats["caracteres"]}',
        ''
    ]
    if args.palavras:
        linhas.append(f'Palavras mais frequentes (mínimo {args.min_length} caracteres):')
        for palavra, contagem in estats['frequencia'].most_common(top_n):
            linhas.append(f'  {palavra}: {contagem}')
        linhas.append('')
    return '\n'.join(linhas)

def imprimir_relatorio(relatorio):
    print(relatorio)

def salvar_relatorio(relatorio, caminho):
    try:
        with open(caminho, 'w', encoding='utf-8') as arquivo:
            arquivo.write(relatorio)
        print(f'Relatório salvo em {caminho}')
    except Exception as e:
        print(f'Erro ao salvar relatório: {e}')

No main, integramos tudo: lemos o arquivo, processamos e geramos a saída. Se o usuário passou --saida, salvamos o relatório; caso contrário, imprimimos no terminal.

def main():
    parser = argparse.ArgumentParser(description='Analisa um arquivo de texto e mostra estatísticas.')
    parser.add_argument('arquivo', help='Caminho do arquivo de texto')
    parser.add_argument('--palavras', action='store_true', help='Lista as palavras mais frequentes')
    parser.add_argument('--min-length', type=int, default=1, help='Comprimento mínimo das palavras (padrão: 1)')
    parser.add_argument('--saida', help='Arquivo de saída para o relatório (opcional)')
    parser.add_argument('--top', type=int, default=10, help='Quantas palavras listar (padrão: 10)')

    args = parser.parse_args()

    linhas = ler_arquivo(args.arquivo)
    if linhas is None:
        return

    estats = processar_texto(linhas, args.min_length)
    relatorio = gerar_relatorio(estats, args.top)

    if args.saida:
        salvar_relatorio(relatorio, args.saida)
    else:
        imprimir_relatorio(relatorio)

if __name__ == '__main__':
    main()

Com isso, temos uma ferramenta funcional. Vamos testar com um arquivo de exemplo e opções variadas.

Boas práticas e observações finais

Ao construir ferramentas CLI, lembre-se de:

  • Sempre usar if __name__ == '__main__' para permitir que o módulo seja importado sem executar o main.
  • Tratar exceções de forma elegante, fornecendo mensagens claras ao usuário.
  • Documentar as opções com help para que a saída de --help seja informativa.
  • Considerar a codificação dos arquivos (UTF-8 é o padrão recomendado).
  • Testar com diferentes entradas, incluindo arquivos vazios, com acentos e com palavras de tamanhos variados.

Este projeto pode ser expandido com mais funcionalidades, como suporte a múltiplos arquivos, filtros por expressão regular, ou geração de gráficos. O importante é dominar os conceitos de argumentos, I/O e processamento de texto.

Referências

Exercícios

  1. Modifique a ferramenta para aceitar múltiplos arquivos como argumentos posicionais (nargs='+') e processar cada um separadamente, mostrando o nome do arquivo antes das estatísticas.

    ✓ Resposta:
    import argparse
    import re
    from collections import Counter
    
    def ler_arquivo(caminho):
        try:
            with open(caminho, 'r', encoding='utf-8') as f:
                return f.readlines()
        except Exception as e:
            print(f'Erro ao ler {caminho}: {e}')
            return None
    
    def processar_texto(linhas, min_length):
        texto = ''.join(linhas)
        palavras = re.findall(r'\b\w+\b', texto, flags=re.UNICODE)
        palavras_filtradas = [p.lower() for p in palavras if len(p) >= min_length]
        return {'linhas': len(linhas), 'palavras': len(palavras_filtradas), 'caracteres': len(texto), 'frequencia': Counter(palavras_filtradas)}
    
    def main():
        parser = argparse.ArgumentParser(description='Analisa arquivos de texto')
        parser.add_argument('arquivos', nargs='+', help='Caminhos dos arquivos')
        parser.add_argument('--min-length', type=int, default=1)
        parser.add_argument('--top', type=int, default=10)
        args = parser.parse_args()
    
        for arquivo in args.arquivos:
            linhas = ler_arquivo(arquivo)
            if linhas is None:
                continue
            estats = processar_texto(linhas, args.min_length)
            print(f'\n--- {arquivo} ---')
            print(f'Linhas: {estats["linhas"]}')
            print(f'Palavras: {estats["palavras"]}')
            print(f'Caracteres: {estats["caracteres"]}')
            if args.top > 0:
                print('Frequência:')
                for palavra, contagem in estats['frequencia'].most_common(args.top):
                    print(f'  {palavra}: {contagem}')
    
    if __name__ == '__main__':
        main()
  2. Adicione uma opção --ignorar-vazias que remove linhas vazias da contagem de linhas e do processamento de texto.

    ✓ Resposta:
    # No processar_texto, se ignorar_vazias for True, filtra linhas vazias antes de juntar:
    def processar_texto(linhas, min_length, ignorar_vazias=False):
        if ignorar_vazias:
            linhas = [l for l in linhas if l.strip()]
        texto = ''.join(linhas)
        # ... resto igual
  3. Crie uma função que, dado um texto, retorne a palavra mais longa e a mais curta (considerando o comprimento mínimo). Incorpore isso ao relatório.

    ✓ Resposta:
    def encontrar_extremos(palavras):
        if not palavras:
            return None, None
        mais_longa = max(palavras, key=len)
        mais_curta = min(palavras, key=len)
        return mais_longa, mais_curta
    
    # No processar_texto, após filtrar, chame esta função e adicione ao dicionário:
    palavras_filtradas = [p.lower() for p in palavras if len(p) >= min_length]
    mais_longa, mais_curta = encontrar_extremos(palavras_filtradas)
    # Adicione ao retorno: 'mais_longa': mais_longa, 'mais_curta': mais_curta
  4. Implemente uma opção --reverse que inverte a ordem das linhas no relatório (imprime de trás para frente).

    ✓ Resposta:
    # No main, após ler as linhas, se args.reverse, inverta a lista:
    if args.reverse:
        linhas = linhas[::-1]
    # Depois processe normalmente.
  5. Adicione tratamento para arquivos binários (por exemplo, extensão .bin) que exiba uma mensagem de erro e não processe.

    ✓ Resposta:
    # Na função ler_arquivo, verifique a extensão:
    import os
    if caminho.endswith('.bin'):
        print(f'Erro: arquivo binário não suportado: {caminho}')
        return None
    # Caso contrário, proceda normalmente.