Integrates a Scrapy-based TibiaWiki scraper (spider, pipelines, SQLite/Images settings, README). Updates .gitignore to exclude generated data directories (/iconScript/data, /data/images) to keep the repository clean.
207 lines
6.3 KiB
Python
207 lines
6.3 KiB
Python
"""
|
|
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
|
|
|