Python

Automação de Planilhas e PDFs Já leu

12 min de leitura

Automação de Planilhas e PDFs
Planilhas e PDFs são dois dos formatos mais onipresentes no ambiente corporativo — relatórios financeiros, boletins escolares, notas fiscais, contratos, folhas de pagamento. Manipulá-los manualmente é lento e sujeito a e

Planilhas e PDFs são dois dos formatos mais onipresentes no ambiente corporativo — relatórios financeiros, boletins escolares, notas fiscais, contratos, folhas de pagamento. Manipulá-los manualmente é lento e sujeito a erros. Python tem bibliotecas maduras para automatizar completamente a criação, leitura e modificação desses arquivos, transformando horas de trabalho repetitivo em segundos de execução.


openpyxl: Planilhas Excel

pip install openpyxl

Criando Planilhas

from openpyxl import Workbook
from openpyxl.styles import (
    Font, Fill, PatternFill, Alignment,
    Border, Side, numbers
)
from openpyxl.utils import get_column_letter
from openpyxl.chart import BarChart, Reference
from datetime import datetime


def criar_boletim(alunos: list, output_path: str):
    wb = Workbook()
    ws = wb.active
    ws.title = "Boletim"

    # Estilos
    fonte_titulo  = Font(name="Calibri", bold=True, size=14, color="FFFFFF")
    fonte_header  = Font(name="Calibri", bold=True, size=11, color="FFFFFF")
    fonte_normal  = Font(name="Calibri", size=11)
    fill_titulo   = PatternFill("solid", fgColor="1F4E79")
    fill_header   = PatternFill("solid", fgColor="2E75B6")
    fill_aprovado = PatternFill("solid", fgColor="E2EFDA")
    fill_reprovado= PatternFill("solid", fgColor="FCE4D6")
    fill_linha_par= PatternFill("solid", fgColor="F2F2F2")

    borda = Border(
        left=Side(style="thin"),
        right=Side(style="thin"),
        top=Side(style="thin"),
        bottom=Side(style="thin")
    )

    centralizado = Alignment(horizontal="center", vertical="center")

    # Título
    ws.merge_cells("A1:F1")
    ws["A1"] = "BOLETIM ESCOLAR — 2024"
    ws["A1"].font       = fonte_titulo
    ws["A1"].fill       = fill_titulo
    ws["A1"].alignment  = centralizado
    ws.row_dimensions[1].height = 30

    # Subtítulo
    ws.merge_cells("A2:F2")
    ws["A2"] = f"Gerado em {datetime.now().strftime('%d/%m/%Y às %H:%M')}"
    ws["A2"].font      = Font(name="Calibri", italic=True, size=10, color="666666")
    ws["A2"].alignment = centralizado

    # Cabeçalhos
    headers = ["#", "Nome", "Email", "Nota", "Status", "Conceito"]
    for col, header in enumerate(headers, 1):
        cell           = ws.cell(row=3, column=col, value=header)
        cell.font      = fonte_header
        cell.fill      = fill_header
        cell.alignment = centralizado
        cell.border    = borda

    ws.row_dimensions[3].height = 20

    # Dados
    for idx, aluno in enumerate(alunos, 1):
        linha = idx + 3
        nota  = aluno["nota"]

        if nota >= 9:    conceito = "A"
        elif nota >= 7:  conceito = "B"
        elif nota >= 6:  conceito = "C"
        else:            conceito = "F"

        status   = "Aprovado" if nota >= 6 else "Reprovado"
        fill_row = fill_aprovado if nota >= 6 else fill_reprovado
        if nota >= 6 and idx % 2 == 0:
            fill_row = fill_linha_par

        valores = [idx, aluno["nome"], aluno["email"], nota, status, conceito]
        for col, valor in enumerate(valores, 1):
            cell           = ws.cell(row=linha, column=col, value=valor)
            cell.font      = fonte_normal
            cell.fill      = fill_row
            cell.border    = borda
            cell.alignment = centralizado

        # Formata a nota com 1 casa decimal
        ws.cell(row=linha, column=4).number_format = "0.0"

    # Linha de totais
    total_linha = len(alunos) + 4
    ws.merge_cells(f"A{total_linha}:C{total_linha}")
    ws[f"A{total_linha}"]      = "ESTATÍSTICAS"
    ws[f"A{total_linha}"].font = Font(bold=True)

    notas = [a["nota"] for a in alunos]
    ws[f"D{total_linha}"]      = sum(notas) / len(notas)
    ws[f"D{total_linha}"].number_format = "0.00"
    ws[f"D{total_linha}"].font = Font(bold=True)

    aprovados = sum(1 for n in notas if n >= 6)
    ws[f"E{total_linha}"]      = f"{aprovados}/{len(alunos)} aprovados"
    ws[f"E{total_linha}"].font = Font(bold=True)

    # Largura das colunas
    larguras = {"A": 5, "B": 25, "C": 30, "D": 10, "E": 12, "F": 10}
    for col, largura in larguras.items():
        ws.column_dimensions[col].width = largura

    # Congela o cabeçalho
    ws.freeze_panes = "A4"

    # Gráfico de barras
    chart      = BarChart()
    chart.type = "col"
    chart.title     = "Distribuição de Notas"
    chart.y_axis.title = "Nota"
    chart.x_axis.title = "Aluno"

    dados_chart = Reference(
        ws,
        min_col=4, max_col=4,
        min_row=3, max_row=len(alunos) + 3
    )
    nomes_chart = Reference(
        ws,
        min_col=2, max_col=2,
        min_row=4, max_row=len(alunos) + 3
    )
    chart.add_data(dados_chart, titles_from_data=True)
    chart.set_categories(nomes_chart)
    chart.shape    = 4
    chart.width    = 20
    chart.height   = 12

    ws.add_chart(chart, f"A{total_linha + 2}")

    wb.save(output_path)
    print(f"Boletim salvo: {output_path}")


alunos = [
    {"nome": "Ana Silva",    "email": "ana@email.com",    "nota": 9.5},
    {"nome": "Bruno Costa",  "email": "bruno@email.com",  "nota": 7.0},
    {"nome": "Carla Souza",  "email": "carla@email.com",  "nota": 8.5},
    {"nome": "Diego Lima",   "email": "diego@email.com",  "nota": 5.5},
    {"nome": "Elena Pires",  "email": "elena@email.com",  "nota": 6.0},
]

criar_boletim(alunos, "boletim_2024.xlsx")

Lendo Planilhas Existentes

from openpyxl import load_workbook


def ler_planilha(caminho: str) -> list:
    """Lê planilha e retorna lista de dicionários."""
    wb = load_workbook(caminho, read_only=True, data_only=True)
    ws = wb.active

    dados = []
    headers = [cell.value for cell in ws[1]]   # primeira linha = cabeçalho

    for linha in ws.iter_rows(min_row=2, values_only=True):
        if any(v is not None for v in linha):   # ignora linhas vazias
            registro = dict(zip(headers, linha))
            dados.append(registro)

    wb.close()
    return dados


def processar_multiplas_abas(caminho: str):
    """Processa todas as abas de uma planilha."""
    wb = load_workbook(caminho, data_only=True)

    for nome_aba in wb.sheetnames:
        ws     = wb[nome_aba]
        linhas = list(ws.iter_rows(values_only=True))
        print(f"\nAba: {nome_aba} — {len(linhas)} linhas")
        for linha in linhas[:3]:
            print(f"  {linha}")

    wb.close()

pandas: Análise de Planilhas em Escala

Para planilhas grandes, pandas é muito mais eficiente:

pip install pandas openpyxl xlrd
import pandas as pd


def analisar_boletim(caminho: str):
    """Análise completa de boletim com pandas."""

    # Lendo Excel
    df = pd.read_excel(caminho, sheet_name="Boletim", header=2)

    print("=== Visão Geral ===")
    print(df.describe())

    # Filtrando
    aprovados  = df[df["Nota"] >= 6.0]
    reprovados = df[df["Nota"] < 6.0]

    print(f"\nAprovados:  {len(aprovados)}")
    print(f"Reprovados: {len(reprovados)}")
    print(f"Média:      {df['Nota'].mean():.2f}")

    # Agrupando
    por_conceito = df.groupby("Conceito")["Nota"].agg(["count", "mean"])
    print("\nPor conceito:")
    print(por_conceito)

    # Exportando resultado filtrado
    aprovados.to_excel("aprovados.xlsx", index=False)

    # Múltiplas abas
    with pd.ExcelWriter("relatorio_completo.xlsx", engine="openpyxl") as writer:
        df.to_excel(writer,         sheet_name="Todos",      index=False)
        aprovados.to_excel(writer,  sheet_name="Aprovados",  index=False)
        reprovados.to_excel(writer, sheet_name="Reprovados", index=False)

    print("\nRelatório completo salvo.")

ReportLab: Criando PDFs do Zero

pip install reportlab
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import (
    SimpleDocTemplate, Paragraph, Spacer,
    Table, TableStyle, HRFlowable
)
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
from datetime import datetime


def gerar_boletim_pdf(alunos: list, output_path: str):
    """Gera boletim escolar em PDF com ReportLab."""

    doc    = SimpleDocTemplate(
        output_path,
        pagesize=A4,
        rightMargin=2*cm, leftMargin=2*cm,
        topMargin=2*cm,   bottomMargin=2*cm
    )
    estilos = getSampleStyleSheet()
    elementos = []

    # Estilos customizados
    estilo_titulo = ParagraphStyle(
        "Titulo",
        parent=estilos["Heading1"],
        fontSize=18,
        textColor=colors.HexColor("#1F4E79"),
        alignment=TA_CENTER,
        spaceAfter=6
    )
    estilo_subtitulo = ParagraphStyle(
        "Subtitulo",
        parent=estilos["Normal"],
        fontSize=10,
        textColor=colors.grey,
        alignment=TA_CENTER,
        spaceAfter=20
    )
    estilo_normal = ParagraphStyle(
        "Normal2",
        parent=estilos["Normal"],
        fontSize=10,
        spaceAfter=6
    )

    # Cabeçalho
    elementos.append(Paragraph("SISTEMA ESCOLAR", estilo_titulo))
    elementos.append(Paragraph("Boletim de Desempenho — 2024", estilo_subtitulo))
    elementos.append(HRFlowable(width="100%", thickness=2, color=colors.HexColor("#1F4E79")))
    elementos.append(Spacer(1, 0.5*cm))

    # Informações gerais
    notas      = [a["nota"] for a in alunos]
    media      = sum(notas) / len(notas)
    aprovados  = sum(1 for n in notas if n >= 6)

    info = [
        [Paragraph("<b>Total de Alunos:</b>", estilo_normal),
         Paragraph(str(len(alunos)), estilo_normal)],
        [Paragraph("<b>Média Geral:</b>", estilo_normal),
         Paragraph(f"{media:.2f}", estilo_normal)],
        [Paragraph("<b>Aprovados:</b>", estilo_normal),
         Paragraph(f"{aprovados} ({aprovados/len(alunos)*100:.1f}%)", estilo_normal)],
        [Paragraph("<b>Data de Emissão:</b>", estilo_normal),
         Paragraph(datetime.now().strftime("%d/%m/%Y às %H:%M"), estilo_normal)],
    ]

    tabela_info = Table(info, colWidths=[5*cm, 10*cm])
    tabela_info.setStyle(TableStyle([
        ("VALIGN",       (0, 0), (-1, -1), "MIDDLE"),
        ("BOTTOMPADDING",(0, 0), (-1, -1), 6),
    ]))
    elementos.append(tabela_info)
    elementos.append(Spacer(1, 0.8*cm))

    # Tabela de alunos
    elementos.append(Paragraph("<b>Resultado por Aluno</b>", estilo_normal))
    elementos.append(Spacer(1, 0.3*cm))

    dados_tabela = [["#", "Nome", "E-mail", "Nota", "Status"]]

    for idx, aluno in enumerate(alunos, 1):
        status = "Aprovado" if aluno["nota"] >= 6 else "Reprovado"
        dados_tabela.append([
            str(idx),
            aluno["nome"],
            aluno["email"],
            f"{aluno['nota']:.1f}",
            status
        ])

    tabela = Table(
        dados_tabela,
        colWidths=[1*cm, 5*cm, 6*cm, 2*cm, 3*cm]
    )

    # Estilo da tabela
    estilo_tabela = TableStyle([
        # Cabeçalho
        ("BACKGROUND",    (0, 0), (-1, 0),  colors.HexColor("#2E75B6")),
        ("TEXTCOLOR",     (0, 0), (-1, 0),  colors.white),
        ("FONTNAME",      (0, 0), (-1, 0),  "Helvetica-Bold"),
        ("FONTSIZE",      (0, 0), (-1, 0),  10),
        ("ALIGN",         (0, 0), (-1, 0),  "CENTER"),
        ("BOTTOMPADDING", (0, 0), (-1, 0),  8),
        ("TOPPADDING",    (0, 0), (-1, 0),  8),

        # Corpo
        ("FONTNAME",      (0, 1), (-1, -1), "Helvetica"),
        ("FONTSIZE",      (0, 1), (-1, -1), 9),
        ("ALIGN",         (0, 1), (-1, -1), "CENTER"),
        ("VALIGN",        (0, 0), (-1, -1), "MIDDLE"),
        ("TOPPADDING",    (0, 1), (-1, -1), 6),
        ("BOTTOMPADDING", (0, 1), (-1, -1), 6),

        # Linhas alternadas
        *[("BACKGROUND", (0, i), (-1, i), colors.HexColor("#F2F2F2"))
          for i in range(2, len(dados_tabela), 2)],

        # Aprovados/Reprovados na última coluna
        *[("TEXTCOLOR", (4, i), (4, i),
           colors.HexColor("#375623") if dados_tabela[i][4] == "Aprovado"
           else colors.HexColor("#C00000"))
          for i in range(1, len(dados_tabela))],

        # Bordas
        ("GRID",          (0, 0), (-1, -1), 0.5, colors.HexColor("#CCCCCC")),
        ("BOX",           (0, 0), (-1, -1), 1,   colors.HexColor("#2E75B6")),
    ])
    tabela.setStyle(estilo_tabela)
    elementos.append(tabela)

    # Rodapé com estatísticas
    elementos.append(Spacer(1, 1*cm))
    elementos.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
    elementos.append(Spacer(1, 0.3*cm))

    rodape = ParagraphStyle(
        "Rodape",
        parent=estilos["Normal"],
        fontSize=8,
        textColor=colors.grey,
        alignment=TA_CENTER
    )
    elementos.append(Paragraph(
        "Documento gerado automaticamente pelo Sistema Escolar. "
        "Não requer assinatura.",
        rodape
    ))

    # Gera o PDF
    doc.build(elementos)
    print(f"PDF gerado: {output_path}")


gerar_boletim_pdf(alunos, "boletim_2024.pdf")

PyMuPDF: Lendo e Manipulando PDFs

pip install pymupdf
import fitz   # PyMuPDF


def extrair_texto_pdf(caminho: str) -> str:
    """Extrai todo o texto de um PDF."""
    doc   = fitz.open(caminho)
    texto = ""

    for pagina in doc:
        texto += pagina.get_text()

    doc.close()
    return texto


def extrair_por_pagina(caminho: str) -> list:
    """Extrai texto por página."""
    doc    = fitz.open(caminho)
    paginas = []

    for num, pagina in enumerate(doc, 1):
        paginas.append({
            "pagina":  num,
            "texto":   pagina.get_text(),
            "tamanho": pagina.rect
        })

    doc.close()
    return paginas


def dividir_pdf(caminho: str, paginas_por_arquivo: int = 10):
    """Divide PDF em arquivos menores."""
    doc     = fitz.open(caminho)
    total   = len(doc)
    base    = caminho.replace(".pdf", "")
    partes  = []

    for inicio in range(0, total, paginas_por_arquivo):
        fim    = min(inicio + paginas_por_arquivo, total)
        novo   = fitz.open()

        novo.insert_pdf(doc, from_page=inicio, to_page=fim - 1)
        saida = f"{base}_parte_{inicio//paginas_por_arquivo + 1}.pdf"
        novo.save(saida)
        novo.close()
        partes.append(saida)
        print(f"Salvo: {saida} ({fim - inicio} páginas)")

    doc.close()
    return partes


def mesclar_pdfs(lista_caminhos: list, saida: str):
    """Mescla múltiplos PDFs em um único arquivo."""
    resultado = fitz.open()

    for caminho in lista_caminhos:
        doc = fitz.open(caminho)
        resultado.insert_pdf(doc)
        doc.close()
        print(f"Adicionado: {caminho}")

    resultado.save(saida)
    resultado.close()
    print(f"\nMesclado: {saida}")


def adicionar_marca_dagua(caminho: str, texto: str, saida: str):
    """Adiciona marca d'água em todas as páginas."""
    doc = fitz.open(caminho)

    for pagina in doc:
        rect     = pagina.rect
        ponto    = fitz.Point(rect.width / 2, rect.height / 2)
        pagina.insert_text(
            ponto,
            texto,
            fontsize=50,
            color=(0.8, 0.8, 0.8),   # cinza claro
            rotate=45
        )

    doc.save(saida)
    doc.close()
    print(f"Marca d'água adicionada: {saida}")

Exemplo Completo: Pipeline de Relatórios

from pathlib import Path
from datetime import datetime
import json


def pipeline_relatorios(dados_json: str, pasta_saida: str):
    """
    Pipeline completo:
    1. Lê dados JSON
    2. Gera planilha Excel
    3. Gera PDF
    4. Adiciona marca d'água no PDF
    """
    pasta = Path(pasta_saida)
    pasta.mkdir(exist_ok=True)

    # 1. Carrega dados
    with open(dados_json, encoding="utf-8") as f:
        dados = json.load(f)

    alunos  = dados["alunos"]
    periodo = dados.get("periodo", datetime.now().strftime("%Y-%m"))
    print(f"Processando {len(alunos)} alunos — Período: {periodo}")

    # 2. Gera Excel
    excel_path = str(pasta / f"boletim_{periodo}.xlsx")
    criar_boletim(alunos, excel_path)

    # 3. Gera PDF
    pdf_path    = str(pasta / f"boletim_{periodo}.pdf")
    pdf_final   = str(pasta / f"boletim_{periodo}_oficial.pdf")
    gerar_boletim_pdf(alunos, pdf_path)

    # 4. Adiciona marca d'água
    adicionar_marca_dagua(pdf_path, "DOCUMENTO OFICIAL", pdf_final)

    # 5. Remove PDF intermediário
    Path(pdf_path).unlink()

    print(f"\n✓ Pipeline concluído:")
    print(f"  Excel: {excel_path}")
    print(f"  PDF:   {pdf_final}")

    return {"excel": excel_path, "pdf": pdf_final}


# Dados de exemplo
dados_exemplo = {
    "periodo": "2024-03",
    "alunos": [
        {"nome": "Ana Silva",    "email": "ana@email.com",    "nota": 9.5},
        {"nome": "Bruno Costa",  "email": "bruno@email.com",  "nota": 7.0},
        {"nome": "Carla Souza",  "email": "carla@email.com",  "nota": 8.5},
        {"nome": "Diego Lima",   "email": "diego@email.com",  "nota": 5.5},
        {"nome": "Elena Pires",  "email": "elena@email.com",  "nota": 6.0},
    ]
}

with open("dados.json", "w", encoding="utf-8") as f:
    json.dump(dados_exemplo, f, ensure_ascii=False, indent=2)

resultado = pipeline_relatorios("dados.json", "relatorios")

Resumo

  • openpyxl cria e lê planilhas Excel com controle total sobre estilos, fórmulas e gráficos
  • pandas é a escolha para análise de planilhas grandes — leitura vetorizada e transformações eficientes
  • ReportLab gera PDFs do zero com controle preciso de layout, tabelas e estilos
  • PyMuPDF (fitz) lê, divide, mescla e anota PDFs existentes com alta performance
  • Paragraph, Table e Spacer são os blocos construtores do ReportLab
  • TableStyle do ReportLab controla cores, bordas, alinhamento e fontes por célula
  • Pipelines combinam Excel + PDF para relatórios corporativos completos e automatizados
  • Sempre use data_only=True ao abrir planilhas com fórmulas para obter valores calculados

Referências e Leituras Complementares

  • openpyxl — documentação oficial — https://openpyxl.readthedocs.io/en/stable/
  • ReportLab — guia do usuário — https://www.reportlab.com/docs/reportlab-userguide.pdf
  • PyMuPDF — documentação oficial — https://pymupdf.readthedocs.io/en/latest/
  • pandas — leitura de Excel — https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html
  • SWEIGART, Al. Automate the Boring Stuff with Python. 2. ed. No Starch Press, 2019. Cap. 12–13 — automação de planilhas Excel e PDFs com Python.
  • MCKINNEY, Wes. Python for Data Analysis. 3. ed. O'Reilly Media, 2022. Cap. 6 — leitura e escrita de dados tabulares com pandas.
Comentários

Mais em Python

FastAPI: APIs modernas com tipagem e documentação automática
FastAPI: APIs modernas com tipagem e documentação automática

FastAPI é o framework web que mais cresceu no ecossistema Python nos últimos...

Artigo 46 — Logging, Monitoramento e Observabilidade
Artigo 46 — Logging, Monitoramento e Observabilidade

Um sistema em produção que você não consegue observar é uma caixa preta. Quan...

Dominando o Python
Dominando o Python

Se voc&ecirc; j&aacute; pesquisou sobre programa&ccedil;&atilde;o nos &uacute...