feat: initial commit - Instagram giveaway analyzer & auto-inscriber
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
# Dependencies
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
venv/
|
||||
.env
|
||||
*.env*
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
"""Analizador de IA local simple para detección de sorteos."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from config import GIVEAWAY_KEYWORDS, AI_MODE
|
||||
|
||||
|
||||
class GiveawayAnalyzer:
|
||||
"""Analizador simple basado en reglas para detectar sorteos en texto."""
|
||||
|
||||
def __init__(self, mode="rule_based"):
|
||||
self.mode = mode
|
||||
self.keywords = GIVEAWAY_KEYWORDS
|
||||
|
||||
def is_giveaway(self, text):
|
||||
"""
|
||||
Determinar si un texto corresponde a un sorteo.
|
||||
|
||||
Args:
|
||||
text: Texto de la descripción o caption de Instagram
|
||||
|
||||
Returns:
|
||||
bool: True si parece un sorteo
|
||||
"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
text_lower = text.lower()
|
||||
|
||||
# Contar coincidencias de palabras clave
|
||||
matches = sum(1 for kw in self.keywords if kw.lower() in text_lower)
|
||||
|
||||
# Heurística: si hay al menos 2 palabras clave, es probable que sea un sorteo
|
||||
if matches >= 2:
|
||||
return True
|
||||
|
||||
# Patrones más específicos
|
||||
giveaway_patterns = [
|
||||
"#sorteo" in text_lower,
|
||||
"#giveaway" in text_lower,
|
||||
"sorteo" in text_lower and ("ganar" in text_lower or "win" in text_lower),
|
||||
"participa" in text_lower,
|
||||
"etiqueta" in text_lower or "tag" in text_lower,
|
||||
"comenta" in text_lower,
|
||||
]
|
||||
|
||||
if any(giveaway_patterns):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def extract_requirements(self, text):
|
||||
"""
|
||||
Extraer los requisitos del sorteo del texto.
|
||||
|
||||
Args:
|
||||
text: Texto del sorteo
|
||||
|
||||
Returns:
|
||||
dict: Diccionario con requisitos extraídos
|
||||
"""
|
||||
requirements = {
|
||||
"follow": False,
|
||||
"like": False,
|
||||
"comment": False,
|
||||
"tag_friends": False,
|
||||
"hashtag": None,
|
||||
"username": None
|
||||
}
|
||||
|
||||
text_lower = text.lower()
|
||||
|
||||
if "follow" in text_lower or "seguir" in text_lower:
|
||||
requirements["follow"] = True
|
||||
if "like" in text_lower or "dar like" in text_lower or "corazón" in text_lower:
|
||||
requirements["like"] = True
|
||||
if "comment" in text_lower or "comenta" in text_lower:
|
||||
requirements["comment"] = True
|
||||
if "tag" in text_lower or "etiqueta" in text_lower or "menciona" in text_lower:
|
||||
requirements["tag_friends"] = True
|
||||
|
||||
# Buscar hashtag
|
||||
import re
|
||||
hashtag_match = re.search(r"#(\w+)", text)
|
||||
if hashtag_match:
|
||||
requirements["hashtag"] = hashtag_match.group(1)
|
||||
|
||||
# Buscar usuario (@usuario)
|
||||
user_match = re.search(r"@(\w+)", text)
|
||||
if user_match:
|
||||
requirements["username"] = user_match.group(1)
|
||||
|
||||
return requirements
|
||||
|
||||
def analyze(self, text):
|
||||
"""
|
||||
Análisis completo de un texto de sorteo.
|
||||
|
||||
Args:
|
||||
text: Texto a analizar
|
||||
|
||||
Returns:
|
||||
dict: Resultado del análisis
|
||||
"""
|
||||
return {
|
||||
"is_giveaway": self.is_giveaway(text),
|
||||
"requirements": self.extract_requirements(text),
|
||||
"text": text[:100] + "..." if len(text) > 100 else text
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Instagram Giveaway Sorteo Configuration
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configuración de Instagram
|
||||
INSTAGRAM_USERNAME = os.getenv("INSTAGRAM_USERNAME", "tu_usuario")
|
||||
INSTAGRAM_PASSWORD = os.getenv("INSTAGRAM_PASSWORD", "tu_contraseña")
|
||||
TARGET_USERS = os.getenv("TARGET_USERS", "").split(",") if os.getenv("TARGET_USERS") else []
|
||||
|
||||
# Palabras clave para detectar sorteos
|
||||
GIVEAWAY_KEYWORDS = [
|
||||
"sorteo", "giveaway", "ganar", "win", "ganando", "ganada",
|
||||
"premio", "premios", "comparte", "share", "etiquetar", "tag",
|
||||
"comenta", "comment", "follow", "seguir"
|
||||
]
|
||||
|
||||
# Configuración de IA local (reglas simples)
|
||||
AI_MODE = "rule_based" # o "ollama" si tiene modelo local configurado
|
||||
|
||||
# Comentario automático
|
||||
AUTO_COMMENT = "¡Participo! #sorteo #ganar"
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"""Módulo para inscribirse automáticamente en sorteos."""
|
||||
import time
|
||||
import random
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class GiveawayInscriber:
|
||||
"""Clase para inscribir automáticamente en sorteos de Instagram."""
|
||||
|
||||
def __init__(self, scraper, analyzer, comment="¡Participo! #sorteo"):
|
||||
self.scraper = scraper
|
||||
self.analyzer = analyzer
|
||||
self.comment = comment
|
||||
self.successful_inscriptions = 0
|
||||
self.failed_attempts = 0
|
||||
|
||||
def inscription_comment(self, post_url, target_comment=None):
|
||||
"""
|
||||
Publicar un comentario en una publicación de sorteo.
|
||||
|
||||
Args:
|
||||
post_url: URL de la publicación
|
||||
target_comment: Comentario personalizado (usa self.comment por defecto)
|
||||
|
||||
Returns:
|
||||
bool: Éxito o no
|
||||
"""
|
||||
comment = target_comment or self.comment
|
||||
try:
|
||||
# Nota: Instagram no permite comentarios fáciles sin autenticación adecuada
|
||||
# Esta es la estructura; la implementación completa requiere
|
||||
# manejo de sesiones CSRF, tokens, etc.
|
||||
print(f"📝 Intentando comentar en: {post_url}")
|
||||
print(f" Comentario: {comment}")
|
||||
|
||||
# En un implementación completa, aquí se tendría:
|
||||
# - CSRF token extraction
|
||||
# - Session cookie management
|
||||
# - POST request al endpoint de comentarios
|
||||
# - Manejo de respuestas y rate limiting
|
||||
|
||||
# Simulación para demostración
|
||||
time.sleep(random.uniform(1, 3))
|
||||
self.successful_inscriptions += 1
|
||||
print("✅ Comentario publicado (simulación)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.failed_attempts += 1
|
||||
print(f"❌ Error al comentar: {e}")
|
||||
return False
|
||||
|
||||
def auto_enter(self, posts, min_keyword_match=2):
|
||||
"""
|
||||
Analizar publicaciones e inscribirse en las que son sorteos.
|
||||
|
||||
Args:
|
||||
posts: Lista de publicaciones (diccionarios con 'caption', 'url', etc.)
|
||||
min_keyword_match: Mínimo de coincidencias de palabras clave
|
||||
|
||||
Returns:
|
||||
list: Publicaciones en las que se inscribió
|
||||
"""
|
||||
inscribed = []
|
||||
|
||||
for post in posts:
|
||||
caption = post.get("caption", "")
|
||||
url = post.get("url", "")
|
||||
|
||||
if not caption or not url:
|
||||
continue
|
||||
|
||||
# Analizar con la IA local
|
||||
analysis = self.analyzer.analyze(caption)
|
||||
|
||||
if analysis["is_giveaway"]:
|
||||
print(f"\n🎯 Sorteo detectado en: {url}")
|
||||
print(f" Requerimientos: {analysis['requirements']}")
|
||||
|
||||
# Inscribirse (comentar)
|
||||
if self.inscription_comment(url):
|
||||
inscribed.append({
|
||||
"url": url,
|
||||
"caption": caption,
|
||||
"requirements": analysis["requirements"]
|
||||
})
|
||||
else:
|
||||
print(f"📄 Publicación normal (no sorteo): {url[:50] if url else 'N/A'}...")
|
||||
|
||||
return inscribed
|
||||
|
||||
def run_on_hashtag(self, hashtag, post_count=20):
|
||||
"""
|
||||
Ejecutar en un hashtag específico.
|
||||
|
||||
Args:
|
||||
hashtag: Hashtag a buscar (ej: "sorteo")
|
||||
post_count: Número de publicaciones a revisar
|
||||
"""
|
||||
print(f"🔍 Buscando sorteos con hashtag: #{hashtag}")
|
||||
posts = self.scraper.search_hashtag(hashtag, post_count)
|
||||
|
||||
if posts:
|
||||
print(f"📊 Encontradas {len(posts)} publicaciones")
|
||||
inscribed = self.auto_enter(posts)
|
||||
print(f"\n✅ Inscripciones completadas: {len(inscribed)}")
|
||||
return inscribed
|
||||
else:
|
||||
print("❌ No se encontraron publicaciones")
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🐍 Módulo de inscripción directo")
|
||||
print("Usa: from inscriber import GiveawayInscriber")
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Aplicación principal - Instagram Giveaway Sorteo Analyzer & Inscriber."""
|
||||
import sys
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Cargar variables de entorno
|
||||
load_dotenv()
|
||||
|
||||
from scraping.instagram import InstagramScraper
|
||||
from ai.analyzer import GiveawayAnalyzer
|
||||
from inscriber import GiveawayInscriber
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("🎯 Instagram Sorteo Analyzer & Auto-Inscriber")
|
||||
print("=" * 60)
|
||||
|
||||
# Verificar configuración
|
||||
username = os.getenv("INSTAGRAM_USERNAME")
|
||||
password = os.getenv("INSTAGRAM_PASSWORD")
|
||||
|
||||
if not username or not password:
|
||||
print("⚠️ Configuración faltante.")
|
||||
print(" Copia .env y establece INSTAGRAM_USERNAME y INSTAGRAM_PASSWORD")
|
||||
print(" Ejemplo: echo 'INSTAGRAM_USERNAME=tu_usuario' > .env")
|
||||
print(" echo 'INSTAGRAM_PASSWORD=tu_contraseña' >> .env")
|
||||
return
|
||||
|
||||
# Inicializar componentes
|
||||
print("\n🔧 Inicializando componentes...")
|
||||
scraper = InstagramScraper(username=username, password=password)
|
||||
analyzer = GiveawayAnalyzer(mode="rule_based")
|
||||
inscriber = GiveawayInscriber(scraper=scraper, analyzer=analyzer)
|
||||
|
||||
print("✅ Componentes listos")
|
||||
|
||||
# Modo de operación
|
||||
print("\n📋 Modos disponibles:")
|
||||
print(" 1. Buscar por hashtag (#sorteo, #giveaway)")
|
||||
print(" 2. Revisar feed personalizado")
|
||||
print(" 3. Analizar publicaciones existentes")
|
||||
|
||||
choice = input("\nElige una opción (1-3): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
hashtag = input("Hashtag a buscar (ej: sorteo): ").strip() or "sorteo"
|
||||
count = int(input("Número de publicaciones a revisar (default 20): ").strip() or "20")
|
||||
print(f"\n🚀 Iniciando búsqueda con hashtag: #{hashtag}")
|
||||
inscriber.run_on_hashtag(hashtag, count)
|
||||
|
||||
elif choice == "2":
|
||||
print("\n📱 Obteniendo feed...")
|
||||
feed = scraper.get_feed(count=15)
|
||||
if feed:
|
||||
print(f"📊 {len(feed)} publicaciones obtenidas del feed")
|
||||
inscriber.auto_enter(feed)
|
||||
else:
|
||||
print("❌ No se pudieron obtener publicaciones del feed")
|
||||
|
||||
elif choice == "3":
|
||||
# Permitir analizar publicaciones manualmente proporcionadas
|
||||
print("\n📝 Ingresa URLs o captions para analizar (enter vacío para terminar):")
|
||||
posts = []
|
||||
while True:
|
||||
entry = input("> ").strip()
|
||||
if not entry:
|
||||
break
|
||||
posts.append({"caption": entry, "url": entry})
|
||||
|
||||
if posts:
|
||||
print("\n🔍 Analizando publicaciones...")
|
||||
inscribed = inscriber.auto_enter(posts)
|
||||
print(f"\n✅ Total inscripciones: {len(inscribed)}")
|
||||
|
||||
else:
|
||||
print("❌ Opción no válida")
|
||||
|
||||
# Cerrar scraper
|
||||
scraper.close()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("¡Gracias por usar Instagram Sorteo Analyzer!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Módulo para interactuar con Instagram - scraping básico de feeds."""
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import quote_plus
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
class InstagramScraper:
|
||||
"""Clase básica para obtener contenido de Instagram."""
|
||||
|
||||
def __init__(self, username=None, password=None):
|
||||
self.username = username or "usuario_default"
|
||||
self.password = password or "password_default"
|
||||
self.session = requests.Session()
|
||||
self.base_url = "https://www.instagram.com"
|
||||
|
||||
def get_feed(self, count=10):
|
||||
"""Obtener publicaciones del feed (versión básica)."""
|
||||
# Nota: Instagram bloquea el scraping simple.
|
||||
# Esta es una estructura que se puede mejorar con headers adecuados
|
||||
# o usando la API oficial de Instagram.
|
||||
try:
|
||||
# Endpoint público para explorar
|
||||
url = f"{self.base_url}/explore/feed/"
|
||||
headers = {
|
||||
"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"
|
||||
}
|
||||
response = self.session.get(url, headers=headers, timeout=15)
|
||||
if response.status_code == 200:
|
||||
return self._parse_feed(response.text, count)
|
||||
else:
|
||||
print(f"Error accediendo a Instagram: {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"Excepción al obtener feed: {e}")
|
||||
return []
|
||||
|
||||
def _parse_feed(self, html, count):
|
||||
"""Parsear el feed de HTML."""
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
posts = []
|
||||
|
||||
# Buscar artículos o divs de publicaciones
|
||||
# La estructura de Instagram cambia frecuentemente, por esto es básico
|
||||
posts_divs = soup.find_all("div", class_="._aagw")[:count]
|
||||
|
||||
for div in posts_divs:
|
||||
try:
|
||||
# Título/caption aproximado
|
||||
caption = div.find("span", class_="._aacl _aacp _aacv _aacu")
|
||||
if caption:
|
||||
text = caption.get_text(strip=True)
|
||||
else:
|
||||
text = ""
|
||||
|
||||
# Enlace
|
||||
link = div.find("a")
|
||||
href = link.get("href") if link else ""
|
||||
full_url = f"{self.base_url}{href}" if href else ""
|
||||
|
||||
# Imagen
|
||||
img = div.find("img")
|
||||
img_url = img.get("src") if img else ""
|
||||
|
||||
posts.append({
|
||||
"url": full_url,
|
||||
"caption": text,
|
||||
"image": img_url,
|
||||
"likes": 0, # Valor por defecto
|
||||
"comments": 0
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return posts
|
||||
|
||||
def search_hashtag(self, hashtag, count=10):
|
||||
"""Buscar publicaciones por hashtag."""
|
||||
try:
|
||||
url = f"{self.base_url}/explore/tags/{quote_plus(hashtag.lstrip('/'))}/"
|
||||
headers = {
|
||||
"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"
|
||||
}
|
||||
response = self.session.get(url, headers=headers, timeout=15)
|
||||
if response.status_code == 200:
|
||||
return self._parse_feed(response.text, count)
|
||||
except Exception as e:
|
||||
print(f"Error buscando hashtag: {e}")
|
||||
return []
|
||||
|
||||
def close(self):
|
||||
"""Cerrar sesión."""
|
||||
self.session.close()
|
||||
Reference in New Issue
Block a user