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:
1 parent
bf12f2c6f1
commit
29ba5aece0
27 files changed
+1702
-4
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user