drogon/explorer/explorer.py

287 lines
9.4 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
from db_layer.capturing_tasks_interface import capturing_interface
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
ad_types = {'1': 'alquiler',
'2': 'venta'}
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()
current_task = ExploringTask(self.compose_listing_url())
current_task.explore()
if current_task.status == 'Referencias ready':
referencias = current_task.get_referencias()
for referencia in referencias:
capturing_interface.create_capturing_task(referencia)
current_task._update_status("Sent to queue")
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):
"""
Funcion que agrupa las condiciones que se deben cumplir para poder trabajar
"""
if self.check_if_recent_task():
return False
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 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 check_if_recent_task(self):
"""
Mira si se ha creado alguna tarea recientemente
"""
query_statement = """ SELECT count(uuid)
FROM exploring_tasks_logs
WHERE status = 'Attacked'
AND write_time >= now() - INTERVAL 10 MINUTE
"""
cursor_result = self.tasksdb.query(query_statement)
return cursor_result.row_count
def compose_listing_url(self):
"""
Genera URLs de manera aleatoria
:return:
"""
root = 'https://www.idealista.com/'
type = ad_type[str(randint(1,2))]
city = 'barcelona'
page_number = str(randint(1,30))
url = root + type + '-garajes/' + city + '-' + city + '/' + \
'pagina-' + page_number + '.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')
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 get_referencias(self):
"""
Devuelve las referencias, si las hay
"""
if self.referencias:
return self.referencias
else:
return None
2018-10-13 18:17:05 +02:00
if __name__ == 'main':
explorer = Explorer()
explorer.start()