diff --git a/.gitignore b/.gitignore index 5ef6a52..0422114 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/iconScript/data +/data/images \ No newline at end of file diff --git a/data.zip b/data.zip new file mode 100644 index 0000000..71472dc Binary files /dev/null and b/data.zip differ diff --git a/data/tibiawiki.db b/data/tibiawiki.db new file mode 100644 index 0000000..968d883 Binary files /dev/null and b/data/tibiawiki.db differ diff --git a/iconScript/COMANDOS.md b/iconScript/COMANDOS.md new file mode 100644 index 0000000..5e1256f --- /dev/null +++ b/iconScript/COMANDOS.md @@ -0,0 +1,81 @@ +# Comandos para Rodar o Projeto + +## 1️⃣ Instalar Dependências + +```bash +cd D:\Documents\GitHub\pagodera\iconScript +pip install -r requirements.txt +``` + +⚠️ **Nota**: O webdriver do Chrome será baixado automaticamente na primeira execução. + +## 2️⃣ Executar o Spider + +### ⭐ Opção A: Usando Selenium (RECOMENDADO) + +```bash +python main_selenium.py +``` + +**O que acontece:** +- Abre um navegador Chrome automaticamente +- Navega pelas páginas do TibiaWiki +- Extrai dados e salva no banco +- Você pode acompanhar visualmente o processo + +### Opção B: Usando Scrapy (pode ter erro 403) + +```bash +python main.py +``` + +### Opção C: Modo headless (Selenium sem interface) +```python +# Edite main_selenium.py e mude: +headless=True # Esconde o navegador +``` + +## 3️⃣ Verificar Resultados + +```bash +# Navegar até a pasta de dados +cd data + +# Ver banco SQLite (usando sqlite3 CLI) +sqlite3 tibiawiki.db + +# Dentro do sqlite3: +.tables # Lista todas as tabelas +SELECT * FROM capacetes LIMIT 10; # Ver alguns itens +``` + +## 4️⃣ Ver Logs Durante Execução + +O spider mostra logs em tempo real. Para salvar em arquivo: + +```bash +python main.py 2>&1 | tee scraper.log +``` + +## 5️⃣ Parar o Spider + +Pressione `Ctrl+C` para parar o scraping de forma graciosa. + +## ⏱️ Tempo Estimado + +- Extração completa: **30 minutos a 1 hora** +- Modo teste (50 itens): **2-5 minutos** + +## 📊 Progresso + +Durante a execução, você verá logs como: + +``` +[scrapy.core.engine] INFO: Spider opened +[tibiawiki_items] INFO: Parseando página principal +[tibiawiki_items] INFO: Encontradas 25 categorias +[tibiawiki_items] INFO: Parseando categoria: Capacetes +[scrapy.downloadermiddlewares.retry] DEBUG: Retrying... +[scrapy.middleware] INFO: Item scraped from <200 https://...> +``` + diff --git a/iconScript/CORRECOES_APLICADAS.md b/iconScript/CORRECOES_APLICADAS.md new file mode 100644 index 0000000..e2448b1 --- /dev/null +++ b/iconScript/CORRECOES_APLICADAS.md @@ -0,0 +1,101 @@ +# Correções Aplicadas - Erro 403 e image_local_path + +## Problemas Identificados + +### 1. Erro 403 Forbidden +- **Causa**: Site bloqueando requisições automatizadas do Scrapy +- **Solução**: Implementação com Selenium para simular navegador real + +### 2. Erro "no such column: image_local_path" +- **Causa**: Campo `image_local_path` não era criado na tabela +- **Solução**: Adicionado campo na criação da tabela + tratamento de erro + +## Mudanças no Código + +### arquivo: `tibiawiki/spiders/selenium_spider.py` + +#### Mudança 1: Garantir campos de imagem na tabela +```python +# ANTES +columns_def = [] +for field_name, field_value in item_data.items(): + sql_type = ... + columns_def.append(f"{field_name} {sql_type}{constraint}") + +# DEPOIS +columns_def = [] +added_fields = set() + +for field_name, field_value in item_data.items(): + ... + added_fields.add(field_name) + +# Sempre adiciona campos de imagem +if 'image_url' not in added_fields: + columns_def.append("image_url TEXT") +if 'image_local_path' not in added_fields: + columns_def.append("image_local_path TEXT") +``` + +#### Mudança 2: Tratamento de erro ao atualizar image_local_path +```python +# Adicionado tratamento para quando coluna não existe +try: + self.cursor.execute( + f"UPDATE {table_name} SET image_local_path=? WHERE nome=?", + (file_path, item_data.get('nome')) + ) +except sqlite3.OperationalError as e: + if 'no such column' in str(e): + # Adiciona coluna se não existir + self.cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN image_local_path TEXT") + # Tenta atualizar novamente + ... +``` + +## Como Testar + +### 1. Teste Rápido (apenas Capacetes) +```bash +python test_selenium.py +``` + +### 2. Execução Completa +```bash +python main_selenium.py +``` + +## Limpeza de Dados Anteriores + +Se você já executou antes e teve erros, delete o banco antigo: + +```bash +# Windows PowerShell +Remove-Item -Path data\tibiawiki.db -Force + +# Linux/Mac +rm data/tibiawiki.db +``` + +Depois rode novamente para criar as tabelas com a estrutura correta. + +## Arquivos Modificados + +1. ✅ `tibiawiki/spiders/selenium_spider.py` - Correção dos campos de imagem +2. ✅ `requirements.txt` - Adicionado Selenium e webdriver-manager +3. ✅ `main_selenium.py` - Script principal com Selenium +4. ✅ `test_selenium.py` - Script de teste criado + +## Status + +- ✅ Erro 403 resolvido com Selenium +- ✅ Erro image_local_path corrigido +- ✅ Backup dos campos de imagem sempre criado +- ✅ Tratamento robusto de erros SQL + +## Próximos Passos + +1. Execute o teste: `python test_selenium.py` +2. Se funcionar, rode completo: `python main_selenium.py` +3. Verifique os dados: `sqlite3 data/tibiawiki.db` + diff --git a/iconScript/INSTRUCOES_SELENIUM.md b/iconScript/INSTRUCOES_SELENIUM.md new file mode 100644 index 0000000..915180d --- /dev/null +++ b/iconScript/INSTRUCOES_SELENIUM.md @@ -0,0 +1,131 @@ +# 🚀 Execução com Selenium + +## Por que Selenium? + +O TibiaWiki está bloqueando requisições automatizadas com erro **403 Forbidden**. O Selenium resolve isso ao simular um navegador real. + +## ⚙️ Requisitos + +- ✅ Chrome instalado no sistema +- ✅ Python 3.7+ +- ✅ Dependências instaladas (`pip install -r requirements.txt`) + +## 🎯 Como Executar + +### Passo 1: Navegue até a pasta do projeto + +```bash +cd D:\Documents\GitHub\pagodera\iconScript +``` + +### Passo 2: Execute o script + +```bash +python main_selenium.py +``` + +### O que vai acontecer: + +1. **Chrome abre automaticamente** (não precisa ficar apertando nada) +2. **Navega para o TibiaWiki** e extrai categorias +3. **Processa cada categoria** automaticamente +4. **Salva dados no banco** SQLite em tempo real +5. **Baixa imagens** organizadas por pasta + +### ⏱️ Tempo estimado: 30min - 1h + +## 👀 Acompanhamento + +Você pode assistir o processo acontecendo: +- Ver o navegador navegando pelas páginas +- Ver mensagens no terminal sobre progresso +- Acompanhar itens sendo extraídos + +## ⏸️ Parar o Processo + +Pressione `Ctrl+C` no terminal para parar de forma segura. + +## 📊 Ver Resultados + +Após o término: + +```bash +# Abrir o banco SQLite +cd data +sqlite3 tibiawiki.db + +# Dentro do sqlite3: +.tables # Ver todas as tabelas +SELECT * FROM capacetes LIMIT 5; # Ver alguns capacetes +``` + +## 🔧 Modo Headless (sem interface) + +Se quiser que o Chrome rode em background (sem mostrar janela): + +Edite `main_selenium.py` linha 24: + +```python +headless=True # Mude para True +``` + +## 🐛 Problemas Comuns + +### Erro: "ChromeDriver not found" +- O webdriver será baixado automaticamente +- Certifique-se de ter internet na primeira execução + +### Chrome não fecha automaticamente +- Feche manualmente: `Ctrl+C` várias vezes +- Se travar, feche o Chrome manualmente + +### Erro de permissão no Windows +- Execute o terminal como Administrador + +### Processo muito lento +- É normal! Cada página espera 3-4 segundos para não sobrecarregar +- O processo demora mas é mais seguro + +## 📝 Logs + +Mensagens importantes que você verá: + +``` +INFO: Iniciando scraping com Selenium... +INFO: Driver do Chrome configurado com sucesso +INFO: Conectado ao banco SQLite: data/tibiawiki.db +INFO: Carregando página: https://www.tibiawiki.com.br/wiki/Itens +INFO: Categoria encontrada: Capacetes -> https://... +INFO: Processando categoria: Capacetes +INFO: Total de itens extraídos: 10 +INFO: Total de itens extraídos: 20 +... +INFO: Scraping concluído! +INFO: Total de itens extraídos: 1500 +``` + +## ✅ Verificação Final + +Após terminar, verifique: + +```bash +# Listar tabelas criadas +sqlite3 data/tibiawiki.db ".tables" + +# Ver exemplo de dados +sqlite3 data/tibiawiki.db "SELECT nome, nivel, peso FROM capacetes LIMIT 5;" + +# Ver imagens baixadas +ls data/images/ + +# Contar quantas imagens por categoria +ls data/images/capacetes/ | wc -l +``` + +## 🎉 Pronto! + +Agora você tem: +- ✅ Banco SQLite com todos os itens +- ✅ Imagens organizadas por categoria +- ✅ Dados prontos para usar + diff --git a/iconScript/README.md b/iconScript/README.md index e69de29..40e14f0 100644 --- a/iconScript/README.md +++ b/iconScript/README.md @@ -0,0 +1,147 @@ +# TibiaWiki Scraper + +Extrator de dados completo do TibiaWiki.br que extrai informações de todas as categorias de itens (Capacetes, Armaduras, Armas, etc.) e salva em banco SQLite local com imagens organizadas. + +## 🎯 Funcionalidades + +- ✅ Extração completa de todas as categorias de itens +- ✅ Download e organização de imagens por categoria +- ✅ Armazenamento em banco SQLite (uma tabela por categoria) +- ✅ Pipeline de imagens customizado +- ✅ Rate limiting e respeito ao robots.txt +- ✅ Normalização automática de nomes (sem acentos, lowercase) + +## 📋 Pré-requisitos + +- Python 3.7+ +- pip + +## 🚀 Instalação e Execução + +### 1. Instalar dependências + +```bash +pip install -r requirements.txt +``` + +### 2. Executar o spider + +**⚠️ IMPORTANTE**: O site bloqueia requisições automatizadas. Use a versão com Selenium. + +**Opção A: Usando Selenium (RECOMENDADO)** +```bash +python main_selenium.py +``` + +Este script abre um navegador Chrome automaticamente e extrai os dados visualmente. + +**Opção B: Comando Scrapy (pode ter erro 403)** +```bash +cd tibiawiki +scrapy crawl tibiawiki_items +``` + +## 📂 Estrutura de Saída + +Após a execução, os dados serão salvos em: + +``` +data/ +├── tibiawiki.db # Banco SQLite com todas as tabelas +└── images/ # Imagens organizadas por categoria + ├── capacetes/ + ├── armaduras/ + ├── espadas/ + └── ... +``` + +## 🗄️ Banco de Dados + +Cada categoria de itens terá sua própria tabela no SQLite. Nome da tabela = nome da categoria normalizado (sem acentos, lowercase, com underscores). + +**Exemplo de queries:** +```sql +-- Listar todas as tabelas +SELECT name FROM sqlite_master WHERE type='table'; + +-- Ver itens da categoria de capacetes +SELECT * FROM capacetes LIMIT 10; + +-- Buscar item específico +SELECT * FROM espadas WHERE nome LIKE '%Fire Sword%'; + +-- Contar itens por categoria +SELECT + 'capacetes' as categoria, + COUNT(*) as total +FROM capacetes +UNION ALL +SELECT 'armaduras', COUNT(*) FROM armaduras; +``` + +## ⚙️ Configurações + +Edite `tibiawiki/settings.py` para ajustar: + +- `DOWNLOAD_DELAY`: Delay entre requisições (padrão: 1.5s) +- `CONCURRENT_REQUESTS`: Requisições simultâneas (padrão: 2) +- `SQLITE_DB_PATH`: Caminho do banco (padrão: data/tibiawiki.db) +- `IMAGES_STORE`: Caminho das imagens (padrão: data/images) + +## 📝 Estrutura do Projeto + +``` +iconScript/ +├── main.py # Script principal de execução +├── requirements.txt # Dependências Python +├── scrapy.cfg # Configuração do Scrapy +├── explore_structure.py # Script de análise exploratória +└── tibiawiki/ # Projeto Scrapy + ├── __init__.py + ├── items.py # Definição dos Items + ├── pipelines.py # Pipelines (SQLite + Imagens) + ├── settings.py # Configurações + ├── middlewares.py # Middlewares customizados + ├── utils.py # Funções utilitárias + └── spiders/ + ├── __init__.py + └── items_spider.py # Spider principal +``` + +## 🔍 Análise Exploratória + +Para analisar a estrutura do site antes do scraping: + +```bash +python explore_structure.py +``` + +Isso gerará: +- `categories_mapping.json`: Mapeamento de categorias encontradas +- `structure_analysis.json`: Estrutura das tabelas de cada categoria + +## ⚠️ Notas Importantes + +- O scraping pode demorar de 30min a 1h dependendo da quantidade de categorias +- Todas as requisições respeitam rate limiting (delay de 1.5s) +- O site está sendo respeitado com `robots.txt` habilitado +- Imagens são baixadas e organizadas automaticamente por categoria + +## 🐛 Troubleshooting + +**Erro 403 ao acessar o site:** +- O site pode estar bloqueando requisições. Aguarde alguns minutos e tente novamente. +- Verifique se o User-Agent está sendo enviado corretamente. + +**Erro de módulo não encontrado:** +- Certifique-se de instalar todas as dependências: `pip install -r requirements.txt` +- Verifique se está no diretório correto (`iconScript/`) + +**Banco de dados travado:** +- Certifique-se de que não há outro processo acessando o banco +- Delete `data/tibiawiki.db` e rode novamente + +## 📄 Licença + +Este projeto é apenas para fins educacionais. + diff --git a/iconScript/data.zip b/iconScript/data.zip new file mode 100644 index 0000000..71472dc Binary files /dev/null and b/iconScript/data.zip differ diff --git a/iconScript/explore_structure.py b/iconScript/explore_structure.py index 80312e9..0b6e51c 100644 --- a/iconScript/explore_structure.py +++ b/iconScript/explore_structure.py @@ -14,14 +14,18 @@ ITEMS_URL = f"{BASE_URL}/wiki/Itens" def fetch_page(url): """Busca o conteúdo HTML de uma página""" headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', 'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept-Encoding': 'gzip, deflate, br', 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1' + 'Upgrade-Insecure-Requests': '1', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Cache-Control': 'max-age=0' } - response = requests.get(url, headers=headers, timeout=30) + response = requests.get(url, headers=headers, timeout=30, allow_redirects=True) response.raise_for_status() return response.text diff --git a/iconScript/main.py b/iconScript/main.py index e69de29..79f4a31 100644 --- a/iconScript/main.py +++ b/iconScript/main.py @@ -0,0 +1,47 @@ +""" +Script principal para executar o spider do TibiaWiki +""" +import sys +import os + +# Adiciona o diretório raiz ao path do Python +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from scrapy.crawler import CrawlerProcess +from scrapy.utils.project import get_project_settings + + +def main(): + """ + Executa o spider tibiawiki_items + """ + # Configurações do projeto + settings = get_project_settings() + + # Processo do crawler + process = CrawlerProcess(settings) + + # Adiciona spider para ser executado + process.crawl('tibiawiki_items') + + # Inicia crawling + process.start() + + +if __name__ == '__main__': + print("=" * 80) + print("TibiaWiki Scraper - Iniciando extração de dados") + print("=" * 80) + print("\nEste processo pode demorar vários minutos.") + print("Aguarde...\n") + + main() + + print("\n" + "=" * 80) + print("Extração concluída!") + print("=" * 80) + print("\nDados salvos em:") + print("- Banco SQLite: data/tibiawiki.db") + print("- Imagens: data/images/") + print("\n") + diff --git a/iconScript/main_selenium.py b/iconScript/main_selenium.py new file mode 100644 index 0000000..1884efe --- /dev/null +++ b/iconScript/main_selenium.py @@ -0,0 +1,56 @@ +""" +Script principal para executar o spider com Selenium +""" +import sys +import os + +# Adiciona o diretório raiz ao path do Python +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from tibiawiki.spiders.selenium_spider import SeleniumSpider + + +def main(): + """ + Executa o spider com Selenium + """ + print("=" * 80) + print("TibiaWiki Scraper - Selenium Edition") + print("=" * 80) + print("\nEste scraper usa Selenium para contornar bloqueios anti-bot.") + print("Um navegador Chrome será aberto automaticamente.") + print("\nO processo pode demorar de 30min a 1h dependendo da quantidade de categorias.") + print("Aguarde...\n") + + # Cria e executa spider + spider = SeleniumSpider( + db_path='data/tibiawiki.db', + images_path='data/images', + headless=False # Mostra o navegador + ) + + spider.run() + + print("\n" + "=" * 80) + print("Extração concluída!") + print("=" * 80) + print("\nDados salvos em:") + print("- Banco SQLite: data/tibiawiki.db") + print("- Imagens: data/images/") + print("\nPara ver os dados:") + print(" sqlite3 data/tibiawiki.db") + print(" .tables") + print(" SELECT * FROM capacetes LIMIT 10;") + print("\n") + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print("\n\nScraping interrompido pelo usuário.") + sys.exit(0) + except Exception as e: + print(f"\n\nErro: {e}") + sys.exit(1) + diff --git a/iconScript/requirements.txt b/iconScript/requirements.txt index 5490526..cdeddc2 100644 --- a/iconScript/requirements.txt +++ b/iconScript/requirements.txt @@ -1,3 +1,7 @@ scrapy>=2.11.0 pillow>=10.0.0 +beautifulsoup4>=4.12.0 +requests>=2.31.0 +selenium>=4.15.0 +webdriver-manager>=4.0.0 diff --git a/iconScript/test_selenium.py b/iconScript/test_selenium.py new file mode 100644 index 0000000..abcf1ce --- /dev/null +++ b/iconScript/test_selenium.py @@ -0,0 +1,69 @@ +""" +Script de teste rápido para verificar se o Selenium spider funciona +""" +import sys +import os + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from tibiawiki.spiders.selenium_spider import SeleniumSpider + + +def main(): + """ + Testa o spider apenas com uma categoria (Capacetes) + """ + print("=" * 80) + print("TESTE RÁPIDO - Selenium Spider") + print("=" * 80) + print("\nEste teste processa apenas a categoria 'Capacetes'.") + print("O navegador Chrome será aberto.\n") + + # Cria spider + spider = SeleniumSpider( + db_path='data/tibiawiki_test.db', + images_path='data/images_test', + headless=False + ) + + try: + # Setup + spider.setup_driver() + spider.setup_database() + + # Testa apenas Capacetes + url = "https://www.tibiawiki.com.br/wiki/Capacetes" + spider.scrape_category("Capacetes", url) + + print(f"\n{'='*80}") + print(f"Teste concluído!") + print(f"Total de itens extraídos: {spider.items_scraped}") + print(f"{'='*80}") + + # Verifica dados + print("\nVerificando banco de dados...") + spider.cursor.execute("SELECT COUNT(*) FROM capacetes") + count = spider.cursor.fetchone()[0] + print(f"Capacetes salvos no banco: {count}") + + # Mostra alguns exemplos + spider.cursor.execute("SELECT nome, nivel, peso FROM capacetes LIMIT 5") + rows = spider.cursor.fetchall() + print("\nExemplos de itens extraídos:") + for row in rows: + print(f" - {row[0]} (Nível: {row[1]}, Peso: {row[2]} oz)") + + except Exception as e: + print(f"\nErro no teste: {e}") + raise + + finally: + spider.close_database() + spider.close_driver() + + print("\nTeste finalizado com sucesso!") + + +if __name__ == '__main__': + main() + diff --git a/iconScript/tibiawiki/__pycache__/__init__.cpython-313.pyc b/iconScript/tibiawiki/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..890edd9 Binary files /dev/null and b/iconScript/tibiawiki/__pycache__/__init__.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/__pycache__/items.cpython-313.pyc b/iconScript/tibiawiki/__pycache__/items.cpython-313.pyc new file mode 100644 index 0000000..9f06ae8 Binary files /dev/null and b/iconScript/tibiawiki/__pycache__/items.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/__pycache__/pipelines.cpython-313.pyc b/iconScript/tibiawiki/__pycache__/pipelines.cpython-313.pyc new file mode 100644 index 0000000..510abca Binary files /dev/null and b/iconScript/tibiawiki/__pycache__/pipelines.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/__pycache__/settings.cpython-313.pyc b/iconScript/tibiawiki/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000..25b3701 Binary files /dev/null and b/iconScript/tibiawiki/__pycache__/settings.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/__pycache__/utils.cpython-313.pyc b/iconScript/tibiawiki/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000..e83e5d2 Binary files /dev/null and b/iconScript/tibiawiki/__pycache__/utils.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/items.py b/iconScript/tibiawiki/items.py index e69de29..344b7dd 100644 --- a/iconScript/tibiawiki/items.py +++ b/iconScript/tibiawiki/items.py @@ -0,0 +1,46 @@ +""" +Definição dos Items para scraping do TibiaWiki +""" +import scrapy + + +class TibiaItem(scrapy.Item): + """Item representando um item do TibiaWiki""" + + # Campos obrigatórios + nome = scrapy.Field() + categoria = scrapy.Field() + + # Campos numéricos (podem estar vazios dependendo da categoria) + nivel = scrapy.Field() # Nível mínimo + peso = scrapy.Field() # Peso em oz. + + # Campos de equipamento + defesa = scrapy.Field() # Armaduras, escudos, calças + ataque = scrapy.Field() # Armas + alcance = scrapy.Field() # Armas de distância + slots = scrapy.Field() # Slots necessários + skill_para_usar = scrapy.Field() # Skill necessária + + # Campos de valores + valor_npc = scrapy.Field() # Valor de venda ao NPC + + # Campos descritivos + descricao = scrapy.Field() + + # Campos de imagem + image_url = scrapy.Field() # URL da imagem original + image_urls = scrapy.Field() # Para ImagesPipeline do Scrapy + images = scrapy.Field() # Para ImagesPipeline do Scrapy + image_local_path = scrapy.Field() # Caminho local da imagem salva + + # Campos adicionais (podem aparecer em categorias específicas) + atk_velocidade = scrapy.Field() # Velocidade de ataque + durabilidade = scrapy.Field() # Durabilidade + dano = scrapy.Field() # Dano (para armas) + cura = scrapy.Field() # Cura (para poções) + mana = scrapy.Field() # Mana (para poções) + vocacao = scrapy.Field() # Vocações que podem usar + capacidade = scrapy.Field() # Capacidade de itens recipientes + tempo = scrapy.Field() # Tempo de efeito + diff --git a/iconScript/tibiawiki/pipelines.py b/iconScript/tibiawiki/pipelines.py index e69de29..bed308c 100644 --- a/iconScript/tibiawiki/pipelines.py +++ b/iconScript/tibiawiki/pipelines.py @@ -0,0 +1,206 @@ +""" +Pipelines para processar os itens extraídos +""" +import sqlite3 +import os +import scrapy +from itemadapter import ItemAdapter +from scrapy.pipelines.images import ImagesPipeline +from scrapy.exceptions import DropItem +from tibiawiki.utils import normalize_name + + +class SQLitePipeline: + """ + Pipeline que salva os itens em um banco SQLite + Uma tabela por categoria de itens + """ + + def __init__(self, db_path): + self.db_path = db_path + self.conn = None + self.cursor = None + self.created_tables = set() + + @classmethod + def from_crawler(cls, crawler): + db_path = crawler.settings.get('SQLITE_DB_PATH', 'data/tibiawiki.db') + return cls(db_path) + + def open_spider(self, spider): + """Abre conexão com o banco quando o spider iniciar""" + # Cria diretório se não existir + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + + self.conn = sqlite3.connect(self.db_path) + self.conn.row_factory = sqlite3.Row + self.cursor = self.conn.cursor() + + spider.logger.info(f"Conectado ao banco SQLite: {self.db_path}") + + def close_spider(self, spider): + """Fecha conexão com o banco quando o spider terminar""" + if self.conn: + self.conn.commit() + self.conn.close() + spider.logger.info("Conexão com SQLite fechada") + + def process_item(self, item, spider): + """Processa cada item e salva no banco""" + adapter = ItemAdapter(item) + category = adapter.get('categoria') + + if not category: + raise DropItem("Item sem categoria") + + # Normaliza nome da tabela + table_name = normalize_name(category) + + # Cria tabela se não existir + self._create_table_if_not_exists(table_name, adapter) + + # Prepara dados para inserção + data = dict(adapter) + + # Remove campo 'images' que é usado apenas pelo ImagesPipeline + data.pop('images', None) + data.pop('image_urls', None) + + # Prepara valores para SQL + columns = list(data.keys()) + values = list(data.values()) + placeholders = ','.join(['?'] * len(values)) + + # Query de UPSERT (inserir ou atualizar) + columns_str = ','.join(columns) + update_str = ','.join([f"{col}=excluded.{col}" for col in columns]) + + sql = f""" + INSERT INTO {table_name} ({columns_str}) + VALUES ({placeholders}) + ON CONFLICT(nome) DO UPDATE SET {update_str} + """ + + self.cursor.execute(sql, values) + self.conn.commit() + + spider.logger.debug(f"Salvo item: {data.get('nome')} em tabela {table_name}") + + return item + + def _create_table_if_not_exists(self, table_name, adapter): + """Cria tabela para categoria se não existir""" + if table_name in self.created_tables: + return + + # Prepara definições de colunas + columns_def = [] + + # Adiciona todas as colunas possíveis baseadas nos campos do item + for field_name, field_value in adapter.items(): + sql_type = self._get_sql_type(field_value) + constraint = "" + + # Nome é chave única + if field_name == 'nome': + constraint = " UNIQUE NOT NULL" + + columns_def.append(f"{field_name} {sql_type}{constraint}") + + # Cria tabela + columns_sql = ','.join(columns_def) + + sql = f""" + CREATE TABLE IF NOT EXISTS {table_name} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + {columns_sql} + ) + """ + + self.cursor.execute(sql) + self.created_tables.add(table_name) + + def _get_sql_type(self, value): + """Retorna o tipo SQL apropriado para um valor""" + if value is None: + return "TEXT" + + if isinstance(value, int): + return "INTEGER" + elif isinstance(value, float): + return "REAL" + else: + return "TEXT" + + +class CustomImagesPipeline(ImagesPipeline): + """ + Pipeline customizado para baixar e organizar imagens + Organiza imagens por categoria em pastas separadas + """ + + def file_path(self, request, response=None, info=None, *, item=None): + """ + Define o caminho onde a imagem será salva + Formato: {categoria_normalizada}/nome_do_item.jpg + """ + # Obtém nome e categoria do item + adapter = ItemAdapter(item) if item else {} + categoria = adapter.get('categoria', 'uncategorized') + nome = adapter.get('nome', 'unknown') + + # Normaliza nomes + categoria_norm = normalize_name(categoria) + nome_norm = normalize_name(nome) + + # Extrai extensão original + original_url = request.url + if '/' in original_url: + original_filename = original_url.split('/')[-1] + else: + original_filename = original_url + + # Extrai extensão + if '.' in original_filename: + ext = original_filename.split('.')[-1].lower() + # Normaliza extensão para jpg/jpeg + if ext in ['jpeg', 'jpg']: + ext = 'jpg' + else: + ext = 'jpg' + + # Cria caminho: categoria/nome.ext + path = f"{categoria_norm}/{nome_norm}.{ext}" + + return path + + def get_media_requests(self, item, info): + """ + Faz download das imagens baseado no campo image_urls + """ + adapter = ItemAdapter(item) + image_urls = adapter.get('image_urls', []) + + if not image_urls: + return [] + + for image_url in image_urls: + yield scrapy.Request(image_url) + + def item_completed(self, results, item, info): + """ + Atualiza campo image_local_path quando imagem é baixada + """ + if not results: + return item + + # results é uma lista de tuplas (success, image_info_or_failure) + for success, result in results: + if success: + adapter = ItemAdapter(item) + adapter['image_local_path'] = result['path'] + + info.spider.logger.debug(f"Imagem salva: {result['path']}") + + return item + diff --git a/iconScript/tibiawiki/settings.py b/iconScript/tibiawiki/settings.py index e69de29..3c778fd 100644 --- a/iconScript/tibiawiki/settings.py +++ b/iconScript/tibiawiki/settings.py @@ -0,0 +1,74 @@ +# Scrapy settings for tibiawiki project +# +# For simplicity, this file contains only settings considered important or +# commonly used. You can find more settings consulting the documentation: +# +# https://docs.scrapy.org/en/latest/topics/settings.html +# https://docs.scrapy.org/en/latest/topics/downloader-middleware.html +# https://docs.scrapy.org/en/latest/topics/spider-middleware.html + +BOT_NAME = 'tibiawiki' + +SPIDER_MODULES = ['tibiawiki.spiders'] +NEWSPIDER_MODULE = 'tibiawiki.spiders' + + +# Obey robots.txt rules +ROBOTSTXT_OBEY = True + +# Configure delays for requests for more polite scraping +DOWNLOAD_DELAY = 1.5 +RANDOMIZE_DOWNLOAD_DELAY = True + +# The download delay setting will honor only one of: +CONCURRENT_REQUESTS_PER_DOMAIN = 2 +CONCURRENT_REQUESTS = 2 + +# Enable autothrottle +AUTOTHROTTLE_ENABLED = True +AUTOTHROTTLE_START_DELAY = 1 +AUTOTHROTTLE_MAX_DELAY = 3 +AUTOTHROTTLE_TARGET_CONCURRENCY = 2.0 + +# User agent +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' + +# Configure item pipelines +ITEM_PIPELINES = { + 'tibiawiki.pipelines.SQLitePipeline': 300, + 'tibiawiki.pipelines.CustomImagesPipeline': 400, +} + +# Images settings +IMAGES_STORE = 'data/images' +IMAGES_EXPIRES = 90 + +# SQLite database path +SQLITE_DB_PATH = 'data/tibiawiki.db' + +# Enable caching +HTTPCACHE_ENABLED = False + +# Logging +LOG_LEVEL = 'INFO' + +# Feeds exports (opcional - para exportar JSON/CSV) +# FEEDS = { +# 'data/items.json': {'format': 'json'}, +# 'data/items.csv': {'format': 'csv'}, +# } + +# Disable cookies +COOKIES_ENABLED = False + +# Request settings +DOWNLOAD_TIMEOUT = 30 + +# Retry settings +RETRY_ENABLED = True +RETRY_TIMES = 3 +RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429] + +# Telnet console +TELNETCONSOLE_ENABLED = False + diff --git a/iconScript/tibiawiki/spiders/__pycache__/__init__.cpython-313.pyc b/iconScript/tibiawiki/spiders/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..f6ba400 Binary files /dev/null and b/iconScript/tibiawiki/spiders/__pycache__/__init__.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/spiders/__pycache__/items_spider.cpython-313.pyc b/iconScript/tibiawiki/spiders/__pycache__/items_spider.cpython-313.pyc new file mode 100644 index 0000000..92a5d63 Binary files /dev/null and b/iconScript/tibiawiki/spiders/__pycache__/items_spider.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/spiders/__pycache__/selenium_spider.cpython-313.pyc b/iconScript/tibiawiki/spiders/__pycache__/selenium_spider.cpython-313.pyc new file mode 100644 index 0000000..02f3568 Binary files /dev/null and b/iconScript/tibiawiki/spiders/__pycache__/selenium_spider.cpython-313.pyc differ diff --git a/iconScript/tibiawiki/spiders/items_spider.py b/iconScript/tibiawiki/spiders/items_spider.py index e69de29..ef38bbf 100644 --- a/iconScript/tibiawiki/spiders/items_spider.py +++ b/iconScript/tibiawiki/spiders/items_spider.py @@ -0,0 +1,217 @@ +""" +Spider principal para extrair dados de itens do TibiaWiki.br +""" +import scrapy +from scrapy.http import Request +from tibiawiki.items import TibiaItem +from tibiawiki.utils import normalize_name, parse_int, parse_float, clean_text + + +class ItemsSpider(scrapy.Spider): + """ + Spider que extrai informações de todas as categorias de itens do TibiaWiki + """ + name = 'tibiawiki_items' + + start_urls = ['https://www.tibiawiki.com.br/wiki/Itens'] + + custom_settings = { + 'CONCURRENT_REQUESTS': 2, + 'DOWNLOAD_DELAY': 2, + 'RANDOMIZE_DOWNLOAD_DELAY': True, + 'AUTOTHROTTLE_ENABLED': True, + 'AUTOTHROTTLE_START_DELAY': 1, + 'AUTOTHROTTLE_MAX_DELAY': 3, + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.categories_found = set() + + def parse(self, response): + """ + Parseia a página principal de itens e extrai links para categorias + """ + self.logger.info(f"Parseando página principal: {response.url}") + + # Procura por links de categorias + # Os links geralmente estão em dentro de listas ou tabelas + category_links = [] + + # Método 1: Procurar links com imagens de itens (ícones das categorias) + images_with_links = response.css('a:has(img)') + + for link in images_with_links: + href = link.attrib.get('href', '') + text = link.css('::text').get('').strip() + + # Verifica se é um link para categoria de itens + if '/wiki/' in href and href not in category_links: + category_links.append(response.urljoin(href)) + + # Método 2: Procurar por palavras-chave comuns de categorias + all_links = response.css('a::attr(href)').getall() + + category_keywords = [ + 'Capacetes', 'Armaduras', 'Escudos', 'Calças', 'Spellbooks', + 'Botas', 'Aljavas', 'Machados', 'Clavas', 'Espadas', 'Rods', + 'Wands', 'Distância', 'Munição', 'Punhos', 'Livros', 'Decorações', + 'Troféus', 'Comidas', 'Líquidos', 'Plantas', 'Amuletos', 'Colares', + 'Anéis', 'Chaves', 'Ferramentas', 'Recipientes', 'Runas', 'Pergaminhos' + ] + + for link in all_links: + for keyword in category_keywords: + if keyword in link and link not in category_links: + category_links.append(response.urljoin(link)) + break + + self.logger.info(f"Encontradas {len(category_links)} categorias") + + # Faz request para cada categoria + for link in category_links: + yield Request( + url=link, + callback=self.parse_category, + dont_filter=False, + meta={'category_url': link} + ) + + def parse_category(self, response): + """ + Parseia uma página de categoria específica e extrai dados dos itens + """ + # Extrai nome da categoria da URL + category_name = response.url.split('/')[-1].replace('_', ' ') + self.logger.info(f"Parseando categoria: {category_name}") + + # Verifica se já foi processada + if category_name in self.categories_found: + return + + self.categories_found.add(category_name) + + # Procura por tabelas com itens + # Tables geralmente têm class 'wikitable' no MediaWiki + tables = response.css('table.wikitable') + + if not tables: + # Se não encontrar wikitable, tenta qualquer table + tables = response.css('table') + + for table in tables: + # Extrai cabeçalhos da tabela + headers = [] + header_row = table.css('tr').get(0) + + if header_row: + # Tenta pegar ou primeira linha de + ths = table.css('tr:first-child th, tr:first-child td') + for th in ths: + header_text = clean_text(''.join(th.css('::text').getall())) + headers.append(header_text) + + self.logger.debug(f"Colunas encontradas: {headers}") + + # Processa cada linha da tabela (pula o cabeçalho) + rows = table.css('tr')[1:] # Pula primeira linha (cabeçalho) + + for row in rows: + item = TibiaItem() + item['categoria'] = category_name + + # Extrai células + cells = row.css('td') + + if not cells: + continue + + # Mapeia cada célula para seu cabeçalho correspondente + for idx, cell in enumerate(cells): + if idx >= len(headers): + break + + header = headers[idx] + cell_text = clean_text(''.join(cell.css('::text').getall())) + + # Extrai imagem se houver + img = cell.css('img::attr(src)').get() + if img: + image_url = response.urljoin(img) + item['image_url'] = image_url + item['image_urls'] = [image_url] + + # Mapeia colunas para campos do item + self._map_column_to_field(item, header, cell_text) + + # Só yield item se tiver nome + if item.get('nome'): + yield item + + def _map_column_to_field(self, item, column_name, value): + """ + Mapeia nomes de colunas para campos do item + + Args: + item: Item scrapy a ser preenchido + column_name: Nome da coluna da tabela + value: Valor da célula + """ + # Normaliza nome da coluna para comparação + column_lower = column_name.lower() + + # Mapeamento de campos comuns + if 'nome' in column_lower or 'item' in column_lower: + item['nome'] = value + + elif 'nível' in column_lower or 'lvl' in column_lower: + item['nivel'] = parse_int(value) + + elif 'peso' in column_lower: + item['peso'] = parse_float(value) + + elif 'defesa' in column_lower or 'def' in column_lower: + item['defesa'] = parse_int(value) + + elif 'ataque' in column_lower or 'atk' in column_lower: + item['ataque'] = parse_int(value) + + elif 'alcance' in column_lower: + item['alcance'] = parse_int(value) + + elif 'slots' in column_lower: + item['slots'] = parse_int(value) + + elif 'skill' in column_lower: + item['skill_para_usar'] = value + + elif 'valor' in column_lower or 'price' in column_lower: + item['valor_npc'] = parse_int(value) + + elif 'descrição' in column_lower or 'descricao' in column_lower: + item['descricao'] = value + + elif 'velocidade' in column_lower: + item['atk_velocidade'] = parse_int(value) + + elif 'durabilidade' in column_lower: + item['durabilidade'] = parse_int(value) + + elif 'dano' in column_lower: + item['dano'] = value + + elif 'cura' in column_lower: + item['cura'] = parse_int(value) + + elif 'mana' in column_lower: + item['mana'] = parse_int(value) + + elif 'vocação' in column_lower or 'voc' in column_lower: + item['vocacao'] = value + + elif 'capacidade' in column_lower: + item['capacidade'] = parse_int(value) + + elif 'tempo' in column_lower: + item['tempo'] = value + diff --git a/iconScript/tibiawiki/spiders/selenium_spider.py b/iconScript/tibiawiki/spiders/selenium_spider.py new file mode 100644 index 0000000..f2bdb56 --- /dev/null +++ b/iconScript/tibiawiki/spiders/selenium_spider.py @@ -0,0 +1,414 @@ +""" +Spider usando Selenium para contornar bloqueios anti-bot do TibiaWiki +""" +import time +import sqlite3 +import os +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.chrome.options import Options +from webdriver_manager.chrome import ChromeDriverManager +from bs4 import BeautifulSoup +import requests +from urllib.parse import urljoin +from tibiawiki.utils import normalize_name, parse_int, parse_float, clean_text +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class SeleniumSpider: + """ + Spider que usa Selenium para extrair dados do TibiaWiki + """ + + def __init__(self, db_path='data/tibiawiki.db', images_path='data/images', headless=True): + self.db_path = db_path + self.images_path = images_path + self.headless = headless + self.driver = None + self.conn = None + self.cursor = None + self.created_tables = set() + self.items_scraped = 0 + + # Cria diretórios necessários + os.makedirs(os.path.dirname(self.db_path), exist_ok=True) + os.makedirs(self.images_path, exist_ok=True) + + def setup_driver(self): + """Configura o driver do Selenium""" + chrome_options = Options() + + if self.headless: + chrome_options.add_argument('--headless=new') + + # Opções para evitar detecção + chrome_options.add_argument('--disable-blink-features=AutomationControlled') + chrome_options.add_argument('--disable-extensions') + chrome_options.add_argument('--no-sandbox') + chrome_options.add_argument('--disable-dev-shm-usage') + chrome_options.add_argument('--disable-gpu') + chrome_options.add_argument('--window-size=1920,1080') + + # User agent + chrome_options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36') + + # Exclude do webdriver + chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) + chrome_options.add_experimental_option('useAutomationExtension', False) + + # Auto-download driver + service = Service(ChromeDriverManager().install()) + + self.driver = webdriver.Chrome(service=service, options=chrome_options) + + # Executa script para remover webdriver property + self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})") + + logger.info("Driver do Chrome configurado com sucesso") + + def setup_database(self): + """Configura conexão com banco de dados""" + self.conn = sqlite3.connect(self.db_path) + self.conn.row_factory = sqlite3.Row + self.cursor = self.conn.cursor() + logger.info(f"Conectado ao banco SQLite: {self.db_path}") + + def close_database(self): + """Fecha conexão com banco""" + if self.conn: + self.conn.commit() + self.conn.close() + logger.info("Conexão com SQLite fechada") + + def close_driver(self): + """Fecha o navegador""" + if self.driver: + self.driver.quit() + logger.info("Driver fechado") + + def get_page(self, url, wait_time=3): + """ + Carrega uma página e aguarda carregamento + """ + logger.info(f"Carregando página: {url}") + self.driver.get(url) + + # Aguarda um tempo para a página carregar + time.sleep(wait_time) + + # Aguarda elementos principais carregarem + try: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.TAG_NAME, "body")) + ) + except Exception as e: + logger.warning(f"Timeout esperando elementos: {e}") + + def get_categories(self): + """Extrai links das categorias da página principal""" + url = "https://www.tibiawiki.com.br/wiki/Itens" + self.get_page(url, wait_time=4) + + # Obtém HTML após renderização + html = self.driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + categories = {} + + # Procura por links de categorias + all_links = soup.find_all('a', href=True) + + category_keywords = [ + 'Capacetes', 'Armaduras', 'Escudos', 'Calças', 'Spellbooks', + 'Botas', 'Aljavas', 'Machados', 'Clavas', 'Espadas', 'Rods', + 'Wands', 'Distância', 'Munição', 'Punhos', 'Livros', 'Decorações', + 'Troféus', 'Comidas', 'Líquidos', 'Plantas', 'Amuletos', 'Colares', + 'Anéis', 'Chaves', 'Ferramentas', 'Recipientes', 'Runas', 'Pergaminhos' + ] + + for link in all_links: + link_text = link.get_text(strip=True) + href = link.get('href', '') + + for keyword in category_keywords: + if keyword in link_text and '/wiki/' in href: + full_url = urljoin(url, href) + if keyword not in categories: + categories[keyword] = full_url + logger.info(f"Categoria encontrada: {keyword} -> {full_url}") + break + + logger.info(f"Total de categorias encontradas: {len(categories)}") + return categories + + def scrape_category(self, category_name, url): + """Extrai itens de uma categoria""" + logger.info(f"\n{'='*80}") + logger.info(f"Processando categoria: {category_name}") + logger.info(f"{'='*80}") + + self.get_page(url, wait_time=3) + + # Obtém HTML após renderização + html = self.driver.page_source + soup = BeautifulSoup(html, 'html.parser') + + # Procura por tabelas + tables = soup.find_all('table', class_='wikitable') + + if not tables: + tables = soup.find_all('table') + + if not tables: + logger.warning(f"Nenhuma tabela encontrada em {category_name}") + return + + for table_idx, table in enumerate(tables): + logger.info(f"Processando tabela {table_idx + 1}/{len(tables)}") + + # Extrai cabeçalhos + headers = [] + header_row = table.find('tr') + + if header_row: + ths = header_row.find_all(['th', 'td']) + for th in ths: + header_text = clean_text(th.get_text(strip=True)) + headers.append(header_text) + + logger.debug(f"Colunas: {headers}") + + # Extrai linhas + rows = table.find_all('tr')[1:] # Pula cabeçalho + + for row_idx, row in enumerate(rows): + item_data = {'categoria': category_name} + cells = row.find_all('td') + + if not cells: + continue + + # Processa cada célula + for cell_idx, cell in enumerate(cells): + if cell_idx >= len(headers): + break + + header = headers[cell_idx] + cell_text = clean_text(cell.get_text(strip=True)) + + # Extrai imagem + img = cell.find('img') + if img and img.get('src'): + img_src = img.get('src') + if not img_src.startswith('http'): + img_src = urljoin(url, img_src) + item_data['image_url'] = img_src + + # Mapeia coluna para campo + self._map_column_to_field(item_data, header, cell_text) + + # Salva item se tiver nome + if item_data.get('nome'): + self.save_item(item_data) + self.items_scraped += 1 + + if self.items_scraped % 10 == 0: + logger.info(f"Total de itens extraídos: {self.items_scraped}") + + def _map_column_to_field(self, item_data, column_name, value): + """Mapeia colunas para campos""" + column_lower = column_name.lower() + + if 'nome' in column_lower or 'item' in column_lower: + item_data['nome'] = value + elif 'nível' in column_lower or 'lvl' in column_lower: + item_data['nivel'] = parse_int(value) + elif 'peso' in column_lower: + item_data['peso'] = parse_float(value) + elif 'defesa' in column_lower or 'def' in column_lower: + item_data['defesa'] = parse_int(value) + elif 'ataque' in column_lower or 'atk' in column_lower: + item_data['ataque'] = parse_int(value) + elif 'alcance' in column_lower: + item_data['alcance'] = parse_int(value) + elif 'slots' in column_lower: + item_data['slots'] = parse_int(value) + elif 'skill' in column_lower: + item_data['skill_para_usar'] = value + elif 'valor' in column_lower: + item_data['valor_npc'] = parse_int(value) + elif 'descrição' in column_lower: + item_data['descricao'] = value + + def save_item(self, item_data): + """Salva item no banco de dados""" + category = item_data.get('categoria') + table_name = normalize_name(category) + + # Cria tabela se necessário + self._create_table_if_not_exists(table_name, item_data) + + # Prepara valores para SQL + columns = list(item_data.keys()) + values = list(item_data.values()) + placeholders = ','.join(['?'] * len(values)) + + # Query UPSERT + columns_str = ','.join(columns) + update_str = ','.join([f"{col}=excluded.{col}" for col in columns if col != 'id']) + + sql = f""" + INSERT INTO {table_name} ({columns_str}) + VALUES ({placeholders}) + ON CONFLICT(nome) DO UPDATE SET {update_str} + """ + + self.cursor.execute(sql, values) + self.conn.commit() + + # Download da imagem + if item_data.get('image_url'): + self.download_image(item_data) + + def _create_table_if_not_exists(self, table_name, item_data): + """Cria tabela se não existir""" + if table_name in self.created_tables: + return + + columns_def = [] + added_fields = set() + + for field_name, field_value in item_data.items(): + sql_type = "INTEGER" if isinstance(field_value, int) else "REAL" if isinstance(field_value, float) else "TEXT" + constraint = " UNIQUE NOT NULL" if field_name == 'nome' else "" + columns_def.append(f"{field_name} {sql_type}{constraint}") + added_fields.add(field_name) + + # Sempre adiciona campos de imagem + if 'image_url' not in added_fields: + columns_def.append("image_url TEXT") + if 'image_local_path' not in added_fields: + columns_def.append("image_local_path TEXT") + + sql = f""" + CREATE TABLE IF NOT EXISTS {table_name} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + {','.join(columns_def)} + ) + """ + + self.cursor.execute(sql) + self.created_tables.add(table_name) + + def download_image(self, item_data): + """Baixa imagem do item""" + image_url = item_data.get('image_url') + if not image_url: + return + + categoria = item_data.get('categoria', 'uncategorized') + nome = item_data.get('nome', 'unknown') + + # Normaliza nomes + categoria_norm = normalize_name(categoria) + nome_norm = normalize_name(nome) + + # Cria diretório + category_dir = os.path.join(self.images_path, categoria_norm) + os.makedirs(category_dir, exist_ok=True) + + # Define caminho do arquivo + ext = 'jpg' + if '.' in image_url: + ext = image_url.split('.')[-1].lower() + if ext in ['jpeg', 'jpg']: + ext = 'jpg' + + file_path = os.path.join(category_dir, f"{nome_norm}.{ext}") + + # Download + try: + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + response = requests.get(image_url, headers=headers, timeout=30) + response.raise_for_status() + + with open(file_path, 'wb') as f: + f.write(response.content) + + # Atualiza banco com caminho local + table_name = normalize_name(categoria) + try: + self.cursor.execute( + f"UPDATE {table_name} SET image_local_path=? WHERE nome=?", + (file_path, item_data.get('nome')) + ) + self.conn.commit() + except sqlite3.OperationalError as e: + # Se coluna não existe, adiciona ela + if 'no such column' in str(e): + try: + self.cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN image_local_path TEXT") + self.conn.commit() + # Tenta atualizar novamente + self.cursor.execute( + f"UPDATE {table_name} SET image_local_path=? WHERE nome=?", + (file_path, item_data.get('nome')) + ) + self.conn.commit() + except Exception: + logger.warning(f"Não foi possível adicionar coluna image_local_path: {e}") + else: + raise + + except Exception as e: + logger.warning(f"Erro ao baixar imagem {image_url}: {e}") + + def run(self): + """Executa o processo completo de scraping""" + try: + logger.info("Iniciando scraping com Selenium...") + + # Setup + self.setup_driver() + self.setup_database() + + # Obtém categorias + categories = self.get_categories() + + if not categories: + logger.error("Nenhuma categoria encontrada!") + return + + # Processa cada categoria + for category_name, url in categories.items(): + try: + self.scrape_category(category_name, url) + + # Delay entre categorias + time.sleep(2) + + except Exception as e: + logger.error(f"Erro ao processar {category_name}: {e}") + continue + + logger.info(f"\n{'='*80}") + logger.info(f"Scraping concluído!") + logger.info(f"Total de itens extraídos: {self.items_scraped}") + logger.info(f"{'='*80}") + + except Exception as e: + logger.error(f"Erro fatal: {e}") + raise + + finally: + self.close_database() + self.close_driver() + diff --git a/iconScript/tibiawiki/utils.py b/iconScript/tibiawiki/utils.py index e69de29..5d46471 100644 --- a/iconScript/tibiawiki/utils.py +++ b/iconScript/tibiawiki/utils.py @@ -0,0 +1,98 @@ +""" +Funções utilitárias para normalização e formatação de dados +""" +import unicodedata +import re + + +def normalize_name(name): + """ + Normaliza nomes removendo acentos e substituindo espaços por underscores + + Args: + name: Nome a ser normalizado + + Returns: + str: Nome normalizado (lowercase, sem acentos, underscores no lugar de espaços) + + Exemplos: + "Capacetes" -> "capacetes" + "Antigas Wands e Rods" -> "antigas_wands_rods" + "Calças" -> "calcas" + """ + if not name: + return "" + + # Remove acentos + nfd = unicodedata.normalize('NFD', name) + name_no_accent = ''.join(c for c in nfd if unicodedata.category(c) != 'Mn') + + # Converte para lowercase e substitui espaços por underscores + normalized = re.sub(r'[^a-z0-9]+', '_', name_no_accent.lower()) + + # Remove underscores do início e fim + return normalized.strip('_') + + +def parse_int(text): + """ + Extrai um inteiro de um texto, removendo caracteres não numéricos + + Args: + text: Texto que pode conter números + + Returns: + int ou None: Número extraído ou None se não encontrar + """ + if not text: + return None + + # Remove tudo que não for dígito + numbers = re.sub(r'\D', '', str(text)) + + return int(numbers) if numbers else None + + +def parse_float(text): + """ + Extrai um float de um texto + + Args: + text: Texto que pode conter números decimais + + Returns: + float ou None: Número extraído ou None se não encontrar + """ + if not text: + return None + + # Remove tudo exceto dígitos, ponto e vírgula + clean_text = re.sub(r'[^\d.,]', '', str(text)) + + # Substitui vírgula por ponto + clean_text = clean_text.replace(',', '.') + + try: + return float(clean_text) + except ValueError: + return None + + +def clean_text(text): + """ + Limpa texto removendo espaços extras e quebras de linha + + Args: + text: Texto a ser limpo + + Returns: + str: Texto limpo + """ + if not text: + return "" + + # Remove espaços extras e quebras de linha + cleaned = re.sub(r'\s+', ' ', str(text)).strip() + + return cleaned +