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.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-30 00:55:25 -03:00
1 parent bf12f2c6f1
commit 29ba5aece0
27 files changed
+1702 -4

No files matched your search

+206
View File
@@ -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