Fase 2 — Wrangling avançado | Pré-requisitos: Aulas 0 a 8 | Duração estimada: 90 minutos
Introdução
Coleta é a primeira fase do wrangling. Antes de limpar, avaliar ou analisar qualquer coisa, você precisa trazer os dados para o seu ambiente de trabalho. Na prática, isso significa lidar com fontes e formatos muito diferentes: arquivos locais em formatos variados, bancos de dados relacionais, serviços web que expõem dados via API, e páginas HTML quando não há outra opção.
Esta aula cobre as quatro principais fontes de dados que um analista encontra no dia a dia. Para cada uma, o objetivo não é cobrir todos os parâmetros possíveis, mas entender a lógica da fonte e os problemas típicos que aparecem na coleta.
Arquivos CSV e variantes
A Aula 3 introduziu pd.read_csv() com os parâmetros básicos. Aqui aprofundamos os casos que causam mais problemas na prática.
Codificação de caracteres
Codificação é a forma como caracteres são armazenados em bytes. O padrão moderno é UTF-8, que suporta todos os caracteres de todas as línguas. Mas arquivos gerados por sistemas legados, especialmente no Brasil, frequentemente usam latin-1 (também chamado ISO-8859-1) ou cp1252 (Windows-1252).
import pandas as pd
# Tentativa com UTF-8 (padrão)
try:
df = pd.read_csv('arquivo.csv', encoding='utf-8')
except UnicodeDecodeError:
# Se falhar, tentar latin-1
df = pd.read_csv('arquivo.csv', encoding='latin-1')
Quando você não sabe a codificação, a biblioteca chardet detecta automaticamente:
import chardet
with open('arquivo.csv', 'rb') as f:
resultado = chardet.detect(f.read())
print(resultado)
# {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''}
df = pd.read_csv('arquivo.csv', encoding=resultado['encoding'])
Separadores não convencionais
Arquivos CSV brasileiros frequentemente usam ponto e vírgula como separador porque a vírgula é usada como separador decimal. Arquivos de outros sistemas podem usar tabulação, pipe ou outros caracteres.
# Ponto e vírgula (comum no Brasil)
df = pd.read_csv('arquivo.csv', sep=';')
# Tabulação
df = pd.read_csv('arquivo.csv', sep='\t')
# ou equivalentemente:
df = pd.read_table('arquivo.csv')
# Pipe
df = pd.read_csv('arquivo.csv', sep='|')
# Separador com mais de um caractere (usa regex)
df = pd.read_csv('arquivo.csv', sep='\s+', engine='python')
Cabeçalhos e metadados
Alguns arquivos têm linhas de metadados antes do cabeçalho real, ou não têm cabeçalho nenhum:
# Pular as 3 primeiras linhas antes do cabeçalho
df = pd.read_csv('arquivo.csv', skiprows=3)
# Sem cabeçalho — pandas gera nomes numéricos (0, 1, 2...)
df = pd.read_csv('arquivo.csv', header=None)
# Sem cabeçalho e fornecendo nomes manualmente
df = pd.read_csv('arquivo.csv', header=None,
names=['id', 'nome', 'data', 'valor'])
# Cabeçalho na segunda linha (índice 1)
df = pd.read_csv('arquivo.csv', header=1)
Leitura eficiente de arquivos grandes
Para arquivos que não cabem na memória, duas estratégias:
# Carregar apenas uma amostra
df_amostra = pd.read_csv('arquivo_grande.csv', nrows=10000)
# Carregar apenas colunas necessárias
df = pd.read_csv('arquivo_grande.csv',
usecols=['data', 'produto', 'valor'])
# Processar em chunks (pedaços)
chunks = []
for chunk in pd.read_csv('arquivo_grande.csv', chunksize=50000):
# Processar cada chunk — por exemplo, filtrar
chunk_filtrado = chunk[chunk['valor'] > 100]
chunks.append(chunk_filtrado)
df = pd.concat(chunks, ignore_index=True)
# Especificar tipos para economizar memória
df = pd.read_csv('arquivo.csv', dtype={
'id': 'int32', # int32 usa metade da memória de int64
'categoria': 'category', # category economiza memória em colunas repetitivas
'valor': 'float32'
})
Arquivos JSON
JSON (JavaScript Object Notation) é o formato padrão para troca de dados na web. É mais flexível que CSV porque suporta estruturas aninhadas — objetos dentro de objetos, arrays de objetos — mas por isso é mais difícil de transformar em tabelas.
# JSON simples (array de objetos no nível raiz)
df = pd.read_json('dados.json')
# JSON com caminho específico (orient define a estrutura)
df = pd.read_json('dados.json', orient='records')
# Carregar JSON de uma URL diretamente
df = pd.read_json('https://api.exemplo.com/dados.json')
JSON aninhado
O problema mais comum com JSON é o aninhamento. Quando um campo contém um objeto ou array em vez de um valor simples, read_json cria uma coluna com dicionários ou listas como valores — o que não é útil para análise.
import json
import pandas as pd
# Exemplo de JSON aninhado
dados_json = '''
[
{
"id": 1,
"nome": "Alice",
"endereco": {
"cidade": "São Paulo",
"estado": "SP",
"cep": "01310-100"
},
"pedidos": [101, 102, 105]
},
{
"id": 2,
"nome": "Bruno",
"endereco": {
"cidade": "Rio de Janeiro",
"estado": "RJ",
"cep": "20040-020"
},
"pedidos": [201]
}
]
'''
dados = json.loads(dados_json)
# pd.json_normalize achata estruturas aninhadas
df = pd.json_normalize(
dados,
sep='_' # separador para campos aninhados: endereco_cidade, endereco_estado
)
print(df.columns.tolist())
# ['id', 'nome', 'pedidos', 'endereco_cidade', 'endereco_estado', 'endereco_cep']
# Para achatar arrays aninhados (como 'pedidos'), use record_path
df_pedidos = pd.json_normalize(
dados,
record_path='pedidos',
meta=['id', 'nome'],
meta_prefix='cliente_'
)
print(df_pedidos)
# 0 cliente_id cliente_nome
# 0 101 1 Alice
# 1 102 1 Alice
# 2 105 1 Alice
# 3 201 2 Bruno
Bancos de dados relacionais
Conectar Python a bancos de dados é uma habilidade essencial para quem trabalha em empresas onde os dados estão em sistemas de produção. A biblioteca padrão para isso é SQLAlchemy, que fornece uma interface uniforme para diferentes bancos de dados.
from sqlalchemy import create_engine
import pandas as pd
# Strings de conexão para diferentes bancos de dados
# SQLite (arquivo local, ótimo para desenvolvimento)
engine = create_engine('sqlite:///banco.db')
# PostgreSQL
engine = create_engine('postgresql://usuario:senha@host:5432/nome_banco')
# MySQL
engine = create_engine('mysql+pymysql://usuario:senha@host:3306/nome_banco')
# SQL Server
engine = create_engine('mssql+pyodbc://usuario:senha@host/nome_banco?driver=ODBC+Driver+17+for+SQL+Server')
Com a conexão estabelecida, pd.read_sql() executa uma query e retorna um DataFrame:
# Query completa
df = pd.read_sql(
'SELECT * FROM vendas WHERE ano = 2024',
engine
)
# Query com parâmetros (use sempre parâmetros em vez de f-strings para evitar SQL injection)
ano = 2024
df = pd.read_sql(
'SELECT * FROM vendas WHERE ano = :ano',
engine,
params={'ano': ano}
)
# Ler uma tabela inteira
df = pd.read_sql_table('vendas', engine)
# Query complexa com joins
query = """
SELECT
p.id_pedido,
p.data_pedido,
c.nome AS cliente,
c.cidade,
SUM(i.valor * i.quantidade) AS valor_total
FROM pedidos p
JOIN clientes c ON p.id_cliente = c.id_cliente
JOIN itens_pedido i ON p.id_pedido = i.id_pedido
WHERE p.status = 'entregue'
GROUP BY p.id_pedido, p.data_pedido, c.nome, c.cidade
ORDER BY p.data_pedido DESC
"""
df = pd.read_sql(query, engine)
SQLite para desenvolvimento
SQLite é um banco de dados que existe como um único arquivo, sem servidor. É ideal para desenvolvimento, testes e análises que precisam de um banco de dados mas não têm infraestrutura.
import sqlite3
import pandas as pd
# Criar ou conectar a um banco SQLite
conn = sqlite3.connect('analise.db')
# Carregar um DataFrame para uma tabela SQLite
df.to_sql('vendas', conn, if_exists='replace', index=False)
# Fazer queries
df_query = pd.read_sql('SELECT * FROM vendas LIMIT 100', conn)
conn.close()
APIs REST
Uma API REST (Application Programming Interface) é um serviço web que fornece dados sob demanda via HTTP. É a forma como a maioria dos serviços modernos disponibiliza dados programaticamente: Twitter, GitHub, IBGE, bancos, marketplaces, serviços meteorológicos.
A biblioteca requests é o padrão para fazer requisições HTTP em Python:
pip install requests
Estrutura básica de uma requisição
import requests
import pandas as pd
# GET request simples
resposta = requests.get('https://api.exemplo.com/dados')
# Verificar o status da resposta
print(resposta.status_code)
# 200 = sucesso
# 400 = erro do cliente (parâmetros inválidos)
# 401 = não autorizado (precisa de autenticação)
# 404 = recurso não encontrado
# 429 = muitas requisições (rate limit)
# 500 = erro do servidor
# Obter os dados em JSON
dados = resposta.json()
# Converter para DataFrame
df = pd.DataFrame(dados)
Parâmetros de query
A maioria das APIs aceita parâmetros que filtram ou configuram a resposta:
# Parâmetros passados como dicionário (mais seguro e legível que concatenar na URL)
params = {
'data_inicio': '2024-01-01',
'data_fim': '2024-12-31',
'estado': 'SP',
'limite': 1000
}
resposta = requests.get(
'https://api.dados.gov.br/v1/indicadores',
params=params
)
# requests constrói automaticamente a URL com os parâmetros:
# https://api.dados.gov.br/v1/indicadores?data_inicio=2024-01-01&...
Autenticação
APIs privadas exigem autenticação. Os métodos mais comuns são:
# API Key no header (mais comum)
headers = {
'Authorization': 'Bearer SEU_TOKEN_AQUI',
'Content-Type': 'application/json'
}
resposta = requests.get(url, headers=headers)
# API Key como parâmetro de query
resposta = requests.get(url, params={'api_key': 'SUA_CHAVE'})
# Autenticação básica (usuário e senha)
resposta = requests.get(url, auth=('usuario', 'senha'))
Nunca coloque chaves de API diretamente no código. Use variáveis de ambiente:
import os
api_key = os.environ.get('MINHA_API_KEY')
headers = {'Authorization': f'Bearer {api_key}'}
Paginação
APIs raramente retornam todos os dados de uma vez. O padrão é retornar uma página por vez e você precisa paginar para coletar tudo:
import requests
import pandas as pd
import time
def coletar_todos_os_dados(url_base, params_base, campo_dados='results'):
"""
Coleta todas as páginas de uma API paginada.
Assume que a API retorna um campo 'next' com a URL da próxima página.
"""
todos_os_dados = []
url = url_base
params = params_base.copy()
pagina = 1
while url:
print(f"Coletando página {pagina}...")
resposta = requests.get(url, params=params)
if resposta.status_code != 200:
print(f"Erro {resposta.status_code} na página {pagina}")
break
dados = resposta.json()
todos_os_dados.extend(dados.get(campo_dados, []))
# Próxima página (None se não houver)
url = dados.get('next')
params = {} # parâmetros já estão na URL 'next'
pagina += 1
# Respeitar o rate limit da API
time.sleep(0.5)
return pd.DataFrame(todos_os_dados)
# Exemplo com a API do IBGE (dados de municípios)
df_municipios = coletar_todos_os_dados(
url_base='https://servicodados.ibge.gov.br/api/v1/localidades/municipios',
params_base={},
campo_dados=None # a API do IBGE retorna uma lista diretamente
)
Exemplo real: API do IBGE
O IBGE disponibiliza uma API pública sem autenticação:
import requests
import pandas as pd
# Lista de estados brasileiros
resposta = requests.get(
'https://servicodados.ibge.gov.br/api/v1/localidades/estados'
)
estados = resposta.json()
df_estados = pd.DataFrame(estados)
print(df_estados[['id', 'sigla', 'nome']].head())
# Dados do censo por estado
resposta = requests.get(
'https://servicodados.ibge.gov.br/api/v3/agregados/6579/periodos/2022/variaveis/9324',
params={'localidades': 'N3[all]'} # N3 = estados
)
dados_censo = resposta.json()
Tratamento de erros em requisições
APIs falham. Connections caem. Rate limits são atingidos. Código de coleta robusto trata esses casos:
import requests
import time
def requisicao_com_retry(url, params=None, headers=None, max_tentativas=3):
"""
Faz uma requisição GET com retry automático em caso de falha.
"""
for tentativa in range(max_tentativas):
try:
resposta = requests.get(
url,
params=params,
headers=headers,
timeout=30 # timeout de 30 segundos
)
resposta.raise_for_status() # lança exceção para status 4xx e 5xx
return resposta
except requests.exceptions.Timeout:
print(f"Timeout na tentativa {tentativa + 1}")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
# Rate limit: esperar antes de tentar de novo
wait_time = int(e.response.headers.get('Retry-After', 60))
print(f"Rate limit atingido. Aguardando {wait_time}s...")
time.sleep(wait_time)
else:
print(f"Erro HTTP {e.response.status_code}: {e}")
break
except requests.exceptions.ConnectionError:
print(f"Erro de conexão na tentativa {tentativa + 1}")
time.sleep(5)
return None
Web scraping básico
Web scraping é a extração de dados de páginas HTML quando não há API disponível. Deve ser a última opção, não a primeira, porque é mais frágil (uma mudança no layout da página quebra o código), mais trabalhosa e pode violar os termos de serviço do site.
Antes de fazer scraping, verifique: o site tem uma API? Os dados estão disponíveis para download? O arquivo robots.txt do site permite scraping? Os termos de serviço proíbem coleta automatizada?
As bibliotecas principais são requests para baixar o HTML e BeautifulSoup para parsear e extrair dados:
pip install beautifulsoup4 lxml
Estrutura básica
import requests
from bs4 import BeautifulSoup
import pandas as pd
# Baixar a página
headers = {
'User-Agent': 'Mozilla/5.0 (compatible; análise acadêmica)'
}
resposta = requests.get('https://exemplo.com/tabela', headers=headers)
resposta.raise_for_status()
# Parsear o HTML
soup = BeautifulSoup(resposta.text, 'lxml')
# Encontrar elementos por tag, classe ou id
titulo = soup.find('h1')
print(titulo.text.strip())
# Todos os elementos de uma tag
paragrafos = soup.find_all('p')
for p in paragrafos:
print(p.text.strip())
# Por classe CSS
elemento = soup.find('div', class_='conteudo-principal')
# Por id
elemento = soup.find('div', id='tabela-resultados')
# Navegação na estrutura
tabela = soup.find('table')
linhas = tabela.find_all('tr')
Extraindo tabelas HTML
pandas tem uma função que extrai tabelas HTML diretamente, sem precisar usar BeautifulSoup para tabelas simples:
# pd.read_html retorna uma lista de todos os DataFrames encontrados na página
tabelas = pd.read_html('https://exemplo.com/pagina-com-tabelas')
print(f"Encontradas {len(tabelas)} tabelas")
df = tabelas[0] # primeira tabela encontrada
Para páginas mais complexas onde read_html não funciona bem:
import requests
from bs4 import BeautifulSoup
import pandas as pd
resposta = requests.get('https://exemplo.com/tabela')
soup = BeautifulSoup(resposta.text, 'lxml')
tabela = soup.find('table', id='tabela-principal')
# Extrair cabeçalho
cabecalho = [th.text.strip() for th in tabela.find('tr').find_all('th')]
# Extrair linhas de dados
linhas = []
for tr in tabela.find_all('tr')[1:]: # pular a linha de cabeçalho
celulas = [td.text.strip() for td in tr.find_all('td')]
if celulas: # ignorar linhas vazias
linhas.append(celulas)
df = pd.DataFrame(linhas, columns=cabecalho)
Boas práticas de scraping
Sempre adicione delays entre requisições para não sobrecarregar o servidor:
import time
import random
urls = ['https://exemplo.com/pagina/1', 'https://exemplo.com/pagina/2']
dados = []
for url in urls:
resposta = requests.get(url)
# processar resposta...
dados.append(...)
# Esperar entre 1 e 3 segundos antes da próxima requisição
time.sleep(random.uniform(1, 3))
Salve os dados brutos antes de processar. Se o processamento falhar, você não precisa coletar de novo:
import json
# Salvar resposta bruta
with open('dados_brutos.json', 'w', encoding='utf-8') as f:
json.dump(dados_coletados, f, ensure_ascii=False, indent=2)
# Carregar dados salvos (para reprocessar sem coletar de novo)
with open('dados_brutos.json', 'r', encoding='utf-8') as f:
dados_coletados = json.load(f)
Combinando múltiplas fontes
Na prática, uma análise frequentemente combina dados de fontes diferentes. O processo é sempre o mesmo: coletar cada fonte separadamente, inspecionar cada uma, e então combinar com cuidado.
import requests
import pandas as pd
from sqlalchemy import create_engine
# Fonte 1: banco de dados interno
engine = create_engine('postgresql://usuario:senha@host/banco')
df_pedidos = pd.read_sql('SELECT * FROM pedidos WHERE ano = 2024', engine)
# Fonte 2: API externa de CEPs
def buscar_dados_cep(cep):
resposta = requests.get(f'https://viacep.com.br/ws/{cep}/json/')
if resposta.status_code == 200:
return resposta.json()
return {}
# Coletar dados de CEP para cada pedido (com cache para evitar requisições repetidas)
ceps_unicos = df_pedidos['cep_entrega'].unique()
dados_cep = {}
for cep in ceps_unicos:
cep_limpo = cep.replace('-', '')
dados_cep[cep] = buscar_dados_cep(cep_limpo)
time.sleep(0.2)
df_ceps = pd.DataFrame(dados_cep).T.reset_index()
df_ceps.columns = ['cep_entrega'] + list(df_ceps.columns[1:])
# Fonte 3: arquivo CSV com metas de vendas
df_metas = pd.read_csv('metas_2024.csv', sep=';', encoding='latin-1')
# Combinar as três fontes
df_final = (df_pedidos
.merge(df_ceps[['cep_entrega', 'localidade', 'uf']], on='cep_entrega', how='left')
.merge(df_metas, on=['mes', 'regiao'], how='left')
)
Resumo
Coleta de dados envolve quatro fontes principais. Arquivos CSV requerem atenção a codificação, separadores e cabeçalhos, com estratégias de chunking para arquivos grandes. JSON aninhado precisa ser achatado com pd.json_normalize() antes de ser usável. Bancos de dados são acessados via SQLAlchemy com pd.read_sql(), usando parâmetros em vez de f-strings para evitar SQL injection. APIs REST são consumidas com requests, com tratamento de paginação, autenticação e erros. Web scraping com BeautifulSoup é a última opção e requer delays entre requisições, verificação dos termos de serviço e salvamento dos dados brutos. Todo dado coletado deve ser salvo antes de qualquer processamento para permitir re-execução sem nova coleta.
Exercícios
-
Por que nunca se deve construir queries SQL concatenando strings com f-strings, e qual é a alternativa correta?
✓ Resposta:Construir queries SQL com f-strings cria vulnerabilidade de SQL injection. Se o valor inserido na f-string vier de uma entrada externa — um parâmetro de API, um campo de formulário, um valor de arquivo — um usuário mal-intencionado pode inserir código SQL que modifica ou destrói o banco de dados.
Exemplo vulnerável:
# NUNCA FAÇA ISSO usuario = input("Digite o nome: ") query = f"SELECT * FROM clientes WHERE nome = '{usuario}'"Se o usuário digitar
' OR '1'='1, a query se tornaSELECT * FROM clientes WHERE nome = '' OR '1'='1', que retorna todos os clientes. Se digitar'; DROP TABLE clientes; --, pode destruir a tabela.A alternativa correta é usar parâmetros:
# CORRETO query = "SELECT * FROM clientes WHERE nome = :nome" df = pd.read_sql(query, engine, params={'nome': usuario})A biblioteca cuida de escapar os valores corretamente, tornando SQL injection impossível. Esse princípio se aplica a qualquer valor que vem de fora do código — não apenas entradas de usuário, mas também valores de arquivos, APIs e variáveis de ambiente.
-
Você está coletando dados de uma API que retorna 100 registros por página e tem um total de 50.000 registros. A API tem um rate limit de 60 requisições por minuto. Quanto tempo mínimo levará a coleta completa e como você estruturaria o código para respeitar o rate limit?
✓ Resposta:O número total de requisições necessárias é 50.000 / 100 = 500 requisições. Com um rate limit de 60 requisições por minuto, o tempo mínimo é 500 / 60 ≈ 8,3 minutos, ou seja, pelo menos 9 minutos completos.
Para respeitar o rate limit, a estratégia mais simples é adicionar um delay fixo entre requisições:
import requests import pandas as pd import time def coletar_api_paginada(url_base, token, total_por_pagina=100): todos = [] pagina = 1 headers = {'Authorization': f'Bearer {token}'} # delay mínimo para não ultrapassar 60 req/min: 60/60 = 1 segundo delay_entre_requisicoes = 1.1 # margem de segurança while True: params = {'page': pagina, 'per_page': total_por_pagina} resposta = requests.get(url_base, headers=headers, params=params) if resposta.status_code == 429: # Rate limit atingido de forma inesperada retry_after = int(resposta.headers.get('Retry-After', 60)) print(f"Rate limit. Aguardando {retry_after}s...") time.sleep(retry_after) continue resposta.raise_for_status() dados = resposta.json() if not dados: break todos.extend(dados) print(f"Página {pagina}: {len(dados)} registros coletados") pagina += 1 time.sleep(delay_entre_requisicoes) return pd.DataFrame(todos)Uma melhoria adicional é salvar o progresso periodicamente para que, se a coleta for interrompida, ela possa ser retomada da última página salva em vez de recomeçar do zero.
-
Qual a diferença entre
pd.read_json()epd.json_normalize()? Quando cada um é mais adequado?✓ Resposta:pd.read_json()é adequado para JSON com estrutura flat ou levemente aninhada, onde o arquivo representa diretamente um array de objetos com campos simples. Ele carrega o JSON e tenta criar um DataFrame diretamente, mas quando encontra campos aninhados, os armazena como dicionários ou listas nas células — o que raramente é o que você quer para análise.pd.json_normalize()é adequado para JSON com estrutura aninhada. Ele achata hierarquias de objetos em colunas separadas usando um separador configurável, e pode desdobrar arrays aninhados em múltiplas linhas. É mais verboso mas muito mais poderoso para JSON complexo.Exemplo concreto: um JSON com estrutura
{"nome": "Ana", "endereco": {"cidade": "SP", "estado": "SP"}}tratado com cada método:import json import pandas as pd dados = [{"nome": "Ana", "endereco": {"cidade": "SP", "estado": "SP"}}, {"nome": "Bruno", "endereco": {"cidade": "RJ", "estado": "RJ"}}] # read_json: 'endereco' vira uma coluna com dicionários df1 = pd.read_json(json.dumps(dados)) print(df1['endereco']) # 0 {'cidade': 'SP', 'estado': 'SP'} # 1 {'cidade': 'RJ', 'estado': 'RJ'} # json_normalize: achata o aninhamento em colunas separadas df2 = pd.json_normalize(dados, sep='_') print(df2.columns.tolist()) # ['nome', 'endereco_cidade', 'endereco_estado']Use
read_jsonpara JSON simples e rápido. Usejson_normalizesempre que houver aninhamento que precisa ser achatado em colunas. -
Escreva o código para coletar a lista de todos os municípios do estado de São Paulo usando a API pública do IBGE, convertê-la para um DataFrame com as colunas
id,nomeemicrorregiao, e salvar o resultado em CSV.✓ Resposta:import requests import pandas as pd # Coletar municípios de SP (código do estado SP = 35) url = 'https://servicodados.ibge.gov.br/api/v1/localidades/estados/35/municipios' resposta = requests.get(url) resposta.raise_for_status() municipios = resposta.json() # A resposta tem estrutura aninhada: # [{"id": 3500105, "nome": "Adamantina", # "microrregiao": {"id": 35038, "nome": "Adamantina", ...}, # "mesorregiao": {...}}, ...] df = pd.json_normalize(municipios, sep='_') # Selecionar e renomear colunas de interesse df_final = df[['id', 'nome', 'microrregiao_nome']].copy() df_final.columns = ['id', 'nome', 'microrregiao'] print(f"Total de municípios: {len(df_final)}") print(df_final.head(10)) # Salvar em CSV df_final.to_csv('municipios_sp.csv', index=False, encoding='utf-8') print("Arquivo salvo: municipios_sp.csv") -
Quais são as três perguntas que você deve responder antes de fazer web scraping em um site, e por que cada uma importa?
✓ Resposta:Primeira pergunta: o site tem uma API ou disponibiliza os dados para download? Se sim, use a API ou o download. APIs são mais estáveis que HTML, mais fáceis de parsear, mais eficientes e geralmente permitidas explicitamente pelos termos de serviço. Fazer scraping quando uma API existe é trabalho desnecessário.
Segunda pergunta: o arquivo
robots.txtdo site permite scraping nas páginas que você precisa? O arquivorobots.txt(acessível emhttps://site.com/robots.txt) especifica quais partes do site os robôs automatizados podem ou não acessar. Violar orobots.txtnão é ilegal na maioria das jurisdições, mas é uma violação das convenções da web e pode resultar em bloqueio do seu IP ou em ação legal dependendo dos termos de serviço.Terceira pergunta: os termos de serviço do site proíbem coleta automatizada? Muitos sites incluem cláusulas nos termos de uso proibindo scraping, especialmente para uso comercial. Violar os termos de serviço pode resultar em ação legal, como já aconteceu em casos envolvendo LinkedIn, Craigslist e outros. Para dados públicos governamentais, essa restrição raramente existe. Para dados de plataformas privadas, é essencial verificar.
Uma quarta pergunta, não menos importante: você está sobrecarregando o servidor com suas requisições? Mesmo quando scraping é permitido, fazer centenas de requisições por segundo pode constituir um ataque de negação de serviço involuntário. Delays entre requisições são uma questão de ética, não apenas de boas práticas técnicas.
Referências
- Documentação do pandas — IO tools: pandas.pydata.org/docs/user_guide/io.html Referência completa para todos os formatos de arquivo, incluindo parâmetros avançados de `read_csv` e `read_json`.
- Documentação do requests: docs.python-requests.org. A biblioteca padrão para HTTP em Python, com guia completo de autenticação, sessões, proxies e tratamento de erros.
- Documentação do SQLAlchemy: docs.sqlalchemy.org. Referência completa para conexão com bancos de dados relacionais de diferentes fornecedores.
- Richardson, Leonard. Beautiful Soup Documentation: beautiful-soup-4.readthedocs.io. Documentação oficial do BeautifulSoup com exemplos de todos os métodos de busca e navegação.
- Mitchell, Ryan. Web Scraping with Python, 2ª edição. O'Reilly, 2018. O livro de referência para web scraping em Python, cobrindo BeautifulSoup, Selenium para páginas dinâmicas e considerações legais e éticas.
- API do IBGE — Serviços de Dados: servicodados.ibge.gov.br/api/docs Documentação completa da API pública do IBGE com exemplos de todas as rotas disponíveis.
- ViaCEP API: viacep.com.br. API pública gratuita para consulta de CEPs brasileiros, sem autenticação necessária.