drogon/explorer/explorer.py

275 lines
9.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
import sys
sys.path.append('..')
import uuid
import datetime
from time import sleep
from bs4 import BeautifulSoup
import re
from random import randint
from core.mysql_wrapper import get_anunciosdb, get_tasksdb
from core.scrapping_utils import UrlAttack
from core.alerts import alert_master
class Explorer():
sleep_time_no_work = 60
sleep_time_no_service = 600
working_hours = {'start': datetime.time(9, 0, 0),
'end': datetime.time(18, 0, 0)}
monthly_capture_target = 1000
2018-09-09 19:42:52 +02:00
def __init__(self):
try:
self.anunciosdb = get_anunciosdb()
2018-09-09 19:42:52 +02:00
self.tasksdb = get_tasksdb()
2018-08-14 20:02:40 +02:00
except:
print("Could not connect to anuncios DB")
self.max_db_retries = 3
self.db_retries = 0
self.max_queue_retries = 3
self.queue_retries = 0
2018-08-14 20:02:40 +02:00
def start(self):
while True:
if not self.there_is_work():
sleep(Explorer.sleep_time_no_work)
continue
if not self.database_is_up():
alert_master("SQL DOWN", "El explorer informa de que SQL esta caida. Actividad detenida")
self.stop(self)
if not self.queue_is_up():
alert_master("REDIS DOWN", "El explorer informa de que REDIS esta caido. Actividad detenida")
self.stop(self)
current_task = ExploringTask(self.compose_listing_url)
current_task.explore()
2018-08-14 20:02:40 +02:00
continue
self.stop()
def stop(self):
#TODO Detener el servicio
#Detener el servicio
pass
def there_is_work(self):
2018-09-09 19:42:52 +02:00
#TODO Añadir que no se trabaja si se ha lanzado tarea en los ultimos 10 minutos
"""
Funcion que agrupa las condiciones que se deben cumplir para poder trabajar
"""
if not self.in_working_hours():
return False
if self.get_referencias_acquired_today() >= self.get_max_referencias_for_today():
return False
if self.get_tasks_created_today() >= self.get_max_tasks_today():
return False
return True
def database_is_up(self):
while self.db_retries <= self.max_db_retries:
try:
self.anunciosdb.ping()
self.db_retries = 0
return True
except:
sleep(Explorer.sleep_time_no_service)
self.db_retries = self.db_retries + 1
return False
def queue_is_up(self):
#TODO Comprobar que Redis esta vivo
while self.queue_retries <= self.max_queue_retries:
try:
#codigo que testea si redis esta vivo
self.queue_retries = 0
return True
except:
sleep(Explorer.sleep_time_no_service)
self.queue_retries = self.queue_retries + 1
return False
def in_working_hours(self):
2018-09-09 19:42:52 +02:00
return Explorer.working_hours['start'] <= datetime.datetime.now().time() <= Explorer.working_hours['end']
def get_referencias_acquired_today(self):
"""
Cuenta cuantas nuevas referencias han aparecido en las ultimas 24 horas
"""
2018-09-09 19:42:52 +02:00
query_statement = """ SELECT count(referencia)
FROM primera_captura_full
WHERE fecha_captura >= now() - INTERVAL 1 DAY;
"""
cursor_result = self.anunciosdb.query(query_statement)
2018-09-09 19:42:52 +02:00
return cursor_result.fetchone()[0]
def get_max_referencias_for_today(self):
"""
Calcula la cantidad objetivo para las ultimas 24 horas en base a la
diferencia con el objetivo mensual
"""
2018-09-09 19:42:52 +02:00
query_statement = """ SELECT count(referencia)
FROM primera_captura_full
WHERE fecha_captura >= now() - INTERVAL 30 DAY;
"""
cursor_result = self.anunciosdb.query(query_statement)
2018-09-09 19:42:52 +02:00
new_referencias_last_30 = cursor_result.fetchone()[0]
deviation = (Explorer.monthly_capture_target - new_referencias_last_30) / Explorer.monthly_capture_target
2018-09-09 19:42:52 +02:00
max_referencias = (Explorer.monthly_capture_target/30) * (1 + deviation)
return max_referencias
2018-09-09 19:42:52 +02:00
def get_tasks_created_today(self):
"""
Mira en el task log cuantas tareas se han iniciado en las ultimas 24 horas
"""
query_statement = """ SELECT count(uuid)
2018-09-09 19:42:52 +02:00
FROM exploring_tasks_logs
WHERE status = 'Attacked'
AND write_time >= now() - INTERVAL 1 DAY;
"""
cursor_result = self.tasksdb.query(query_statement)
2018-09-09 19:42:52 +02:00
tasks_created_today = cursor_result.fetchone()[0]
return tasks_created_today
def get_max_tasks_today(self):
"""
Calcula el maximo diario de intentos en forma de tareas, en base al
maximo de capturas mas un multiplicador
"""
return (self.get_max_referencias_for_today() / 30) * 6
def compose_listing_url(self):
"""
Genera URLs de manera aleatoria
:return:
"""
raiz = 'https://www.idealista.com/'
tipo = randint(1,2)
ciudad = 'barcelona'
numero = randint(1,30)
url = raiz + tipo + '-garajes/' + ciudad + '-' + ciudad + '/' + \
'pagina-' + numero + '.htm'
return url
class ExploringTask():
def __init__(self, url):
self.anunciosdb = get_anunciosdb()
self.tasksdb = get_tasksdb()
self.target_url = url
self.id = str(uuid.uuid4())
self._update_status('Pending')
2018-08-14 20:02:40 +02:00
def _update_status(self, new_status):
2018-08-14 20:02:40 +02:00
self.status = new_status
self._log_in_tasksdb()
def explore(self):
attack = UrlAttack(self.target_url)
attack.attack()
self._update_status('Attacked')
if attack.success:
self._validate_referencias(attack.get_text())
2018-08-14 20:02:40 +02:00
self._extract_referencias(attack.get_text())
if self.referencias:
self._update_status('Referencias ready')
self._post_tasks_to_queue()
self._update_status('Sent to Queue')
elif self.there_are_referencias:
self._update_status('Failure - No new referencias in HTML')
else:
self._update_status('Failure - HTML with no referencias')
else:
self._update_status('Failure - Bad request')
def _log_in_tasksdb(self):
"""
Graba en la base de datos de tareas un registro con el UUID de la tarea,
un timestamp y el status
"""
query_statement = """INSERT INTO exploring_tasks_logs
(uuid, write_time, status)
VALUES (%(uuid)s, NOW(), %(status)s)"""
query_parameters = {'uuid': self.id,
'status': self.status}
self.tasksdb.query(query_statement, query_parameters)
def _validate_referencias(self, html):
"""
Comprueba que las etiquetas sigan el formato de un anuncio.
Lanza una advertencia si no es así.
"""
soup = BeautifulSoup(html, 'html5lib')
ads = soup.find_all(class_ = "item")
pattern = "^[0-9]{3,20}$"
for ad in ads:
if not re.match(pattern, ad["data-adid"]):
alert_master("Alerta - Referencias no válidas",
"""Una tarea de exploración ha considerado inválida
una referencia. El texto de la referencia era : {}
""".format(ad["data-adid"]))
break
2018-08-14 20:02:40 +02:00
def _extract_referencias(self, html):
"""
Saca referencias de HTML, descarta las que ya exiten en la base de datos
de capturas, y guarda si han aparecido listings y si hay alguno nuevo
"""
soup = BeautifulSoup(html, 'html5lib')
ads = soup.find_all(class_ = "item")
self.there_are_referencias = bool(ads)
2018-08-14 20:02:40 +02:00
self.referencias = []
for ad in ads:
if self._is_new_listing(ad["data-adid"]):
self.referencias.append(ad["data-adid"])
2018-08-14 20:02:40 +02:00
def _is_new_listing(self, referencia):
"""
Comprueba si el listing ya existe en la base de datos de anuncios
"""
2018-08-14 20:02:40 +02:00
query_statement = """SELECT count(referencia)
FROM capturas
WHERE referencia = %s"""
query_params = (referencia,)
cursor_result = self.anunciosdb.query(query_statement, query_params)
result = cursor_result.fetchone()
if result[0] > 0:
return False
else:
return True
def _post_tasks_to_queue(self):
#TODO Mandar las referencias a redis
pass