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
openpyxlcria e lê planilhas Excel com controle total sobre estilos, fórmulas e gráficospandasé a escolha para análise de planilhas grandes — leitura vetorizada e transformações eficientesReportLabgera PDFs do zero com controle preciso de layout, tabelas e estilosPyMuPDF(fitz) lê, divide, mescla e anota PDFs existentes com alta performanceParagraph,TableeSpacersão os blocos construtores do ReportLabTableStyledo 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=Trueao 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.