# 🎯 Sistema de Captura Dinâmica de Colunas ## Visão Geral O scraper agora captura **automaticamente todas as colunas** encontradas nas tabelas do TibiaWiki, sem necessidade de especificar quais colunas buscar. ## 🔄 Como Funciona ### 1. Normalização Automática de Nomes Cada nome de coluna é normalizado para identificador SQL válido: | Coluna no Site | Campo no Banco | |----------------|----------------| | Nome | nome | | Lvl | lvl | | Voc | voc | | Mãos | maos | | Atq | atq | | Dano Elemental | dano_elemental | | Def | def | | Mod. de Defesa | mod_de_defesa | | Bônus | bonus | | Slots | slots | | Tier | tier | | Peso | peso | | Dropa de | dropa_de | **Regras de normalização:** - Remove acentos: `Nível` → `nivel` - Converte para lowercase: `Atq` → `atq` - Espaços viram underscores: `Dano Elemental` → `dano_elemental` - Remove caracteres especiais: `Mod. de Defesa` → `mod_de_defesa` - Garante nome válido SQL: não começa com número ### 2. Detecção Automática de Tipo O sistema tenta converter o valor para o tipo mais apropriado: ``` Valor no site → Tipo detectado → Salvo como "150" → INTEGER → 150 "10.5" → REAL → 10.5 "80 oz" → INTEGER → 80 "espada" → TEXT → "espada" "Fire damage" → TEXT → "Fire damage" ``` **Ordem de tentativa:** 1. Tenta converter para INTEGER 2. Se falhar, tenta REAL (float) 3. Se falhar, mantém como TEXT ### 3. Campos Especiais Alguns campos têm tratamento especial: | Campo | Tratamento | |-------|-----------| | **(sem nome)** | Imagem do item (`image_url`) | | **agrupável** | Tick/Cross → 1/0 | | **nome** | Sempre TEXT (chave primária) | | **categoria** | Adicionado automaticamente | | **image_url** | Extraído de colunas sem nome | | **image_local_path** | Preenchido após download | ### 4. Criação Dinâmica de Colunas Se a tabela já existe mas não tem uma coluna: - ✅ A coluna é adicionada automaticamente com `ALTER TABLE` - ✅ O tipo SQL é detectado baseado no valor - ✅ Log informa: "Coluna 'X' adicionada à tabela 'Y'" --- ## 📊 Exemplo Prático ### Tabela no Site: Espadas ``` +----------+-------+-----+-----+------+-----+----------------+-----+ | Nome | (img) | Lvl | Voc | Mãos | Atq | Dano Elemental | Def | +----------+-------+-----+-----+------+-----+----------------+-----+ | Broadsword | [🖼️] | 0 | - | 1 | 30 | - | 5 | +----------+-------+-----+-----+------+-----+----------------+-----+ ``` ### Banco de Dados Resultante ```sql CREATE TABLE espadas ( id INTEGER PRIMARY KEY AUTOINCREMENT, categoria TEXT, nome TEXT UNIQUE NOT NULL, image_url TEXT, image_local_path TEXT, lvl INTEGER, -- detectado automaticamente voc TEXT, -- detectado automaticamente maos INTEGER, -- detectado automaticamente atq INTEGER, -- detectado automaticamente dano_elemental TEXT, -- detectado automaticamente def INTEGER -- detectado automaticamente ); ``` ### Registro Inserido ```sql INSERT INTO espadas VALUES ( 1, -- id (auto) 'Espadas', -- categoria 'Broadsword', -- nome 'https://...gif', -- image_url 'data/images/...', -- image_local_path 0, -- lvl '-', -- voc 1, -- maos 30, -- atq '-', -- dano_elemental 5 -- def ); ``` --- ## ✅ Vantagens ### 1. Flexibilidade Total - ✅ Funciona com qualquer categoria - ✅ Não precisa atualizar código para novas colunas - ✅ Adapta-se a mudanças no site ### 2. Manutenção Zero - ✅ Novas colunas aparecem automaticamente - ✅ Tabelas antigas recebem novas colunas - ✅ Tipos de dados corretos ### 3. Dados Completos - ✅ Nenhuma informação é perdida - ✅ Todas as colunas são capturadas - ✅ Valores vazios são tratados (NULL) --- ## 🔍 Exemplos de Normalização ### Colunas Comuns ```python normalize_column_name("Nome") # → "nome" normalize_column_name("Lvl") # → "lvl" normalize_column_name("Nível") # → "nivel" normalize_column_name("Voc") # → "voc" normalize_column_name("Mãos") # → "maos" normalize_column_name("Atq") # → "atq" normalize_column_name("Dano Elemental") # → "dano_elemental" normalize_column_name("Def") # → "def" normalize_column_name("Mod. de Defesa") # → "mod_de_defesa" normalize_column_name("Bônus") # → "bonus" normalize_column_name("Slots") # → "slots" normalize_column_name("Tier") # → "tier" normalize_column_name("Peso") # → "peso" normalize_column_name("Dropa de") # → "dropa_de" ``` ### Casos Especiais ```python normalize_column_name("") # → "coluna_sem_nome" normalize_column_name("123abc") # → "col_123abc" normalize_column_name("@#$%") # → "coluna_sem_nome" normalize_column_name("HP/MP") # → "hp_mp" ``` --- ## 🔄 Detecção de Tipo Detalhada ### Exemplos de Conversão ```python _detect_and_convert_value("150") # → 150 (int) _detect_and_convert_value("10.5") # → 10.5 (float) _detect_and_convert_value("80 oz") # → 80 (int) _detect_and_convert_value("3.50 oz") # → 3.5 (float) _detect_and_convert_value("espada") # → "espada" (text) _detect_and_convert_value("Fire damage") # → "Fire damage" (text) _detect_and_convert_value("-") # → "-" (text) _detect_and_convert_value("") # → None _detect_and_convert_value(" ") # → None ``` ### Lógica de Detecção ```python def _detect_and_convert_value(text): if not text or text.strip() == '': return None # 1. Tenta INT int_value = parse_int(text) # "150" → 150, "80 oz" → 80 if int_value is not None: return int_value # 2. Tenta FLOAT float_value = parse_float(text) # "10.5" → 10.5, "3,50" → 3.5 if float_value is not None: return float_value # 3. Retorna TEXT return text.strip() ``` --- ## ⚠️ Casos Especiais ### 1. Coluna Sem Nome (Imagem) ```html ``` **Comportamento:** - ✅ Não cria campo no banco - ✅ Extrai imagem → `image_url` - ✅ Ignora Tick/Cross ### 2. Coluna "Agrupável" ```html Agrup. ``` **Comportamento:** - ✅ Tick.png → `agrupavel = 1` - ✅ Cross.png → `agrupavel = 0` - ✅ "Sim" → `agrupavel = 1` - ✅ "Não" → `agrupavel = 0` ### 3. Campos Reservados Estes campos não são sobrescritos: - `id` (auto increment) - `categoria` (adicionado pelo script) - `image_url` (extraído da coluna sem nome) - `image_local_path` (após download) --- ## 📝 Logs e Monitoramento ### Log de Nova Coluna ``` INFO: Coluna 'voc' adicionada à tabela 'espadas' INFO: Coluna 'dano_elemental' adicionada à tabela 'espadas' ``` ### Verificar Colunas no Banco ```sql -- Ver estrutura da tabela PRAGMA table_info(espadas); -- Resultado: -- cid | name | type | notnull | dflt_value | pk -- 0 | id | INTEGER | 0 | NULL | 1 -- 1 | categoria | TEXT | 0 | NULL | 0 -- 2 | nome | TEXT | 1 | NULL | 0 -- 3 | lvl | INTEGER | 0 | NULL | 0 -- 4 | voc | TEXT | 0 | NULL | 0 -- 5 | maos | INTEGER | 0 | NULL | 0 -- 6 | atq | INTEGER | 0 | NULL | 0 -- 7 | dano_elemental | TEXT | 0 | NULL | 0 -- ... ``` --- ## 🎯 Casos de Uso ### Caso 1: Nova Categoria com Colunas Diferentes Se você processar "Armaduras" que tem colunas diferentes de "Espadas": ``` Armaduras: Nome, Lvl, Voc, Defesa, Peso ``` **Resultado:** - ✅ Tabela `armaduras` criada automaticamente - ✅ Colunas: `nome`, `lvl`, `voc`, `defesa`, `peso` - ✅ Tipos detectados automaticamente ### Caso 2: Site Adiciona Nova Coluna TibiaWiki adiciona coluna "Raridade": **Próxima execução:** - ✅ Coluna `raridade` adicionada automaticamente - ✅ Itens antigos: `raridade = NULL` - ✅ Itens novos: `raridade = "Comum"` (ou valor) ### Caso 3: Reprocessar Categoria Você roda novamente para "Espadas": **Comportamento:** - ✅ Tabela já existe: não recria - ✅ Colunas novas: adicionadas - ✅ Dados existentes: atualizados (UPSERT por nome) --- ## 🔧 Código Técnico ### Função de Normalização ```python def normalize_column_name(column_name): """ Normaliza nome de coluna para identificador SQL válido """ if not column_name: return "" # Remove acentos nfd = unicodedata.normalize('NFD', column_name) name_no_accent = ''.join(c for c in nfd if unicodedata.category(c) != 'Mn') # Converte para lowercase normalized = name_no_accent.lower() # Substitui caracteres não alfanuméricos por underscore normalized = re.sub(r'[^a-z0-9]+', '_', normalized) # Remove underscores do início e fim normalized = normalized.strip('_') # Garante que não começa com número if normalized and normalized[0].isdigit(): normalized = 'col_' + normalized # Se ficou vazio, retorna um nome padrão if not normalized: return "coluna_sem_nome" return normalized ``` ### Lógica de Processamento ```python # Para cada célula da tabela for cell_idx, cell in enumerate(cells): header = headers[cell_idx] header_clean = clean_text(header) cell_text = clean_text(cell.get_text(strip=True)) # 1. Coluna sem nome → imagem if not header_clean: extract_image(cell) continue # 2. Coluna "agrupável" → Tick/Cross if 'agrup' in header_clean.lower(): handle_tick_cross(cell) continue # 3. Demais colunas → captura automática field_name = normalize_column_name(header_clean) if field_name == 'nome': item_data['nome'] = cell_text.strip() else: value = _detect_and_convert_value(cell_text) if value is not None and value != '': item_data[field_name] = value ``` --- ## ✅ Checklist de Funcionamento - [x] Captura todas as colunas automaticamente - [x] Normaliza nomes para SQL válido - [x] Detecta tipos automaticamente (int/float/text) - [x] Adiciona colunas dinamicamente a tabelas existentes - [x] Preserva campos especiais (nome, categoria, image_url) - [x] Ignora Tick/Cross como imagens - [x] Trata valores vazios (NULL) - [x] Logs informativos - [x] Compatível com tabelas existentes - [x] Funciona com qualquer categoria --- ## 🎉 Resultado Final Agora o scraper é **completamente dinâmico**: - ✅ Nenhuma coluna é perdida - ✅ Nenhum código precisa ser alterado - ✅ Funciona com qualquer estrutura de tabela - ✅ Adapta-se automaticamente a mudanças **Antes:** 8 colunas fixas (nome, nivel, peso, defesa, slots, ataque, alcance, descricao) **Agora:** Todas as colunas da tabela (ilimitado, dinâmico)