Files
pagodera/iconScript/tibiawiki/pipelines.py
T
Evandro Fonseca Junior 29ba5aece0 feat: add TibiaWiki scraper project and update .gitignore
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.
2025-10-30 00:55:25 -03:00

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