feat(library): add vocation filter, normalize categories, improve grid layout
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.
This commit is contained in:
1 parent
44d7f8ce81
commit
2b790d8eb4
19 files changed
+2638
-173
No files matched your search
@@ -0,0 +1,410 @@
|
||||
# 🎯 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)
|
||||
|
||||
Reference in new issue
Block a user