Chega de Copiar Dados na Mão: Construa um Pipeline Automático de PDFs e Relatórios por E-mail com Python


Chega de Copiar Dados na Mão: Construa um Pipeline Automático de PDFs e Relatórios por E-mail com Python

Introdução do Artigo

Você já passou horas copiando dados manualmente de notas fiscais, boletos ou relatórios em PDF para planilhas do Excel, apenas para depois ter que formatar e enviar tudo por e-mail? Essa tarefa repetitiva consome um tempo valioso e aumenta exponencialmente o risco de erros humanos. A boa notícia é que o Python oferece uma solução poderosa e 100% automatizada para esse problema.

Combinando bibliotecas especializadas como pypdf (para leitura, fusão e divisão de documentos), pdfplumber (para extração precisa de tabelas estruturadas) e o módulo nativo smtplib (para o envio programático de e-mails com anexos), você pode transformar um fluxo de trabalho manual demorado em um script automático executado em questão de segundos.

Neste artigo prático, você aprenderá o passo a passo completo para ler PDFs, extrair dados em lote, tratar documentos escaneados e agendar o envio automático dos seus relatórios diretamente para a sua caixa de entrada.


Índice do Artigo (Sumário)

  1. Por que Automatizar a Leitura de PDFs e o Envio de Relatórios?

    • Os impactos da digitação manual no tempo e na taxa de erros.
    • Como o Python reduz custos e simplifica rotinas administrativas.
  2. Escolhendo as Ferramentas Certas: pypdf vs. pdfplumber

    • pypdf: leitura simples de texto, metadados, fusão e divisão de arquivos.
    • pdfplumber: extração avançada de tabelas estruturadas e preservação de layout.
    • Onde entra o PyMuPDF e quando utilizá-lo.
  3. Extraindo Texto e Tabelas Estruturadas de Arquivos PDF

    • Passo a passo para ler páginas e salvar dados em DataFrames (pandas) ou CSV.
    • Otimizando estratégias de delimitadores (vertical_strategy e horizontal_strategy) para tabelas sem bordas.
    • O que fazer quando o PDF é uma imagem escaneada: aplicando OCR com Tesseract.
  4. Manipulação Estrutural: Mesclando e Dividindo Documentos

    • Como agrupar múltiplos relatórios (como DANFEs ou boletos) em um único arquivo.
    • Dividindo PDFs extensos em arquivos ou blocos de páginas individuais.
  5. Envio Automático de E-mails com Anexos Usando smtplib e email

    • Montando a estrutura da mensagem com suporte a HTML e anexos (MIMEMultipart, MIMEText).
    • Conectando com segurança ao servidor SMTP (TLS/SSL nas portas 587 ou 465).
  6. Agendamento e Boas Práticas em Produção

    • Execução automática diária com Task Scheduler (Windows) ou cron (Linux/Mac).
    • Segurança de credenciais: proteja senhas e tokens com variáveis de ambiente (os.getenv).
    • Tratamento de exceções e registros de logs para um pipeline à prova de falhas.
  7. Conclusão e Próximos Passos

    • Resumo do pipeline completo e ideias de projetos práticos para aplicar hoje.

1. Por que Automatizar a Leitura de PDFs e o Envio de Relatórios?

A rotina diária de escritórios, contadores e analistas de dados é frequentemente sobrecarregada pelo processamento manual de documentos em PDF, como notas fiscais, boletos bancários, contratos e relatórios operacionais. O trabalho repetitivo de extrair informações dessas páginas e digitá-las linha por linha em planilhas ou sistemas consome horas valiosas todas as semanas e introduz uma taxa considerável de falhas decorrentes do cansaço humano.

A automação com Python transforma essa realidade ao substituir processos manuais demorados por rotinas programadas capazes de ler, filtrar e consolidar dados em poucos minutos. Além de eliminar erros de digitação e garantir consistência na informação, a criação de pipelines automatizados permite conectar a extração do conteúdo diretamente com o envio de e-mails, assegurando que os destinatários recebam relatórios e anexos pontualmente.

Ao transferir o fluxo de leitura, tratamento e envio para scripts que rodam em segundo plano, as empresas reduzem o tempo gasto em tarefas operacionais e liberam os profissionais para focarem em análises estratégicas e tomada de decisões.


Os Impactos da Digitação Manual no Tempo e na Taxa de Erros

A transcrição manual de dados a partir de relatórios e faturas em formato PDF representa um dos maiores gargalos operacionais e financeiros nas empresas. O fluxo de trabalho tradicional exige que profissionais abram arquivo por arquivo, analisem visualmente o layout do documento, localizem campos como CNPJ, valores e datas, e façam a digitação linha por linha em planilhas do Excel ou sistemas de gestão.

Esse processo analógico gera dois grandes impactos negativos:

  1. Gargalo de Tempo e Produtividade: Um profissional gasta em média de 4 a 5 minutos para copiar e revisar os dados de um único PDF. Em rotinas com dezenas de documentos diários, uma equipe pode consumir facilmente mais de 15 horas por semana apenas em entrada manual de dados.
  2. Alta Taxa de Erros: Devido à fadiga e à repetição constante da tarefa, a taxa de erro humano na digitação manual costuma girar em torno de 8%. Pequenos equívocos — como inverter numerações, errar a vírgula decimal ou esquecer um item — geram inconsistências contábeis e exigem horas adicionais de auditoria e correção.

Com a criação de scripts de automação em Python, a leitura de centenas de arquivos passa a ser feita em questão de segundos. Além de liberar os funcionários para tarefas de maior valor agregado, a extração programática reduz a taxa de falhas para menos de 2%, garantindo a integridade do banco de dados e eliminando os custos decorrentes de erros humanos.


Exemplo de Código: Comparando a Velocidade da Automação com a Digitação Manual

O script a seguir demonstra como usar a biblioteca pdfplumber para processar uma pasta cheia de PDFs. Ele realiza a leitura automática dos documentos e calcula a diferença de tempo entre a execução do Python e o tempo que um ser humano levaria para digitar o mesmo volume de dados:

import os
import time
import pdfplumber
import pandas as pd

# Configurações do ambiente
PASTA_PDFS = "./documentos"
TEMPO_MEDIO_DIGITACAO_MANUAL_SEG = 270  # Estimativa de 4.5 minutos por PDF

dados_extraidos = []
inicio_execucao = time.time()

# Lista todos os arquivos PDF da pasta
arquivos_pdf = [f for f in os.listdir(PASTA_PDFS) if f.lower().endswith(".pdf")]

# Processamento automatizado
for arquivo in arquivos_pdf:
    caminho_completo = os.path.join(PASTA_PDFS, arquivo)

    with pdfplumber.open(caminho_completo) as pdf:
        for numero_pagina, pagina in enumerate(pdf.pages, start=1):
            texto = pagina.extract_text()
            if texto:
                dados_extraidos.append({
                    "arquivo": arquivo,
                    "pagina": numero_pagina,
                    "caracteres_lidos": len(texto)
                })

# Métricas de tempo
tempo_total_python = time.time() - inicio_execucao
total_documentos = len(arquivos_pdf)
tempo_estimado_manual_min = (total_documentos * TEMPO_MEDIO_DIGITACAO_MANUAL_SEG) / 60

# Exibição do relatório no console
print("--- RELATÓRIO DE PRODUTIVIDADE E IMPACTO ---")
print(f"Documentos processados pelo Python: {total_documentos}")
print(f"Tempo de execução do script: {tempo_total_python:.2f} segundos")
print(f"Tempo estimado de digitação manual: {tempo_estimado_manual_min:.1f} minutos ({tempo_estimado_manual_min/60:.1f} horas)")

if tempo_total_python > 0:
    eficiencia = (tempo_estimado_manual_min * 60) / tempo_total_python
    print(f"O Python executou a tarefa aproximadamente {eficiencia:.0f}x mais rápido que um humano.")

Como o Python Reduz Custos e Simplifica Rotinas Administrativas

A adoção do Python na automação de rotinas administrativas não apenas acelera o trabalho diário, mas gera um impacto direto e mensurável na redução de custos operacionais. Em vez de depender de licenças caros de softwares proprietários de captura ou serviços pagos de OCR e IA, o ecossistema Python disponibiliza bibliotecas 100% gratuitas e de código aberto (como pypdf, pdfplumber, pandas e smtplib).

Essa abordagem traz vantagens estratégicas fundamentais:

  • Economia Direta de Licenciamento: Ferramentas abertas permitem construir pipelines completos de extração de dados e geração de relatórios sem custos adicionais de software por usuário ou por documento processado.
  • Simplificação e Consolidação de Processos: Um único script Python consegue ler arquivos em uma pasta, filtrar informações relevantes, consolidar os dados em uma planilha limpa e enviar os resultados por e-mail.
  • Escalabilidade Sem Aumento de Equipe: O mesmo script que processa 5 notas fiscais ou relatórios diários consegue processar 500 sem exigir contratação de mão de obra adicional ou horas extras.
  • Realocação do Talento Humano: Ao eliminar a digitação repetitiva, a equipe passa a focar no atendimento a clientes, auditorias estratégicas e análise de indicadores.

Exemplo de Código: Pipeline Completo de Consolidação e Automação

O exemplo abaixo ilustra como o Python simplifica a rotina: o script varre uma pasta contendo múltiplos PDFs de relatórios/faturas, consolida todos os valores em uma planilha limpa usando pandas e gera um resumo pronto para ser distribuído:

import os
import pdfplumber
import pandas as pd

# 1. Configuração de diretórios
PASTA_ENTRADA = "./relatorios_recebidos"
ARQUIVO_SAIDA = "relatorio_consolidado.xlsx"

dados_consolidados = []

# 2. Leitura e extração estruturada em lote
if os.path.exists(PASTA_ENTRADA):
    for arquivo in os.listdir(PASTA_ENTRADA):
        if arquivo.lower().endswith(".pdf"):
            caminho_pdf = os.path.join(PASTA_ENTRADA, arquivo)

            with pdfplumber.open(caminho_pdf) as pdf:
                for pagina in pdf.pages:
                    tabelas = pagina.extract_tables()
                    for tabela in tabelas:
                        # Descarta o cabeçalho e adiciona os dados
                        for linha in tabela[1:]:
                            if any(linha):  # ignora linhas totalmente vazias
                                dados_consolidados.append({
                                    "Arquivo_Origem": arquivo,
                                    "Item": linha if len(linha) > 0 else "N/A",
                                    "Valor": linha[-1] if len(linha) > 1 else "0"
                                })

# 3. Tratamento e limpeza com Pandas
df = pd.DataFrame(dados_consolidados)

if not df.empty:
    # Tratamento de valores numéricos
    df["Valor_Limpo"] = (
        df["Valor"]
        .astype(str)
        .str.replace("R$", "", regex=False)
        .str.replace(".", "", regex=False)
        .str.replace(",", ".", regex=False)
        .str.strip()
    )
    df["Valor_Limpo"] = pd.to_numeric(df["Valor_Limpo"], errors="coerce").fillna(0)

    # Exportação automática para Excel
    df.to_excel(ARQUIVO_SAIDA, index=False)

    print(f"Sucesso! {len(df)} registros consolidados em '{ARQUIVO_SAIDA}'.")
    print(f"Valor total processado: R$ {df['Valor_Limpo'].sum():,.2f}")
else:
    print("Nenhum dado tabular foi encontrado nos PDFs fornecidos.")

2. Escolhendo as Ferramentas Certas: pypdf vs. pdfplumber

O ecossistema Python disponibiliza bibliotecas consolidadas para o processamento de documentos em PDF. No entanto, a escolha da ferramenta ideal depende do objetivo do seu projeto: se a prioridade é a manipulação da estrutura do arquivo ou a extração detalhada de dados e tabelas. Entender as características de cada biblioteca permite construir um fluxo de trabalho mais rápido, robusto e sem retrabalho.

A biblioteca pypdf (evolução do antigo PyPDF2) é uma solução leve e desenvolvida em Python puro, dispensando dependências de sistema. Ela é excelente para operações estruturais de nível de documento, como leitura de metadados, rotação de páginas, divisão de arquivos extensos e mesclagem de múltiplos PDFs em lote. Embora seja muito eficiente para extrair texto corrido e simples, o pypdf não foi projetado para interpretar a disposição geométrica de tabelas complexas ou colunas alinhadas visualmente.

Em contrapartida, o pdfplumber brilha na análise de layout e extração de conteúdo estruturado. Ele inspeciona a página identificando a posição exata (coordenadas X e Y) de cada caractere, linha e elemento gráfico. Essa precisão torna o pdfplumber a melhor escolha para capturar tabelas de notas fiscais, boletos e demonstrativos financeiros, transformando dados bloqueados no PDF em planilhas limpas ou DataFrames do pandas. Além disso, quando o volume de documentos é extremamente alto e exige velocidade máxima de execução, bibliotecas como o PyMuPDF (fitz) surgem como alternativas complementares de alto desempenho.


pypdf: Leitura Simples de Texto, Metadados, Fusão e Divisão de Arquivos

A biblioteca pypdf (sucessora oficial do antigo PyPDF2) é uma ferramenta desenvolvida em Python puro, perfeita para manipulação estrutural e leitura direta de documentos em PDF. Por não exigir dependências do sistema operacional ou compiladores nativos, ela é leve, rápida e extremamente fácil de integrar em scripts de automação de escritório.

As principais operações que você pode realizar com o pypdf incluem:

  • Leitura de Texto: Através da classe PdfReader, você abre o documento e navega pela lista reader.pages, aplicando o método extract_text() em cada página desejada.
  • Inspeção de Metadados: O atributo reader.metadata expõe informações como autor (/Author), título (/Title) e data de criação (/CreationDate), o que permite identificar e organizar arquivos recebidos em lotes.
  • Fusão (Juntar PDFs): Utilizando a classe PdfWriter, você combina múltiplos arquivos chamando writer.append("arquivo.pdf") para cada documento e grava o resultado final no disco com writer.write().
  • Divisão e Extração de Páginas: Para separar capítulos ou extrair trechos específicos de um relatório, você pode copiar páginas selecionadas para um novo PdfWriter via writer.add_page() ou repassar o intervalo de páginas diretamente no parâmetro pages=(início, fim).

Exemplo de Código: Lendo Texto, Metadados, Mesclando e Dividindo com pypdf

from pypdf import PdfReader, PdfWriter

# 1. LEITURA DE TEXTO E METADADOS
arquivo_original = "relatorio_empresa.pdf"
reader = PdfReader(arquivo_original)

print(f"Total de páginas: {len(reader.pages)}")

# Extraindo metadados do documento
meta = reader.metadata
if meta:
    print("--- Metadados do PDF ---")
    print(f"Título: {meta.get('/Title')}")
    print(f"Autor: {meta.get('/Author')}")
    print(f"Data de Criação: {meta.get('/CreationDate')}")

# Lendo o texto da primeira página (índice 0)
primeira_pagina_texto = reader.pages.extract_text()
print("\n--- Texto da Página 1 ---")
print(primeira_pagina_texto[:300])  # Exibe os primeiros 300 caracteres

# 2. FUSÃO (JUNTAR MÚLTIPLOS PDFS EM UM SÓ)
writer_fusao = PdfWriter()

# Anexando múltiplos arquivos em sequência
for pdf_file in ["relatorio_empresa.pdf", "anexo_financeiro.pdf"]:
    writer_fusao.append(pdf_file)

# Salvando o arquivo combinado
with open("relatorio_completo_consolidado.pdf", "wb") as f_out:
    writer_fusao.write(f_out)

print("\nArquivos mesclados com sucesso em 'relatorio_completo_consolidado.pdf'!")

# 3. DIVISÃO (EXTRAIR PÁGINAS ESPECÍFICAS DE UM PDF)
writer_divisao = PdfWriter()

# Opção A: Adicionando páginas específicas uma a uma (ex: páginas 1 e 2)
writer_divisao.add_page(reader.pages)
writer_divisao.add_page(reader.pages)

# Opção B (Alternativa): Anexando um intervalo de páginas (páginas de índice 0 a 2)
# writer_divisao.append(arquivo_original, pages=(0, 2))

# Salvando as páginas extraídas em um novo arquivo
with open("resumo_executivo.pdf", "wb") as f_divisao:
    writer_divisao.write(f_divisao)

print("Páginas selecionadas salvas em 'resumo_executivo.pdf'!")

pdfplumber: Extração Avançada de Tabelas Estruturadas e Preservação de Layout

Enquanto leitores básicos de PDF tratam a página apenas como um fluxo de texto corrido, o pdfplumber analisa a geometria do documento, mapeando a posição exata (coordenadas X e Y) de cada caractere, palavra e linha gráfica. Essa capacidade de leitura espacial o torna a ferramenta ideal para capturar tabelas complexas de notas fiscais, boletos e relatórios financeiros, preservando o alinhamento de colunas mesmo em layouts desafiadores.

Os recursos fundamentais do pdfplumber para manipulação de tabelas incluem:

  • Métodos de Captura: O método extract_table() localiza a maior tabela da página, enquanto extract_tables() identifica e retorna todas as estruturas tabulares existentes na página como listas de listas.
  • Ajuste de Estratégias (table_settings): Para tabelas sem bordas visíveis ou delimitadas apenas por espaços em branco, você pode alternar os parâmetros vertical_strategy e horizontal_strategy de "lines" (padrão baseado em linhas desenhadas) para "text" (baseado no alinhamento das palavras).
  • Recorte de Área (crop): Se a página contiver cabeçalhos ou rodapés que interferem na extração, é possível isolar apenas a região da tabela usando uma caixa delimitadora (bounding box) com pagina.crop((x0, top, x1, bottom)) antes da leitura.
  • Integração Nativa com Pandas: A estrutura em lista de listas devolvida pelo pdfplumber pode ser convertida diretamente em um DataFrame do pandas, onde a primeira linha atua como os cabeçalhos das colunas.

Exemplo de Código: Extração de Tabelas com pdfplumber e Conversão para Pandas

O script a seguir abre um relatório em PDF, ajusta os parâmetros de extração para tratar tabelas com ou sem bordas visíveis, limpa valores vazios e exporta os dados estruturados para uma planilha Excel:

import pdfplumber
import pandas as pd

caminho_pdf = "relatorio_financeiro.pdf"
dados_tabela = []

# Configurações personalizadas para detecção de tabelas
config_tabela = {
    "vertical_strategy": "text",   # Usa o alinhamento do texto quando não há linhas verticais
    "horizontal_strategy": "lines", # Usa as linhas desenhadas na horizontal
    "snap_tolerance": 3,
}

with pdfplumber.open(caminho_pdf) as pdf:
    for numero_pagina, pagina in enumerate(pdf.pages, start=1):
        # Tenta extrair a tabela da página atual
        tabela = pagina.extract_table(table_settings=config_tabela)

        # Fallback caso a primeira estratégia não encontre nada
        if not tabela:
            tabela = pagina.extract_table()

        if tabela:
            # Na primeira página capturada, guardamos o cabeçalho
            if not dados_tabela:
                cabecalho = [str(col).strip().replace("\n", " ") for col in tabela if col is not None]
                linhas = tabela[1:]
            else:
                # Nas páginas seguintes, descartamos o cabeçalho repetido
                linhas = tabela[1:] if tabela == tabela else tabela

            for linha in linhas:
                # Limpa quebras de linha e espaços extras das células
                linha_limpa = [str(celula).strip().replace("\n", " ") if celula else "" for celula in linha]
                if any(linha_limpa):  # Ignora linhas totalmente vazias
                    dados_tabela.append(linha_limpa)

# Conversão para DataFrame do Pandas
if dados_tabela and cabecalho:
    df = pd.DataFrame(dados_tabela, columns=cabecalho)

    # Exibe as primeiras linhas e salva em Excel/CSV
    print("--- DADOS EXTRAÍDOS COM SUCESSO ---")
    print(df.head())

    df.to_excel("tabela_extraida.xlsx", index=False)
    print("\nTabela salva em 'tabela_extraida.xlsx'.")
else:
    print("Nenhuma tabela estruturada foi encontrada no documento.")

Onde Entra o PyMuPDF e Quando Utilizá-lo

Enquanto o pypdf foca em manipulação de arquivos e o pdfplumber se destaca na extração geométrica de tabelas, o PyMuPDF (módulo importado no Python como fitz) é a solução voltada para alto desempenho e versatilidade extrema. Desenvolvido como uma interface em Python para o motor nativo em C MuPDF, ele é significativamente mais rápido do que as bibliotecas escritas em Python puro.

O PyMuPDF deve ser a sua escolha principal nos seguintes cenários:

  • Processamento de Grandes Volumes de Dados: Se a sua aplicação precisa varrer e ler milhares de arquivos PDF em lote, a velocidade do PyMuPDF reduz minutos de execução para segundos.
  • Renderização de Páginas para Imagens: Quando há a necessidade de converter páginas de um PDF em imagens (PNG, JPEG) de alta qualidade — seja para exibição em sistemas web ou para preparar arquivos para OCR —, o PyMuPDF faz essa conversão nativamente com excelente resolução.
  • Modificação e Edição de Conteúdo: Diferente de leitores que se limitam a extrair dados, o PyMuPDF permite inserir textos, adicionar anotações e editar elementos diretamente nas páginas.
  • Atenção ao Licenciamento Comercial: Um ponto importante para desenvolvedores é que o PyMuPDF adota a licença AGPL-3.0 (com opção de licença comercial), o que exige cuidados ao distribuí-lo dentro de softwares proprietários ou fechados.

Exemplo de Código: Leitura de Alta Velocidade e Renderização de Imagem com PyMuPDF

import fitz  # PyMuPDF é instalado via `pip install pymupdf` e importado como `fitz`

caminho_pdf = "relatorio_extenso.pdf"

# 1. Abertura do documento
doc = fitz.open(caminho_pdf)
print(f"Total de páginas: {len(doc)}")

# Lendo metadados
print("Título:", doc.metadata.get("title"))
print("Autor:", doc.metadata.get("author"))

# 2. Extração rápida de texto acumulado
texto_completo = []
for pagina in doc:
    texto_completo.append(pagina.get_text())

total_caracteres = sum(len(t) for t in texto_completo)
print(f"Extraídos {total_caracteres} caracteres com sucesso!")

# 3. Renderizando a primeira página em uma imagem PNG de alta resolução
primeira_pagina = doc

# Matriz de escala 2.0x aumenta a resolução para a imagem ficar nítida
matriz_escala = fitz.Matrix(2.0, 2.0)
pixmap = primeira_pagina.get_pixmap(matrix=matriz_escala)

# Salva a renderização em arquivo gráfico
caminho_imagem = "pagina_1.png"
pixmap.save(caminho_imagem)
print(f"Primeira página convertida e salva em '{caminho_imagem}'.")

doc.close()

3. Extraindo Texto e Tabelas Estruturadas de Arquivos PDF

Arquivos em formato PDF representam o padrão dominante para a troca de documentos no ambiente corporativo, abrangendo desde notas fiscais e boletos até relatórios contábeis e demonstrativos financeiros. Embora o PDF garanta que a formatação visual permaneça idêntica em qualquer dispositivo, essa mesma rigidez aprisiona dados cruciais em uma estrutura gráfica que não foi criada para ser lida diretamente por sistemas de banco de dados.

A automação com Python resolve esse gargalo ao transformar documentos estáticos em DataFrames limpos da biblioteca pandas e planilhas prontas para análise. Ao inspecionar as coordenadas espaciais e os delimitadores da página, é possível extrair textos corridos, separar linhas de tabelas e processar pastas inteiras de relatórios em lote, reduzindo o tempo de ingestão de dados de horas para poucos segundos.

Neste capítulo, abordaremos o passo a passo para construir um pipeline de extração consistente, lidando com a limpeza de caracteres, a configuração de réguas verticais para tabelas sem bordas e o tratamento de documentos escaneados que exigem OCR.


Passo a Passo para Ler Páginas e Salvar Dados em DataFrames (pandas) ou CSV

A conversão de tabelas presas em PDFs para formatos manipuláveis exige uma sequência estruturada de etapas para garantir que o alinhamento das colunas seja mantido e que os dados fiquem prontos para análise. Utilizando a biblioteca pdfplumber em conjunto com o pandas, você pode construir um pipeline direto e escalável.


O Passo a Passo do Pipeline

  1. Abertura Segura do Documento:
    Utilize o gerenciador de contexto with pdfplumber.open(...) as pdf para abrir o arquivo. Isso garante que o documento seja liberado da memória automaticamente após a leitura.

  2. Extração das Tabelas:
    Acesse a página desejada (pdf.pages[i]) e chame o método extract_tables() para capturar todas as tabelas ou extract_table() para a tabela principal. O pdfplumber interpreta a geometria da página e retorna os registros organizados como uma lista de listas.

  3. Separação de Cabeçalhos e Limpeza das Linhas:
    Geralmente, o primeiro subitem da lista extraída contém o nome das colunas. Separe a primeira linha para usar como cabeçalho e aplique uma sanitização rápida nas demais linhas para remover quebras de linha (\n) ou células nulas.

  4. Criação do DataFrame e Tratamento de Tipos:
    Passe a lista de dados limpos para a classe pd.DataFrame(dados, columns=cabecalho). Em seguida, converta colunas numéricas ou de datas para seus tipos adequados (como float ou datetime), corrigindo vírgulas decimais.

  5. Exportação para Arquivo Final:
    Com o DataFrame estruturado, salve os dados no formato desejado usando df.to_csv("saida.csv", index=False) ou df.to_excel("saida.xlsx", index=False).


Exemplo de Código Completo

O script abaixo exemplifica esse processo do início ao fim, lendo todas as páginas de um relatório PDF e gerando um arquivo CSV e outro Excel:

import pdfplumber
import pandas as pd

caminho_pdf = "relatorio_vendas.pdf"
dados_totais = []
cabecalho = None

# 1. Abertura segura com gerenciador de contexto
with pdfplumber.open(caminho_pdf) as pdf:
    for i, pagina in enumerate(pdf.pages):
        # 2. Extração da tabela da página atual
        tabela = pagina.extract_table()

        if tabela:
            # 3. Identificação do cabeçalho na primeira página
            if i == 0:
                cabecalho = [str(col).strip().replace("\n", " ") for col in tabela if col]
                linhas = tabela[1:]
            else:
                # Nas páginas seguintes, desconsidera o cabeçalho repetido
                linhas = tabela[1:] if tabela == cabecalho else tabela

            # Limpeza rápida de quebras de linha e células vazias
            for linha in linhas:
                linha_limpa = [str(celula).strip().replace("\n", " ") if celula is not None else "" for celula in linha]
                if any(linha_limpa):
                    dados_totais.append(linha_limpa)

# 4. Criação e tratamento do DataFrame com Pandas
if dados_totais and cabecalho:
    df = pd.DataFrame(dados_totais, columns=cabecalho)

    # Exemplo de tratamento simples em colunas numéricas (se houver a coluna "Valor")
    if "Valor" in df.columns:
        df["Valor"] = (
            df["Valor"]
            .str.replace("R$", "", regex=False)
            .str.replace(".", "", regex=False)
            .str.replace(",", ".", regex=False)
            .str.strip()
        )
        df["Valor"] = pd.to_numeric(df["Valor"], errors="coerce")

    # 5. Exportação dos dados
    df.to_csv("dados_extraidos.csv", index=False, encoding="utf-8-sig", sep=";")
    df.to_excel("dados_extraidos.xlsx", index=False)

    print("Processamento concluído com sucesso!")
    print(df.info())
else:
    print("Nenhuma tabela estruturada foi encontrada no PDF.")

Otimizando Estratégias de Delimitadores (vertical_strategy e horizontal_strategy) para Tabelas Sem Bordas

O comportamento padrão do pdfplumber para detectar tabelas consiste em buscar por linhas e bordas desenhadas na página. No entanto, relatórios corporativos, extratos bancários e boletos frequentemente apresentam tabelas “invisíveis” ou sem bordas laterais, nas quais as colunas são separadas apenas por espaços em branco ou alinhamentos de texto. Nessas situações, o algoritmo padrão falha e devolve valores nulos.

Para resolver esse problema, o pdfplumber permite ajustar o dicionário de configurações table_settings. Os dois parâmetros fundamentais dessa customização são a vertical_strategy (delimitação de colunas) e a horizontal_strategy (delimitação de linhas):

  • "lines" (Padrão): Procura por linhas físicas e vetoriais desenhadas na página. Funciona perfeitamente em tabelas com grade completa.
  • "text": Ignora a ausência de linhas e infere as divisões de colunas e linhas a partir do alinhamento espacial e do espaçamento entre as palavras. É a opção recomendada para tabelas sem bordas.
  • "explicit": Permite definir manualmente as posições exatas (coordenadas X ou Y) das divisórias.
  • Estratégias Mistas: Em documentos que possuem linhas horizontais dividindo os registros, mas nenhuma linha vertical entre as colunas, você pode mesclar os parâmetros configurando horizontal_strategy: "lines" e vertical_strategy: "text".

Além das estratégias de delimitadores, é possível calibrar o parâmetro snap_tolerance (tolerância em pixels para unir pontos de linhas próximas) e utilizar estratégias de fallback para garantir que o script tente ler com bordas e, caso falhe, tente a leitura por texto.


Exemplo de Código: Extração de Tabela Sem Bordas com table_settings e Fallback

import pdfplumber
import pandas as pd

caminho_pdf = "relatorio_sem_bordas.pdf"
dados_finais = []

# Configuração otimizada para tabelas sem linhas verticais/gradientes
config_sem_bordas = {
    "vertical_strategy": "text",      # Infere colunas pelo alinhamento do texto
    "horizontal_strategy": "text",    # Infere linhas pelo alinhamento do texto
    "snap_tolerance": 3,              # Junta elementos próximos com tolerância de 3px
    "join_tolerance": 3,
}

with pdfplumber.open(caminho_pdf) as pdf:
    for numero_pagina, pagina in enumerate(pdf.pages, start=1):
        # 1. Tenta extrair com a estratégia padrão (linhas desenhadas)
        tabela = pagina.extract_table({
            "vertical_strategy": "lines",
            "horizontal_strategy": "lines",
            "snap_tolerance": 3,
        })

        # 2. Se não encontrar nada (tabela sem bordas), aplica a estratégia baseada em texto
        if not tabela:
            tabela = pagina.extract_table(config_sem_bordas)

        if tabela:
            # Processa e limpa as células da tabela extraída
            for linha in tabela:
                linha_limpa = [str(celula).strip().replace("\n", " ") if celula else "" for celula in linha]
                if any(linha_limpa):
                    dados_finais.append(linha_limpa)

# Conversão para DataFrame do Pandas
if dados_finais:
    cabecalho = dados_finais
    df = pd.DataFrame(dados_finais[1:], columns=cabecalho)

    print("--- TABELA EXTRAÍDA COM SUCESSO ---")
    print(df.head())

    # Salva o resultado limpo em CSV
    df.to_csv("resultado_sem_bordas.csv", index=False, sep=";")
else:
    print("Não foi possível identificar estruturas tabulares na página.")

O que Fazer Quando o PDF é uma Imagem Escaneada: Aplicando OCR com Tesseract

Ao processar relatórios, faturas ou recibos digitalizados no scanner do escritório, é comum encontrar arquivos em que o método extract_text() do pypdf ou do pdfplumber retorna apenas uma string vazia (''). Isso acontece porque o PDF não possui uma camada de texto digital selecionável — ele é, na verdade, uma foto ou imagem estática de um documento.

Para recuperar o texto preso nesses arquivos, é necessário utilizar uma técnica de OCR (Optical Character Recognition ou Reconhecimento Óptico de Caracteres). No ecossistema Python, a combinação mais eficiente e utilizada para essa tarefa é:

  • pdf2image: Biblioteca responsável por converter as páginas do documento PDF em arquivos de imagem (utiliza o motor Poppler do sistema).
  • pytesseract: Módulo Python que se conecta diretamente ao mecanismo de reconhecimento de texto Tesseract OCR.

Nota de Configuração: O Tesseract OCR e o Poppler são programas executáveis do sistema operacional e precisam estar instalados no seu computador (Linux, Windows ou Mac) para que o script funcione. É fundamental instalar também o pacote de idioma em português (por) do Tesseract para garantir a leitura correta de palavras acentuadas e cedilhas.


Exemplo de Código: Detecção Automática e Extração via OCR

O exemplo a seguir verifica se o PDF possui texto vetorial selecionável; caso não possua, ele ativa o pipeline de conversão para imagens e leitura via Tesseract:

import os
from pdf2image import convert_from_path
import pytesseract
from pypdf import PdfReader

caminho_pdf = "documento_escaneado.pdf"

# 1. Teste inicial: Verifica se existe texto digital na primeira página
reader = PdfReader(caminho_pdf)
texto_inicial = reader.pages.extract_text()

if texto_inicial and texto_inicial.strip():
    print("Texto digital encontrado diretamente no PDF!")
    print(texto_inicial[:300])
else:
    print("Nenhum texto selecionável detectado. Iniciando OCR com Tesseract...")

    # 2. Converte as páginas do PDF em imagens (300 DPI garante ótima resolução para leitura)
    imagens_paginas = convert_from_path(caminho_pdf, dpi=300)

    paginas_extraidas = []

    # 3. Executa o OCR em cada imagem
    for numero_pagina, imagem in enumerate(imagens_paginas, start=1):
        # 'lang="por"' habilita o suporte ao idioma português
        texto_reconhecido = pytesseract.image_to_string(imagem, lang="por")

        if texto_reconhecido.strip():
            paginas_extraidas.append(f"--- Página {numero_pagina} ---\n{texto_reconhecido.strip()}")
            print(f"Página {numero_pagina} lida com sucesso.")
        else:
            print(f"Não foi possível extrair caracteres da Página {numero_pagina}.")

    # 4. Resultado final consolidado
    texto_final = "\n\n".join(paginas_extraidas)
    print("\n--- TEXTO EXTRAÍDO VIA OCR ---")
    print(texto_final[:500])

4. Manipulação Estrutural: Mesclando e Dividindo Documentos

Além de extrair texto e dados de tabelas, uma das rotinas mais comuns no gerenciamento de arquivos digitais envolve a organização estrutural de documentos em formato PDF. É muito frequente receber dezenas de faturas, recibos ou relatórios mensais separados que precisam ser consolidados em um único arquivo para envio à contabilidade. Do mesmo modo, contratos extensos ou relatórios corporativos muitas vezes precisam ser divididos em blocos menores ou ter capítulos específicos isolados para análise.

No Python, essa manipulação de nível de arquivo é realizada de forma leve e rápida através da biblioteca pypdf. Utilizando as classes PdfReader (para leitura) e PdfWriter (para criação, combinação e gravação), é possível anexar múltiplos documentos em sequência, recortar intervalos específicos de páginas ou quebrar um PDF extenso em arquivos individuais de uma única página sem precisar de softwares pagos.

Nesta seção, você entenderá como implementar esses fluxos de fusão e divisão programática em poucas linhas de código, eliminando o trabalho manual de organização de arquivos no seu dia a dia.


Como Agrupar Múltiplos Relatórios (como DANFEs ou Boletos) em um Único Arquivo

No dia a dia administrativo, é muito comum receber diversos arquivos PDF separados — como boletos bancários, notas fiscais (DANFEs) ou relatórios quinzenais — e precisar agrupá-los em um único documento para envio ao setor financeiro ou contábil. A biblioteca pypdf oferece uma solução simples e rápida para essa tarefa por meio da classe PdfWriter.

Com o método append(), o script lê os arquivos e anexa todas as suas páginas sequencialmente ao final do documento consolidado. Para automatizar a fusão de dezenas ou centenas de documentos de uma só vez, basta utilizar o módulo pathlib para localizar todos os PDFs de uma pasta e processá-los automaticamente em ordem alfabética.


Exemplo de Código: Mesclando Múltiplos PDFs em Lote

O script a seguir percorre uma pasta contendo relatórios individuais, faz a leitura de cada documento e gera um arquivo único consolidado:

from pathlib import Path
from pypdf import PdfWriter

# 1. Configuração do diretório de entrada e arquivo de saída
pasta_relatorios = Path("./relatorios_separados")
arquivo_saida = "relatorio_mesclado_final.pdf"

writer = PdfWriter()

# 2. Busca e ordenação alfabética dos arquivos PDF
arquivos_pdf = sorted(pasta_relatorios.glob("*.pdf"))

if arquivos_pdf:
    print(f"Encontrados {len(arquivos_pdf)} arquivos PDF para agrupamento.")

    # 3. Anexa cada arquivo sequencialmente ao consolidado
    for arquivo in arquivos_pdf:
        writer.append(arquivo)
        print(f"Anexado: {arquivo.name}")

    # 4. Gravação do arquivo consolidado no disco
    writer.write(arquivo_saida)
    print(f"\nSucesso! Todos os relatórios foram agrupados em '{arquivo_saida}'.")
    print(f"Total de páginas no documento final: {len(writer.pages)}")
else:
    print("Nenhum arquivo PDF foi encontrado no diretório especificado.")

Dividindo PDFs Extensos em Arquivos ou Blocos de Páginas Individuais

Quando trabalhamos com relatórios corporativos extensos, manuais ou volumes de documentos consolidados, frequentemente surge a necessidade de fracionar o arquivo original. Essa operação é útil para distribuir seções específicas para equipes distintas, respeitar limites de tamanho de anexo em sistemas web ou otimizar o armazenamento.

A biblioteca pypdf realiza o fatiamento de documentos de forma direta e eficiente. O procedimento baseia-se em abrir o documento fonte com o PdfReader e instanciar objetos PdfWriter para receber apenas as páginas selecionadas via método add_page(), salvando os novos arquivos no disco.

Principais abordagens de divisão:

  • Divisão Página por Página: Separa cada folha do PDF original em um arquivo independente (ex: pagina_1.pdf, pagina_2.pdf).
  • Divisão em Blocos (Fatiamento por Intervalos): Agrupa o documento em blocos com um número fixo de páginas (por exemplo, arquivos contendo 5 páginas cada) utilizando intervalos de iteração do Python.

Exemplo de Código: Divisão por Páginas Individuais e em Blocos Fixos

from pypdf import PdfReader, PdfWriter

caminho_pdf_extenso = "relatorio_completo.pdf"
reader = PdfReader(caminho_pdf_extenso)
total_paginas = len(reader.pages)

print(f"O documento possui {total_paginas} páginas no total.")

# -------------------------------------------------------------------
# ABORDAGEM 1: Dividir o PDF em arquivos individuais de 1 página
# -------------------------------------------------------------------
for i, pagina in enumerate(reader.pages, start=1):
    writer_individual = PdfWriter()
    writer_individual.add_page(pagina)

    nome_arquivo_individual = f"pagina_{i}.pdf"
    with open(nome_arquivo_individual, "wb") as f_saida:
        writer_individual.write(f_saida)

print("Divisão página por página concluída com sucesso!")

# -------------------------------------------------------------------
# ABORDAGEM 2: Dividir em blocos de tamanho fixo (ex: 5 páginas por arquivo)
# -------------------------------------------------------------------
tamanho_bloco = 5

for inicio in range(0, total_paginas, tamanho_bloco):
    writer_bloco = PdfWriter()

    # Fatiamento da lista de páginas correspondente ao bloco atual
    paginas_do_bloco = reader.pages[inicio : inicio + tamanho_bloco]
    for pagina in paginas_do_bloco:
        writer_bloco.add_page(pagina)

    numero_bloco = (inicio // tamanho_bloco) + 1
    nome_arquivo_bloco = f"relatorio_parte_{numero_bloco}.pdf"

    with open(nome_arquivo_bloco, "wb") as f_bloco:
        writer_bloco.write(f_bloco)

    print(f"Bloco {numero_bloco} salvo com {len(writer_bloco.pages)} página(s) em '{nome_arquivo_bloco}'.")

5. Envio Automático de E-mails com Anexos Usando smtplib e email

Após automatizar a leitura, a extração de dados e a organização dos arquivos em PDF, a etapa final para fechar o ciclo de automação corporativa é a distribuição dos relatórios consolidados para os destinatários. O Python oferece uma solução nativa, robusta e segura para o envio programático de mensagens através dos módulos smtplib e email, que já vêm integrados à biblioteca padrão da linguagem sem a necessidade de instalações adicionais.

Nesse fluxo de trabalho, o módulo smtplib atua no nível de rede, sendo responsável por estabelecer a conexão segura com o servidor SMTP (Simple Mail Transfer Protocol), autenticar o usuário e disparar a mensagem final. Já o módulo email cuida da construção estrutural do documento através das classes MIME (Multipurpose Internet Mail Extensions), permitindo definir remetentes, destinatários, assunto, corpo da mensagem em texto ou HTML e o empacotamento de arquivos binários anexados.

Neste capítulo, você aprenderá a estruturar e enviar e-mails automáticos contendo os relatórios em PDF gerados nos passos anteriores, cobrindo desde a montagem do container multipart até as conexões seguras via SSL/TLS e as melhores práticas de proteção de credenciais.


Montando a Estrutura da Mensagem com Suporte a HTML e Anexos (MIMEMultipart, MIMEText)

Para enviar e-mails profissionais com formatação gráfica (como títulos estilizados, tabelas e links) acompanhados de arquivos anexos (como PDFs ou planilhas), o módulo nativo email do Python fornece a infraestrutura necessária.

Em vez de enviar apenas uma string de texto simples, utiliza-se a classe MIMEMultipart, que funciona como um container principal capaz de agrupar múltiplos elementos da mensagem. Dentro dessa estrutura, conectam-se os submódulos específicos:

  • MIMEText: Adiciona o corpo do e-mail, podendo ser formatado como texto puro (_subtype="plain") ou código HTML (_subtype="html").
  • MIMEBase ou MIMEApplication: Processa arquivos binários baixados ou gerados localmente.
  • encoders.encode_base64: Codifica o anexo binário em caracteres ASCII (Base64), garantindo a integridade do arquivo durante o tráfego nos servidores de e-mail.

Exemplo de Código: E-mail Formatado em HTML com Anexo PDF

O script a seguir estrutura uma mensagem com cabeçalhos completos (From, To, Subject), insere um corpo formatado em HTML e anexa o arquivo PDF do relatório:

import os
import smtplib
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 1. Configurações de remetente, destinatário e arquivo
REMETENTE = os.getenv("EMAIL_REMETENTE", "seu_email@empresa.com")
DESTINATARIO = "cliente@empresa.com"
SENHA = os.getenv("EMAIL_SENHA", "sua_senha_ou_app_token")
CAMINHO_ANEXO = "relatorio_financeiro.pdf"

# 2. Criação do container multipart e cabeçalhos
msg = MIMEMultipart()
msg["From"] = REMETENTE
msg["To"] = DESTINATARIO
msg["Subject"] = "Relatório Semanal de Operações - Processamento Automático"

# 3. Definição do corpo do e-mail em HTML
corpo_html = """
<html>
  <body style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
    <h2 style="color: #0056b3;">Relatório Consolidado de Vendas</h2>
    <p>Olá,</p>
    <p>O processamento dos arquivos PDF foi concluído com sucesso pelo pipeline em Python.</p>
    <p>Em anexo, você encontrará o documento em <strong>PDF</strong> com a síntese de todos os dados extraídos.</p>
    <hr style="border: none; border-top: 1px solid #ddd;" />
    <p style="font-size: 0.85em; color: #777;"><em>Este e-mail foi gerado e enviado automaticamente. Por favor, não responda a esta mensagem.</em></p>
  </body>
</html>
"""

# Anexa o corpo HTML ao container principal
msg.attach(MIMEText(corpo_html, "html", "utf-8"))

# 4. Leitura e codificação do arquivo anexo
if os.path.exists(CAMINHO_ANEXO):
    with open(CAMINHO_ANEXO, "rb") as arquivo_f:
        anexo = MIMEBase("application", "octet-stream")
        anexo.set_payload(arquivo_f.read())

    # Codifica o arquivo em Base64
    encoders.encode_base64(anexo)

    # Adiciona o cabeçalho informando o nome do arquivo para o cliente de e-mail
    nome_arquivo = os.path.basename(CAMINHO_ANEXO)
    anexo.add_header("Content-Disposition", f"attachment; filename={nome_arquivo}")

    # Adiciona o anexo à mensagem
    msg.attach(anexo)
else:
    print(f"Aviso: O anexo '{CAMINHO_ANEXO}' não foi encontrado. Enviando apenas o corpo da mensagem.")

# 5. Conexão segura e disparo via SMTP com SSL (Porta 465)
try:
    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login(REMETENTE, SENHA)
        server.sendmail(REMETENTE, DESTINATARIO, msg.as_string())
    print("E-mail em HTML com anexo enviado com sucesso!")
except Exception as e:
    print(f"Erro ao disparar o e-mail: {e}")

Conectando com Segurança ao Servidor SMTP (TLS/SSL nas Portas 587 ou 465)

Ao enviar e-mails de forma programática usando a biblioteca nativa smtplib, a segurança da conexão e a proteção das suas credenciais devem ser prioridades absolutas. A comunicação com o servidor SMTP precisa ser criptografada para evitar a interceptação de senhas e do conteúdo dos relatórios durante o tráfego na rede.

Existem dois métodos padrões para estabelecer uma conexão segura em Python:

  • Porta 587 (TLS Explícito / STARTTLS): A conexão é iniciada em texto puro via smtplib.SMTP() e, em seguida, elevada para uma conexão criptografada através do método smtp.starttls() antes de realizar o login.
  • Porta 465 (SSL/TLS Implícito): A criptografia SSL/TLS é estabelecida imediatamente no momento da abertura da conexão utilizando a classe smtplib.SMTP_SSL().

Para garantir a validação de certificados digitais e proteger a aplicação contra ataques, recomenda-se criar um contexto seguro com ssl.create_default_context(). Além disso, senhas de acesso e tokens de aplicação jamais devem ser gravados diretamente no código-fonte; em vez disso, carregue as credenciais a partir de variáveis de ambiente do sistema operacional via os.getenv() ou os.environ.


Exemplo de Código: Conexão Segura com STARTTLS (Porta 587) e SSL (Porta 465)

import os
import smtplib
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 1. Carregamento seguro de credenciais via variáveis de ambiente
SMTP_SERVER = os.getenv("SMTP_HOST", "smtp.gmail.com")
REMETENTE = os.getenv("SMTP_USER")
SENHA = os.getenv("SMTP_PASS")
DESTINATARIO = "gestor@empresa.com"

# 2. Construção da mensagem
msg = MIMEMultipart()
msg["From"] = REMETENTE
msg["To"] = DESTINATARIO
msg["Subject"] = "Relatório Diário - Teste de Conexão Segura"
msg.attach(MIMEText("Olá! Segue a validação do pipeline de envio automático.", "plain", "utf-8"))

# Criando o contexto SSL padrão para verificação de certificados
contexto_ssl = ssl.create_default_context()

# -------------------------------------------------------------------
# ABORDAGEM 1: STARTTLS na Porta 587 (Recomendado para a maioria dos provedores)
# -------------------------------------------------------------------
try:
    with smtplib.SMTP(SMTP_SERVER, 587, timeout=30) as server:
        server.ehlo()
        server.starttls(context=contexto_ssl)  # Eleva a conexão para TLS seguro
        server.ehlo()
        server.login(REMETENTE, SENHA)

        recusados = server.send_message(msg)
        if not recusados:
            print("Sucesso! E-mail enviado via STARTTLS na porta 587.")
        else:
            print(f"Atenção: Destinatários recusados pelo servidor: {recusados}")
except Exception as e:
    print(f"Falha na conexão via porta 587: {e}")

# -------------------------------------------------------------------
# ABORDAGEM 2: SSL Implícito na Porta 465 (Alternativa)
# -------------------------------------------------------------------
try:
    with smtplib.SMTP_SSL(SMTP_SERVER, 465, context=contexto_ssl, timeout=30) as server:
        server.login(REMETENTE, SENHA)
        server.send_message(msg)
        print("Sucesso! E-mail enviado via SSL na porta 465.")
except Exception as e:
    print(f"Falha na conexão via porta 465: {e}")

6. Agendamento e Boas Práticas em Produção

Desenvolver um script em Python capaz de extrair dados de PDFs e disparar e-mails é um grande avanço, mas o verdadeiro valor da automação só é alcançado quando o sistema roda de forma autônoma, sem necessidade de execução manual no dia a dia. Para transformar um código de teste em um pipeline resiliente e pronto para o ambiente de produção, é indispensável configurar o agendamento automático das rotinas e adotar padrões rigorosos de segurança.

A transição para um ambiente corporativo exige cuidar de dois pilares fundamentais: a execução pontual e recorrente dos scripts — utilizando agendadores nativos do sistema operacional, como o Task Scheduler no Windows ou o cron em sistemas Linux e macOS — e a proteção de credenciais sensíveis, garantindo que senhas de e-mail e tokens de acesso jamais fiquem expostos no código-fonte. Além disso, a incorporação de rotinas para tratamento de exceções e registros de logs assegura que eventuais falhas de rede ou arquivos ausentes sejam identificados rapidamente, sem interromper o fluxo de trabalho.

Nesta seção, você entenderá como integrar essas ferramentas de agendamento, como isolar credenciais usando variáveis de ambiente e quais práticas adotar para manter seu robô de relatórios rodando com estabilidade e segurança total.


Execução Automática Diária com Task Scheduler (Windows) ou cron (Linux/Mac)

A automação de scripts em Python alcança seu potencial máximo quando elimina totalmente a necessidade de intervenção humana para iniciar a execução. Em vez de depender do acionamento manual por um operador, o sistema operacional pode ser configurado para disparar o pipeline de leitura de PDFs e envio de relatórios em horários programados.

Os dois principais utilitários nativos para agendamento de tarefas recorrentes são o Task Scheduler no Windows e o utilitário cron em ambientes Linux e macOS.


1. Configurando no Windows (Task Scheduler)

No Windows, o Agendador de Tarefas (Task Scheduler) executa scripts do Python em segundo plano:

  1. Acessar o Agendador: Abra o Menu Iniciar, pesquise por Agendador de Tarefas e clique em Criar Tarefa Básica... (Create Basic Task).
  2. Definir o Disparador (Trigger): Selecione a frequência Diariamente (Daily) e escolha a hora exata da primeira execução (por exemplo, 08:00:00).
  3. Configurar a Ação (Action): Escolha Iniciar um programa (Start a Program):
    • Programa/script: Informe o caminho absoluto do executável do Python (ex: C:\Python312\python.exe ou C:\projeto\.venv\Scripts\python.exe).
    • Adicionar argumentos (Add arguments): Digite o caminho completo do seu arquivo Python (ex: C:\automacao\enviar_relatorios.py).
    • Iniciar em (Start in): Insira a pasta de trabalho do projeto (ex: C:\automacao\).

2. Configurando no Linux / macOS (cron)

Em sistemas Unix, o serviço de segundo plano cron gerencia o agendamento por meio do arquivo de configuração crontab.

  1. Abrir a Edição do Crontab: No terminal, execute o comando:

    crontab -e
    
  2. Sintaxe dos Cinco Campos: A estrutura básica do cron é definida por cinco posições de tempo seguidas do comando: \[\text{minuto (0-59) | hora (0-23) | dia do mês (1-31) | mês (1-12) | dia da semana (0-6)}\]

  3. Exemplo de Regra Diária: Para rodar o script todos os dias às 08:00 da manhã, adicione a seguinte linha no final do arquivo:

    0 8 * * * /usr/bin/python3 /home/usuario/automacao/enviar_relatorios.py >> /home/usuario/automacao/execucao.log 2>&1
    

Exemplo de Código: Script Python Preparado para Execução em Segundo Plano

Para garantir que o script agendado rode de forma resiliente e autônoma, utilize caminhos absolutos para arquivos de entrada/saída e configure um sistema de logs para registrar falhas sem travar a execução:

import os
import sys
import logging
from pathlib import Path
from datetime import datetime

# 1. Definição do diretório base do script (garante o funcionamento com caminhos absolutos)
DIRETORIO_BASE = Path(__file__).resolve().parent
ARQUIVO_LOG = DIRETORIO_BASE / "automacao.log"

# 2. Configuração do sistema de logs para monitoramento autônomo
logging.basicConfig(
    filename=ARQUIVO_LOG,
    level=logging.INFO,
    format="%(asctime)s - [%(levelname)s] - %(message)s",
    encoding="utf-8"
)

def executar_rotina_diaria():
    logging.info("Iniciando a rotina agendada de relatórios...")

    try:
        # Exemplo: Caminhos absolutos para o relatório e scripts
        pasta_entrada = DIRETORIO_BASE / "documentos_pdf"
        arquivo_relatorio = DIRETORIO_BASE / "saida" / f"relatorio_{datetime.now().strftime('%Y%m%d')}.pdf"

        # Insira aqui a chamada das funções de leitura do PDF e envio por e-mail
        logging.info(f"Verificando pasta de entrada: {pasta_entrada}")

        if not pasta_entrada.exists():
            raise FileNotFoundError(f"A pasta '{pasta_entrada}' não foi encontrada.")

        # Lógica de processamento...
        logging.info("Processamento de PDFs e envio de e-mails concluído com sucesso.")

    except Exception as e:
        logging.error(f"Erro crítico durante a execução agendada: {e}", exc_info=True)
        sys.exit(1)

if __name__ == "__main__":
    executar_rotina_diaria()

Segurança de Credenciais: Proteja Senhas e Tokens com Variáveis de Ambiente (os.getenv)

No desenvolvimento de automações de e-mail e leitura de relatórios, o vazamento acidental de chaves de acesso e senhas de e-mail representa um dos maiores riscos de segurança. Deixar credenciais gravadas diretamente no código-fonte (hardcoded) expõe suas contas a vazamentos caso o script seja compartilhado com a equipe, enviado para repositórios públicos como o GitHub ou disponibilizado em ambiente corporativo.

A boa prática recomendada para ambiente de produção consiste em isolar dados sensíveis através de variáveis de ambiente. Dessa forma, o script Python lê as informações de acesso dinamicamente do sistema operacional durante a execução, mantendo o código limpo, seguro e pronto para ser movido entre ambientes de teste e produção sem alterações no arquivo .py.

Padrões e Boas Práticas de Segurança
  • os.getenv() com Valor Padrão (Fallback): A função os.getenv("NOME_DA_VARIAVEL", default) tenta buscar o valor configurado no sistema. Caso a variável não exista, ela retorna o valor padrão fornecido ou None.
  • Arquivo .env para Desenvolvimento Local: Em vez de configurar variáveis globais no sistema operacional a todo momento, utiliza-se a biblioteca python-dotenv para carregar as chaves a partir de um arquivo simples .env localizado na raiz do projeto.
  • Proteção no Git (.gitignore): O arquivo .env contendo as senhas reais jamais deve ser enviado para o controle de versão. Adicione a linha .env no arquivo .gitignore do seu repositório para evitar o envio acidental de dados sensíveis.

Exemplo de Código: Carregando Credenciais Seguras com python-dotenv e os.getenv

Para utilizar o suporte a arquivos .env, instale a biblioteca no terminal com pip install python-dotenv.

1. Conteúdo do arquivo .env (salvo na raiz do projeto):

SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=seu_email@empresa.com
SMTP_PASS=sua_senha_de_app_ou_token

2. Script Python (enviar_seguro.py):

import os
import smtplib
import ssl
from email.message import EmailMessage
from dotenv import load_dotenv

# 1. Carrega as variáveis declaradas no arquivo .env
load_dotenv()

# 2. Obtém as credenciais usando os.getenv
smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
smtp_port = int(os.getenv("SMTP_PORT", 587))
smtp_user = os.getenv("SMTP_USER")
smtp_pass = os.getenv("SMTP_PASS")

# Validação prévia para evitar tentativas de conexão sem credenciais
if not smtp_user or not smtp_pass:
    raise ValueError("Erro: As variáveis SMTP_USER ou SMTP_PASS não foram configuradas no ambiente.")

# 3. Construção do e-mail
msg = EmailMessage()
msg["Subject"] = "Relatório Diário - Pipeline Seguro"
msg["From"] = smtp_user
msg["To"] = "diretoria@empresa.com"
msg.set_content("Olá! Segue o relatório diário gerado automaticamente com credenciais protegidas.")

# 4. Conexão segura e envio via TLS
contexto_ssl = ssl.create_default_context()

try:
    with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
        server.ehlo()
        server.starttls(context=contexto_ssl)
        server.ehlo()
        server.login(smtp_user, smtp_pass)

        server.send_message(msg)
        print("E-mail enviado com sucesso utilizando credenciais seguras!")
except Exception as e:
    print(f"Falha ao conectar ou enviar mensagem: {e}")

Tratamento de Exceções e Registros de Logs para um Pipeline à Prova de Falhas

Em sistemas de produção executados de forma automática em segundo plano, o pipeline de automação não pode falhar silenciosamente ou ser interrompido totalmente por um único arquivo danificado ou instabilidade temporária de rede. Para garantir a resiliência do sistema, a arquitetura da automação precisa ser construída com dois pilares fundamentais: tratamento defensivo de exceções (try-except) e registros detalhados de execução (logging).

Princípios de um Pipeline Resiliente
  1. Isolamento de Falhas por Arquivo: O processamento de cada documento PDF deve ser isolado dentro de blocos de captura de erros (try-except). Se um arquivo estiver protegido por senha, corrompido ou fora do layout esperado, o erro é registrado e o script continua processando os demais documentos do lote sem travar a execução.
  2. Logs Estruturados: Trocar o uso de print() pelo módulo nativo logging permite registrar a data, hora e severidade (INFO, WARNING, ERROR, CRITICAL) de cada etapa diretamente em arquivos de log (.log) para auditorias futuras.
  3. Validação Prévia de Recursos: Antes de efetuar o processamento de um anexo ou tentar autenticar no servidor SMTP, o script deve validar se os diretórios e arquivos existem e se as credenciais foram carregadas corretamente, executando fluxos alternativos (fallbacks) caso identifique inconsistências.

Exemplo de Código: Pipeline com Tratamento de Erros e Logging Completo

O script abaixo consolida essas boas práticas em um fluxo que processa arquivos em lote, registra exceções sem interromper a execução e envia um relatório final sobre o estado do processamento:

import os
import sys
import smtplib
import logging
from pathlib import Path
from pypdf import PdfReader
from email.message import EmailMessage
from dotenv import load_dotenv

# 1. Configuração do arquivo de log
DIRETORIO_BASE = Path(__file__).resolve().parent
ARQUIVO_LOG = DIRETORIO_BASE / "pipeline_execucao.log"

logging.basicConfig(
    filename=ARQUIVO_LOG,
    level=logging.INFO,
    format="%(asctime)s - [%(levelname)s] - %(message)s",
    encoding="utf-8"
)

load_dotenv()

def processar_pdf_seguro(caminho_pdf):
    """Lê o PDF de forma defensiva e retorna o texto extraído."""
    try:
        reader = PdfReader(caminho_pdf)
        if reader.is_encrypted:
            logging.warning(f"O arquivo '{caminho_pdf.name}' está protegido por senha. Ignorando...")
            return None

        texto = ""
        for pagina in reader.pages:
            texto += pagina.extract_text() or ""

        return texto.strip()
    except Exception as e:
        logging.error(f"Falha ao ler a estrutura do arquivo '{caminho_pdf.name}': {e}")
        return None

def enviar_relatorio_log(destinatario, assunto, conteudo, anexo_path=None):
    """Envia o e-mail de notificação capturando falhas de rede e autenticação."""
    smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
    smtp_port = int(os.getenv("SMTP_PORT", 587))
    smtp_user = os.getenv("SMTP_USER")
    smtp_pass = os.getenv("SMTP_PASS")

    if not smtp_user or not smtp_pass:
        logging.critical("Credenciais SMTP não foram encontradas nas variáveis de ambiente.")
        return False

    msg = EmailMessage()
    msg["From"] = smtp_user
    msg["To"] = destinatario
    msg["Subject"] = assunto
    msg.set_content(conteudo)

    # Validação do anexo
    if anexo_path and os.path.exists(anexo_path):
        try:
            with open(anexo_path, "rb") as f:
                msg.add_attachment(f.read(), maintype="application", subtype="pdf", filename=os.path.basename(anexo_path))
        except Exception as e:
            logging.error(f"Erro ao anexar arquivo '{anexo_path}': {e}")
    elif anexo_path:
        logging.warning(f"Anexo '{anexo_path}' não foi localizado. O e-mail será enviado sem anexo.")

    # Tentativa de disparo com tratamento de erros de conexão
    try:
        with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
            server.starttls()
            server.login(smtp_user, smtp_pass)
            server.send_message(msg)
            logging.info(f"Notificação enviada com sucesso para {destinatario}.")
            return True
    except smtplib.SMTPAuthenticationError:
        logging.error(f"Erro de autenticação no servidor SMTP para o usuário '{smtp_user}'.")
    except smtplib.SMTPException as e:
        logging.error(f"Erro de protocolo SMTP durante envio para {destinatario}: {e}")
    except Exception as e:
        logging.error(f"Falha de conexão com a rede ou servidor: {e}")

    return False

def executar_pipeline_completo():
    logging.info("=== INICIANDO CICLO DE EXECUÇÃO DO PIPELINE ===")
    pasta_documentos = DIRETORIO_BASE / "documentos_entrantes"

    if not pasta_documentos.exists():
        logging.error(f"Diretório de entrada '{pasta_documentos}' não existe. Encerrando execução.")
        return

    arquivos_pdf = list(pasta_documentos.glob("*.pdf"))
    logging.info(f"Encontrados {len(arquivos_pdf)} arquivos PDF para processar.")

    sucessos = 0
    falhas = 0

    for pdf_file in arquivos_pdf:
        logging.info(f"Processando: {pdf_file.name}")
        conteudo = processar_pdf_seguro(pdf_file)

        if conteudo:
            sucessos += 1
            logging.info(f"Extração concluída para '{pdf_file.name}' ({len(conteudo)} caracteres).")
        else:
            falhas += 1

    resumo = f"Processamento concluído. Arquivos lidos com sucesso: {sucessos} | Falhas/Alertas: {falhas}"
    logging.info(resumo)

    # Notificação final do status da automação
    enviar_relatorio_log(
        destinatario="operacoes@empresa.com",
        assunto="[STATUS PIPELINE] Resumo Diário de Processamento",
        conteudo=f"Relatório de execução:\n\n{resumo}\n\nVerifique o arquivo 'pipeline_execucao.log' para detalhes."
    )

if __name__ == "__main__":
    executar_pipeline_completo()

7. Conclusão e Próximos Passos

A automação do fluxo de trabalho com documentos PDF e e-mails é uma das estratégias de maior impacto para aumentar a produtividade e eliminar tarefas repetitivas no ambiente corporativo. Ao longo deste guia, transitamos da extração básica de caracteres até a construção de um pipeline de produção resiliente, autônomo e seguro.


Resumo do Pipeline Completo

O ciclo de automação ponta a ponta que estruturamos é fundamentado em cinco etapas integradas:

  1. Ingestão e Validação de PDFs: Verificação da natureza do documento (PDF vetorial nativo versus imagem escaneada). Quando o texto digital está disponível, utilizamos parsers leves como pypdf ou pdfplumber; caso contrário, acionamos o motor de OCR via Tesseract.
  2. Extração e Sanitização de Dados: Leitura de tabelas e alinhamentos espaciais com pdfplumber. Os dados brutos são convertidos em DataFrames do pandas, onde realizamos a limpeza de quebras de linha, padronização de datas e conversão da vírgula decimal brasileira para tipo numérico (float).
  3. Manipulação Estrutural de Arquivos: Organização de páginas com pypdf, permitindo mesclar múltiplos relatórios em lote ou dividir documentos extensos em partes menores.
  4. Composição e Disparo de E-mails: Construção de mensagens em HTML profissional com a classe MIMEMultipart, codificação de anexos em Base64 e envio seguro via smtplib (com TLS/SSL nas portas 587 ou 465).
  5. Execução Autônoma e Segurança: Isolamento de chaves e senhas com variáveis de ambiente (os.getenv), rastreamento de exceções via logging e agendamento pelo Task Scheduler (Windows) ou cron (Linux/Mac).

O Script Mestre: Pipeline Ponta a Ponta

O código abaixo integra todas as etapas estudadas em um único fluxo de execução: ele varre uma pasta em busca de relatórios PDF, extrai e consolida as tabelas em uma planilha Excel, salva um histórico em arquivo de log e dispara um e-mail formatado em HTML com o relatório consolidado em anexo:

import os
import ssl
import logging
import smtplib
from pathlib import Path
import pdfplumber
import pandas as pd
from dotenv import load_dotenv
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email import encoders

# 1. Configuração do ambiente e logging
DIRETORIO_BASE = Path(__file__).resolve().parent
ARQUIVO_LOG = DIRETORIO_BASE / "execucao_pipeline.log"

logging.basicConfig(
    filename=ARQUIVO_LOG,
    level=logging.INFO,
    format="%(asctime)s - [%(levelname)s] - %(message)s",
    encoding="utf-8"
)

load_dotenv(DIRETORIO_BASE / ".env")

def executar_pipeline_mestre():
    logging.info("Iniciando o Pipeline Mestre de Processamento e Notificação...")

    pasta_entrada = DIRETORIO_BASE / "documentos_entrantes"
    arquivo_excel_saida = DIRETORIO_BASE / "relatorio_consolidado.xlsx"

    if not pasta_entrada.exists():
        pasta_entrada.mkdir(parents=True, exist_ok=True)
        logging.warning(f"Pasta '{pasta_entrada}' criada. Adicione arquivos PDF para processamento.")
        return

    arquivos_pdf = list(pasta_entrada.glob("*.pdf"))
    if not arquivos_pdf:
        logging.info("Nenhum arquivo PDF encontrado na pasta de entrada.")
        return

    dados_finais = []
    cabecalho = None

    # 2. Processamento e extração de dados
    for pdf_path in arquivos_pdf:
        logging.info(f"Lendo arquivo: {pdf_path.name}")
        try:
            with pdfplumber.open(pdf_path) as pdf:
                for pagina in pdf.pages:
                    tabela = pagina.extract_table({
                        "vertical_strategy": "text",
                        "horizontal_strategy": "lines",
                        "snap_tolerance": 3
                    }) or pagina.extract_table()

                    if tabela:
                        if cabecalho is None:
                            cabecalho = [str(c).strip().replace("\n", " ") for c in tabela if c]
                            linhas = tabela[1:]
                        else:
                            linhas = tabela[1:] if tabela == cabecalho else tabela

                        for linha in linhas:
                            linha_limpa = [str(cel).strip().replace("\n", " ") if cel else "" for cel in linha]
                            if any(linha_limpa):
                                dados_finais.append(linha_limpa)
        except Exception as err:
            logging.error(f"Erro ao processar '{pdf_path.name}': {err}")

    # 3. Tratamento dos dados com Pandas e exportação
    if dados_finais and cabecalho:
        df = pd.DataFrame(dados_finais, columns=cabecalho[:len(dados_finais)])
        df.to_excel(arquivo_excel_saida, index=False)
        logging.info(f"Planilha consolidada gerada com {len(df)} registros em '{arquivo_excel_saida.name}'.")
    else:
        logging.warning("Nenhum dado tabular foi identificado nos PDFs.")
        return

    # 4. Envio do e-mail de notificação com anexo
    smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
    smtp_port = int(os.getenv("SMTP_PORT", 587))
    smtp_user = os.getenv("SMTP_USER")
    smtp_pass = os.getenv("SMTP_PASS")
    destinatario = os.getenv("SMTP_TO", "diretoria@empresa.com")

    if not smtp_user or not smtp_pass:
        logging.critical("Credenciais SMTP ausentes. O e-mail não será enviado.")
        return

    msg = MIMEMultipart()
    msg["From"] = smtp_user
    msg["To"] = destinatario
    msg["Subject"] = "Relatório Consolidado de Dados - Automação Python"

    corpo_html = f"""
    <html>
      <body style="font-family: Arial, sans-serif; color: #333;">
        <h2>Processamento Concluído com Sucesso</h2>
        <p>Foram processados <strong>{len(arquivos_pdf)}</strong> arquivos PDF.</p>
        <p>Total de registros consolidados: <strong>{len(df)}</strong>.</p>
        <p>A planilha atualizada segue em anexo a esta mensagem.</p>
      </body>
    </html>
    """
    msg.attach(MIMEText(corpo_html, "html", "utf-8"))

    # Anexando o arquivo Excel
    with open(arquivo_excel_saida, "rb") as f_anexo:
        parte = MIMEBase("application", "octet-stream")
        parte.set_payload(f_anexo.read())
        encoders.encode_base64(parte)
        parte.add_header("Content-Disposition", f"attachment; filename={arquivo_excel_saida.name}")
        msg.attach(parte)

    try:
        contexto = ssl.create_default_context()
        with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
            server.ehlo()
            server.starttls(context=contexto)
            server.ehlo()
            server.login(smtp_user, smtp_pass)
            server.send_message(msg)
            logging.info(f"E-mail enviado com sucesso para '{destinatario}'.")
    except Exception as e_smtp:
        logging.error(f"Falha ao enviar e-mail via SMTP: {e_smtp}")

if __name__ == "__main__":
    executar_pipeline_mestre()

Ideias de Projetos Práticos para Aplicar Hoje

Para consolidar o conhecimento adquirido, aqui estão quatro projetos práticos do mundo real que você pode implementar imediatamente na sua empresa ou portfólio:

  1. Parser de Faturas e Notas Fiscais (NFs-e / DANFEs):
    • Objetivo: Criar um script que monitora uma pasta de entrada de notas fiscais recebidas em PDF, extrai CNPJ do emitente, data de emissão, número da nota e valor total usando pdfplumber e Expressões Regulares (re), exportando tudo para o sistema contábil ou planilha financeira.
  2. Robô de Extratos Bancários e Conciliação Financeira:
    • Objetivo: Ler extratos bancários mensais em formato PDF (que costumam ser tabelas sem bordas visíveis), utilizando table_settings={"vertical_strategy": "text"}. O script categoriza entradas e saídas automáticas e gera um relatório consolidado de fluxo de caixa.
  3. Bot de Alerta Diário de KPIs e Vendas por E-mail:
    • Objetivo: Ler os relatórios de vendas diários gerados automaticamente pelo ERP da empresa em PDF, calcular o acumulado do mês com pandas e enviar um resumo em HTML estilizado para a diretoria todas as manhãs às 08:00 via cron.
  4. Organizador e Fatiador Inteligente de Contratos:
    • Objetivo: Ler uma pasta contendo contratos longos em PDF, inspecionar os metadados com pypdf, separar os anexos e capítulos importantes em arquivos individuais de 1 página e renomeá-los no formato YYYYMMDD_NomeCliente_Contrato.pdf.


Comentários

Postagens mais visitadas deste blog

Gerando Relatórios em PDF com Python (ReportLab e FPDF)

Python para Computação Quântica: Introdução com Qiskit

C# e Banco de Dados: Como Conectar e Realizar Operações com SQL Server