feat(item-card): add 'market'/'catalog' mode prop with defensive price checks
Introduces optional `mode` prop ("market"/"catalog") to conditionally render components like rarity badges. Adds defensive checks for missing price data and priceHistory properties to prevent runtime errors. New icons `Badge` and `Boxes` imported from lucide-react.
This commit is contained in:
1 parent
29ba5aece0
commit
44d7f8ce81
21 files changed
+1181
-714
No files matched your search
@@ -1,46 +0,0 @@
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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
|
||||
|
||||
Binary file not shown.
@@ -1,217 +0,0 @@
|
||||
"""
|
||||
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 <a> 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 <th> ou primeira linha de <td>
|
||||
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
|
||||
|
||||
@@ -55,6 +55,18 @@ class SeleniumSpider:
|
||||
chrome_options.add_argument('--disable-gpu')
|
||||
chrome_options.add_argument('--window-size=1920,1080')
|
||||
|
||||
# Desabilita imagens para acelerar carregamento
|
||||
chrome_prefs = {
|
||||
'profile.managed_default_content_settings.images': 2
|
||||
}
|
||||
chrome_options.add_experimental_option('prefs', chrome_prefs)
|
||||
|
||||
# Estratégia de carregamento mais rápida
|
||||
try:
|
||||
chrome_options.page_load_strategy = 'eager'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 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')
|
||||
|
||||
@@ -92,20 +104,17 @@ class SeleniumSpider:
|
||||
self.driver.quit()
|
||||
logger.info("Driver fechado")
|
||||
|
||||
def get_page(self, url, wait_time=3):
|
||||
def get_page(self, url, wait_time=0):
|
||||
"""
|
||||
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"))
|
||||
EC.presence_of_element_located((By.TAG_NAME, "table"))
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Timeout esperando elementos: {e}")
|
||||
@@ -113,7 +122,7 @@ class SeleniumSpider:
|
||||
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)
|
||||
self.get_page(url, wait_time=0)
|
||||
|
||||
# Obtém HTML após renderização
|
||||
html = self.driver.page_source
|
||||
@@ -153,7 +162,7 @@ class SeleniumSpider:
|
||||
logger.info(f"Processando categoria: {category_name}")
|
||||
logger.info(f"{'='*80}")
|
||||
|
||||
self.get_page(url, wait_time=3)
|
||||
self.get_page(url, wait_time=0)
|
||||
|
||||
# Obtém HTML após renderização
|
||||
html = self.driver.page_source
|
||||
@@ -194,24 +203,105 @@ class SeleniumSpider:
|
||||
if not cells:
|
||||
continue
|
||||
|
||||
# Destacar visualmente a linha atual no navegador
|
||||
try:
|
||||
self.driver.execute_script(
|
||||
"""
|
||||
(function(tableIndex, rowIndex){
|
||||
try {
|
||||
if (!window.__scrapeStyleInjected) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '.__scrapeHighlight{outline:2px solid #e11d48;background:rgba(225,29,72,0.08);}';
|
||||
document.head.appendChild(style);
|
||||
window.__scrapeStyleInjected = true;
|
||||
}
|
||||
document.querySelectorAll('tr.__scrapeHighlight').forEach(el=>el.classList.remove('__scrapeHighlight'));
|
||||
let tables = document.querySelectorAll('table.wikitable');
|
||||
if (!tables || tables.length===0) tables = document.querySelectorAll('table');
|
||||
const t = tables[tableIndex];
|
||||
if (!t) return;
|
||||
const trs = t.querySelectorAll('tr');
|
||||
const r = trs[rowIndex+1]; // +1 para ignorar cabeçalho
|
||||
if (!r) return;
|
||||
r.classList.add('__scrapeHighlight');
|
||||
r.scrollIntoView({block:'center'});
|
||||
} catch(e) {}
|
||||
})(arguments[0], arguments[1]);
|
||||
""",
|
||||
table_idx, row_idx
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Processa cada célula
|
||||
for cell_idx, cell in enumerate(cells):
|
||||
if cell_idx >= len(headers):
|
||||
break
|
||||
|
||||
header = headers[cell_idx]
|
||||
header_clean = clean_text(header)
|
||||
header_key = header_clean.casefold()
|
||||
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)
|
||||
# Extrai possíveis imagens da célula
|
||||
imgs = cell.find_all('img')
|
||||
img_srcs = []
|
||||
for im in imgs:
|
||||
src = im.get('src') if im else None
|
||||
if not src:
|
||||
continue
|
||||
if not src.startswith('http'):
|
||||
src = urljoin(url, src)
|
||||
img_srcs.append(src)
|
||||
|
||||
# 1) Coluna sem título: considerar como ícone do item; ignorar Tick/Cross
|
||||
if not header_clean:
|
||||
for img_src in img_srcs:
|
||||
filename = img_src.split('/')[-1].lower()
|
||||
if 'tick' in filename or 'cross' in filename:
|
||||
continue
|
||||
if 'image_url' not in item_data:
|
||||
item_data['image_url'] = img_src
|
||||
break
|
||||
# nada mais a fazer nesta célula
|
||||
continue
|
||||
|
||||
# 2) Coluna "agrupável": mapear Tick/Cross para 1/0 e não baixar imagem
|
||||
if 'agrup' in header_key:
|
||||
assigned = False
|
||||
for img_src in img_srcs:
|
||||
filename = img_src.split('/')[-1].lower()
|
||||
if 'tick' in filename:
|
||||
item_data['agrupavel'] = 1
|
||||
assigned = True
|
||||
break
|
||||
if 'cross' in filename:
|
||||
item_data['agrupavel'] = 0
|
||||
assigned = True
|
||||
break
|
||||
if not assigned:
|
||||
text_key = cell_text.casefold()
|
||||
if text_key == 'sim':
|
||||
item_data['agrupavel'] = 1
|
||||
assigned = True
|
||||
elif text_key in ('não', 'nao'):
|
||||
item_data['agrupavel'] = 0
|
||||
assigned = True
|
||||
# nunca definir image_url a partir desta coluna
|
||||
continue
|
||||
|
||||
# 3) Demais colunas: mapear campos; só usar imagem se não for Tick/Cross
|
||||
# Preferir texto para campos conhecidos
|
||||
self._map_column_to_field(item_data, header_clean, cell_text)
|
||||
|
||||
# Considerar imagem apenas se ainda não temos image_url e não for Tick/Cross
|
||||
if 'image_url' not in item_data and img_srcs:
|
||||
for img_src in img_srcs:
|
||||
filename = img_src.split('/')[-1].lower()
|
||||
if 'tick' in filename or 'cross' in filename:
|
||||
continue
|
||||
item_data['image_url'] = img_src
|
||||
break
|
||||
|
||||
# Salva item se tiver nome
|
||||
if item_data.get('nome'):
|
||||
@@ -312,6 +402,11 @@ class SeleniumSpider:
|
||||
if not image_url:
|
||||
return
|
||||
|
||||
# Nunca baixar ícones Tick/Cross
|
||||
basename = image_url.split('/')[-1].lower()
|
||||
if 'tick' in basename or 'cross' in basename:
|
||||
return
|
||||
|
||||
categoria = item_data.get('categoria', 'uncategorized')
|
||||
nome = item_data.get('nome', 'unknown')
|
||||
|
||||
@@ -392,8 +487,8 @@ class SeleniumSpider:
|
||||
try:
|
||||
self.scrape_category(category_name, url)
|
||||
|
||||
# Delay entre categorias
|
||||
time.sleep(2)
|
||||
# Delay mínimo entre categorias (reduzido)
|
||||
time.sleep(0.2)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao processar {category_name}: {e}")
|
||||
|
||||
Reference in new issue
Block a user