Added vocation state and included it in query string builder, updated onFilter to accept a vocation parameter, improved grid layout with grid flow classes, normalized category names for consistent item icons, and refined stats mapping to handle type safety.
411 lines
11 KiB
Markdown
411 lines
11 KiB
Markdown
# 🎯 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
|
|
<th></th> <!-- Coluna sem nome -->
|
|
```
|
|
|
|
**Comportamento:**
|
|
- ✅ Não cria campo no banco
|
|
- ✅ Extrai imagem → `image_url`
|
|
- ✅ Ignora Tick/Cross
|
|
|
|
### 2. Coluna "Agrupável"
|
|
|
|
```html
|
|
<th>Agrup.</th>
|
|
```
|
|
|
|
**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)
|
|
|