DEV Community

Cover image for [pt-BR] Como realizar tratamento de arquivos .pdf com PYTHON 🐍
Juliano Laurentino
Juliano Laurentino

Posted on

[pt-BR] Como realizar tratamento de arquivos .pdf com PYTHON 🐍

eaeh caba bom 🤙

Hoje vamos aprender como tratar arquivos pdf com python e se livrar daquele trabalho chatão de tratamento de base toda vez que te mandarem atualizar o dashboard a partir desse arquivo.

Cenário

Imagina sempre receber um arquivo pdf 🥶 com uma informação importante e ter que transformar esse dado em uma tabela para alimentar uma ferramenta de data viz. Podendo ser uma base extraída para csv/excel etc.

Usaremos um exemplo de entradas de passageiros dentro de um aeroporto

Apenas o quadro "Estimativa de Passageiros - Partida" nos interessa nesse momento no exemplo a seguir

estimativa de passageiros

Bibliotecas Python

Essas são as bibliotecas utilizas nesse projeto (dependendo, pode até rolar um .venv depois):

import pandas as pd
import numpy as np
import pdfplumber
import re
Enter fullscreen mode Exit fullscreen mode

Ingestão

Aqui será realizado o tratamento de acordo com a demanda do arquivo, podendo variar em cada cenário e tipo de pdf's, mas sempre seguindo a mesma lógica:

  • Achar a tabela a partir de referencias dentro do pdf (nesse caso acharemos a informação a partir da palavra "Partidas" como referềncia)
  • transformar em tabela, estruturar e exportar uma base limpa para ser consumida em alguma ferramenta de data viz

Estrutura desse arquivo

  • Linha 0: Ano = 2026
  • Linha 3: Mês = janeiro / fevereiro (meses espalhados em colunas)
  • Linha 6: Hora | Dias (19, 20, 21, 22... 31, 1, 2) — são os dias do mês como colunas
  • Linhas 9+: Horários (00:00, 01:00, 02:00...) com volumes por dia

Etapas

Etapa 1 — Abre o PDF com pdfplumber e varre todas as palavras da página para localizar as coordenadas verticais (eixo Y) das ocorrências do termo "Partidas". A primeira ocorrência marca o início da tabela desejada e a segunda (que corresponde ao título "Chegadas + Partidas") marca o fim. Com esses dois pontos é feito um recorte preciso da região (crop) e a tabela é extraída como lista de listas via extract_table(). Louco não?

with pdfplumber.open(caminho_pdf) as pdf:
    pagina = pdf.pages[0]

    palavras = pagina.extract_words()

    y_partidas = None
    y_chegadas_partidas = None
    ocorrencias_estimativa = []


    for palavra in palavras:
        if "Partidas" in palavra["text"]:
            ocorrencias_estimativa.append(palavra["top"])

    if len(ocorrencias_estimativa) >= 1:
        y_partidas = ocorrencias_estimativa[0]
    if len(ocorrencias_estimativa) >= 2:
        y_chegadas_partidas = ocorrencias_estimativa[1]

    print(f"Y início Partidas: {y_partidas}")
    print(f"Y fim (Chegadas + Partidas): {y_chegadas_partidas}")

    if y_partidas and y_chegadas_partidas:
        # Recortar a região da tabela de Partidas
        bbox = (0, y_partidas, pagina.width, y_chegadas_partidas)
        regiao = pagina.crop(bbox)
        df_raw = regiao.extract_table()
Enter fullscreen mode Exit fullscreen mode

Etapa 2 — A lista de listas retornada pelo pdfplumber é convertida em um pd.DataFrame, tornando possível o uso de todas as ferramentas do pandas nas etapas seguintes.

df_raw = pd.DataFrame(df_raw)
print(f"Shape: {df_raw.shape}")
df_raw.head(10)
Enter fullscreen mode Exit fullscreen mode

Etapa 3 — Valores None e strings vazias " " — muito comuns em extrações de PDF — são uniformizados para NaN, garantindo que as verificações de nulidade com pd.isna() funcionem de forma consistente em todo o DataFrame.

df_raw = df_raw.replace({None: np.nan, '': np.nan})
Enter fullscreen mode Exit fullscreen mode

Etapa 4 — Como o cabeçalho da tabela é composto por múltiplas linhas espalhadas, esta etapa localiza cada informação individualmente:

  • Ano: encontra a linha que contém a string 'Ano' e extrai o valor numérico ao lado.
  • Mês: encontra a linha com 'Mês' e monta um dicionário {índice_coluna → nome_do_mês}, permitindo saber qual mês corresponde a cada bloco de colunas.
  • Dias: encontra a linha com 'Hora' (que contém os dias do mês como cabeçalho) e monta um dicionário {índice_coluna → número_do_dia}.
def encontrar_linha(df, valor):
    """Retorna índice da linha onde o valor aparece em qualquer coluna."""
    for i, row in df.iterrows():
        if valor in row.values:
            return i
    return None

# --- Ano ---
idx_ano = encontrar_linha(df_raw, 'Ano')
ano = None
if idx_ano is not None:
    row_ano = df_raw.iloc[idx_ano].dropna()
    for v in row_ano.values:
        try:
            ano = int(float(str(v)))
            break
        except ValueError:
            continue
print(f"Ano encontrado: {ano}")

# --- Mês ---
idx_mes = encontrar_linha(df_raw, 'Mês')
mapa_mes = {}
if idx_mes is not None:
    for col_idx, val in enumerate(df_raw.iloc[idx_mes]):
        if pd.notna(val) and str(val).strip() not in ('', 'Mês'):
            mapa_mes[col_idx] = str(val).strip()
print(f"Meses encontrados: {mapa_mes}")

# --- Dias (linha com 'Hora') ---
idx_hora = encontrar_linha(df_raw, 'Hora')
mapa_dia = {}
if idx_hora is not None:
    for col_idx, val in enumerate(df_raw.iloc[idx_hora]):
        if pd.notna(val) and str(val).strip() not in ('', 'Hora'):
            try:
                mapa_dia[col_idx] = int(float(str(val).replace(',', '.')))
            except ValueError:
                pass
print(f"Dias encontrados ({len(mapa_dia)}): {list(mapa_dia.values())}")
Enter fullscreen mode Exit fullscreen mode

Etapa 5 — Identifica automaticamente qual coluna contém os horários no formato HH:MM (usando regex) e filtra o DataFrame mantendo apenas as linhas que representam dados reais, descartando cabeçalhos, linhas em branco e metadados residuais.

def is_horario(val):
    return bool(re.match(r'^\d{1,2}:\d{2}$', str(val).strip()))

# Descobre qual coluna tem os horários (primeira coluna não-nula com HH:MM)
col_hora = None
for col in df_raw.columns:
    if df_raw[col].apply(is_horario).sum() > 5:
        col_hora = col
        break

print(f"Coluna de horários: {col_hora}")
df_dados = df_raw[df_raw[col_hora].apply(is_horario)].copy()
print(f"Linhas de dados encontradas: {len(df_dados)}")
Enter fullscreen mode Exit fullscreen mode

Etapa 6 — Converte a tabela do formato wide (colunas = dias, linhas = horas) para o formato long (um registro por combinação hora + dia). Para cada célula com valor válido, o código consulta os dicionários da Etapa 4 para descobrir o dia e o mês corretos. Volumes são limpos de formatação (pontos de milhar, vírgulas decimais) e convertidos para inteiro. O resultado é um DataFrame com as colunas: hora, dia, mes, ano, volume.

def _mes_para_coluna(col_idx, mapa_mes):
    mes_atual = None
    for idx in sorted(mapa_mes.keys()):
        if idx <= col_idx:
            mes_atual = mapa_mes[idx]
    return mes_atual

registros = []
for _, row in df_dados.iterrows():
    hora = str(row[col_hora]).strip()
    for col_idx, val in enumerate(row):
        if col_idx == col_hora:
            continue
        if pd.isna(val) or str(val).strip() in ('', 'nan'):
            continue
        dia = mapa_dia.get(col_idx)
        if dia is None:
            continue
        mes = _mes_para_coluna(col_idx, mapa_mes)
        try:
            volume = int(str(val).replace('.', '').replace(',', ''))
        except ValueError:
            continue
        registros.append({'hora': hora, 'dia': dia, 'mes': mes, 'ano': ano, 'volume': volume})

df_long = pd.DataFrame(registros)
print(f"Registros após melt: {len(df_long)}")
df_long.head()
Enter fullscreen mode Exit fullscreen mode

Etapa 7

  • mes_ref: converte o nome do mês em português para o número ordinal (ex: 'janeiro' → 1).
  • ano_ref: garante o ano como inteiro tipado (Int64).
  • turno: classifica cada hora em um dos quatro turnos do dia — Madrugada (00h–05h), Manhã (06h–11h), Tarde (12h–17h) e Noite (18h–23h).
  • data: monta uma data completa (datetime) a partir de ano, mês e dia, útil para ordenações e análises de série temporal.
MESES = {
    'janeiro':1,'fevereiro':2,'março':3,'abril':4,
    'maio':5,'junho':6,'julho':7,'agosto':8,
    'setembro':9,'outubro':10,'novembro':11,'dezembro':12
}

df_long['mes_ref'] = df_long['mes'].str.lower().map(MESES).astype('Int64')
df_long['ano_ref'] = df_long['ano'].astype('Int64')

hora_int = df_long['hora'].str.split(':').str[0].astype(int)
df_long['turno'] = np.select(
    [hora_int.between(0,5), hora_int.between(6,11),
     hora_int.between(12,17), hora_int.between(18,23)],
    ['Madrugada','Manhã','Tarde','Noite'],
    default='Indefinido'
)

df_long['data'] = pd.to_datetime(
    df_long[['ano_ref','mes_ref','dia']].rename(
        columns={'ano_ref':'year','mes_ref':'month','dia':'day'}
    ), errors='coerce'
)
Enter fullscreen mode Exit fullscreen mode

Etapa 8 - Consolida os registros agrupando por todas as dimensões analíticas (ano_ref, mes_ref, mes, dia, data, turno, hora) e somando os volumes. O resultado é ordenado cronologicamente por ano, mês, dia e hora, gerando o DataFrame final limpo, estruturado e pronto para carga ou análise.

df_final = (
    df_long
    .groupby(['ano_ref','mes_ref','mes','dia','data','turno','hora'], dropna=False)
    ['volume'].sum()
    .reset_index()
    .sort_values(['ano_ref','mes_ref','dia','hora'])
    .reset_index(drop=True)
)

print(f"DataFrame final: {df_final.shape}")
df_final.head(200)
Enter fullscreen mode Exit fullscreen mode

Resultado

Olha só que belezinha que fica:
Tabela final

o seu analista de BI receber essa tabelinha sempre limpa e no horário é o ápice 🫨.

Exportar

Agora exporte para algum diretório e seja feliz (nesse caso exportei para csv "Escolha o melhor diretório para o seu caso"):

df_final.to_csv("partidas_processado.csv", index=False)
Enter fullscreen mode Exit fullscreen mode

Resumo

Então é isso glr. Com uma simples engenharia de dados, da para transformar um trampo bem trabalhoso em algo automático. Caso a sua base pdf seja um pouco diferente, a lógica de transformação pode ser a mesma, alterando pequenos parâmetros.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.