Cambios notables. Creadas interfaces para la tabla de capturing task
y la tabla de capturas. Traslado todo lo relacionado a Geocoding a un servicio independiente del capturer. Replanteo totalmente el parseo del html, creando un objeto nuevo.
This commit is contained in:
parent
3bd8de0e02
commit
240a61649c
7 changed files with 474 additions and 262 deletions
|
|
@ -2,85 +2,45 @@ import sys
|
|||
sys.path.append('..')
|
||||
import uuid
|
||||
from time import sleep
|
||||
from core.mysql_wrapper import get_anunciosdb, get_tasksdb
|
||||
from bs4 import BeautifulSoup
|
||||
import re
|
||||
from mysql.capturing_tasks_interface import capturing_interface
|
||||
from mysql.capturas_interface import capturas_interface
|
||||
from core.scrapping_utils import UrlAttack
|
||||
from core.alerts import alert_master
|
||||
from capturer.geocoder import GeocodingTask
|
||||
|
||||
ads_root = 'https://www.idealista.com/inmueble/'
|
||||
|
||||
#TODO Crear la lista de campos
|
||||
|
||||
ad_fields_parameters = [{'name': 'referencia',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'precio',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'tamano_categorico',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'm2',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'telefono',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'texto_tipo',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'ciudad',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'distrito',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'barrio',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'calle',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'cubierta',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'puerta_auto',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'ascensor',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'alarma',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'circuito',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'personal',
|
||||
'search_method': '',
|
||||
'validation_method': ''},
|
||||
{'name': 'texto_libre',
|
||||
'search_method': '',
|
||||
'validation_method': ''}]
|
||||
|
||||
|
||||
def create_capturing_task(referencia, db_wrapper, uuid_exploring=None):
|
||||
|
||||
query_parameters = {'ad_url': ads_root + referencia,
|
||||
'uuid': str(uuid.uuid4()),
|
||||
'status': 'Pending'}
|
||||
class Capturer:
|
||||
|
||||
if uuid_exploring is None:
|
||||
query_statement = """INSERT INTO capturing_tasks_logs
|
||||
(uuid, write_time, status, url)
|
||||
VALUES (%(uuid)s, NOW(), %(status)s, %(ad_url)s)"""
|
||||
else:
|
||||
query_parameters['uuid_exploring'] = uuid_exploring
|
||||
query_statement = """INSERT INTO capturing_tasks_logs
|
||||
(uuid, write_time, status, url, fk_uuid_exploring)
|
||||
VALUES (%(uuid)s, NOW(), %(status)s, %(ad_url)s, %(uuid_exploring)s)"""
|
||||
sleep_time_no_work = 60
|
||||
minimum_seconds_between_tries = 120
|
||||
|
||||
def start(self):
|
||||
|
||||
#Juzgar si hay que currar
|
||||
while True:
|
||||
|
||||
if capturing_interface.get_pending_task() is None:
|
||||
sleep(Capturer.sleep_time_no_work)
|
||||
continue
|
||||
|
||||
if capturing_interface.seconds_since_last_try() < minimum_seconds_between_tries:
|
||||
sleep(Capturer.sleep_time_no_work)
|
||||
continue
|
||||
|
||||
task_parameters = capturing_interface.get_pending_task()
|
||||
|
||||
task = CapturingTask(task_parameters)
|
||||
task.capture()
|
||||
|
||||
if tasks.status = 'Data ready':
|
||||
ad_data = task.get_ad_data()
|
||||
else:
|
||||
continue
|
||||
|
||||
capturas_interface.insert_captura(ad_data)
|
||||
|
||||
db_wrapper.query(query_statement, query_parameters)
|
||||
|
||||
|
||||
class CapturingTask:
|
||||
|
|
@ -90,34 +50,16 @@ class CapturingTask:
|
|||
def __init__(self, parameters):
|
||||
self.uuid = parameters['uuid']
|
||||
self.ad_url = parameters['ad_url']
|
||||
self.uuid_exploring = parameters['uuid_exploring']
|
||||
self.uuid_exploring = parameters['fk_uuid_exploring']
|
||||
self.status = parameters['status']
|
||||
self.request_failures = 1
|
||||
self.geocode_status = "Pending"
|
||||
|
||||
self.tasksdb = get_tasksdb()
|
||||
|
||||
self._update_status('Loading')
|
||||
|
||||
def _update_status(self, new_status):
|
||||
self.status = new_status
|
||||
self._log_in_tasksdb()
|
||||
|
||||
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 capturing_tasks_logs
|
||||
(uuid, write_time, status, ad_url, fk_uuid_exploring)
|
||||
VALUES (%(uuid)s, NOW(), %(status)s, %(ad_url)s, %(fk_uuid_exploring)s)"""
|
||||
|
||||
query_parameters = {'uuid': self.uuid,
|
||||
'status': self.status,
|
||||
'ad_url': self.ad_url,
|
||||
'fk_uuid_exploring': self.uuid_exploring}
|
||||
|
||||
self.tasksdb.query(query_statement, query_parameters)
|
||||
capturing_interface.update_capturing_task(self.uuid, self.uuid_exploring,
|
||||
self.status, self.ad_url)
|
||||
|
||||
def capture(self):
|
||||
"""
|
||||
|
|
@ -135,25 +77,12 @@ class CapturingTask:
|
|||
if attack.success():
|
||||
self.html = attack.get_text()
|
||||
|
||||
with self._fields_not_present() as missing_fields:
|
||||
if missing_fields:
|
||||
alert_master('ERROR CAPTURER',
|
||||
'Los siguientes campos no estaban presentes {}. '
|
||||
'URL = {}'.format(missing_fields, self.ad_url))
|
||||
self._update_status('Dead ad')
|
||||
return
|
||||
|
||||
with self._fields_not_valid() as unvalid_fields:
|
||||
if unvalid_fields:
|
||||
alert_master('ERROR CAPTURER',
|
||||
'Los siguientes campos no tenian valores presentes {}'
|
||||
'URL = {}'.format(unvalid_fields, self.ad_url))
|
||||
self._update_status('Dead ad')
|
||||
return
|
||||
|
||||
#Extraer datos
|
||||
self.extract_data()
|
||||
|
||||
|
||||
self._update_status('Data ready')
|
||||
|
||||
else:
|
||||
self.request_failures += 1
|
||||
self._update_status('Fail {}'.format(self.request_failures))
|
||||
|
|
@ -162,97 +91,120 @@ class CapturingTask:
|
|||
|
||||
self._update_status('Surrender')
|
||||
|
||||
|
||||
def _read_fields(self):
|
||||
self.fields = []
|
||||
for field_parameters in ad_fields_parameters:
|
||||
self.fields.append(ScrapTargetField(field_parameters))
|
||||
|
||||
def _fields_not_present(self, html=self.html):
|
||||
"""
|
||||
Lee el HTML y devuelve los campos que no esten presentes
|
||||
"""
|
||||
#TODO Implementar campos optativos
|
||||
fields_not_present = []
|
||||
for field in self.fields:
|
||||
if not field.exists(html):
|
||||
fields_not_present.append(field.name)
|
||||
|
||||
return fields_not_present
|
||||
|
||||
def _fields_not_valid(self, html=self.html):
|
||||
"""
|
||||
Lee el HTML y devuelve los campos que no tengan valores validos
|
||||
"""
|
||||
fields_not_valid = []
|
||||
for field in self.fields:
|
||||
if not field.validate_value(html):
|
||||
fields_not_valid.append(field.name)
|
||||
|
||||
return fields_not_valid
|
||||
|
||||
def extract_data(self):
|
||||
self.ad_data = {}
|
||||
|
||||
for field in self.fields:
|
||||
self.ad_data[field.name] = field.get_value(self.html)
|
||||
#TODO Crear un objeto parser y ver que todo esta bien
|
||||
|
||||
def get_ad_data(self):
|
||||
return self.ad_data
|
||||
|
||||
def geocode(self):
|
||||
#TODO Hacer esta funcion bien
|
||||
# Construir direccion con formato adecuado
|
||||
geocode_tries = 0
|
||||
|
||||
geo_task = GeocodingTask(formated_address)
|
||||
|
||||
while geocode_tries < 3:
|
||||
geo_task.geocode()
|
||||
|
||||
if geo_task.get_request_status() == 200:
|
||||
google_status = geo_task.success_surrender_retry()
|
||||
|
||||
if google_status == 'Success':
|
||||
self.geocode_status = 'Success'
|
||||
self.geocode_results = geo_task.get_results()
|
||||
return
|
||||
elif google_status == 'Surrender':
|
||||
self.geocode_status = 'Surrender'
|
||||
return
|
||||
elif google_status == 'Retry':
|
||||
geocode_tries += 1
|
||||
|
||||
self.geocode_status = 'Surrender'
|
||||
return
|
||||
|
||||
|
||||
class ScrapTargetField:
|
||||
class AdHtmlParser:
|
||||
|
||||
def __init__(self, html_string):
|
||||
self.html = html_string
|
||||
|
||||
self.ad_fields = {'referencia': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'precio': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'tamano_categorico': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'm2': {
|
||||
'found': False,
|
||||
'optional': True,
|
||||
'value': None},
|
||||
'tipo_anuncio': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'calle': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'barrio': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'distrito': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'ciudad': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'cubierta': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'puerta_auto': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'ascensor': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'alarma': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'circuito': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'personal': {
|
||||
'found': False,
|
||||
'optional': False,
|
||||
'value': None},
|
||||
'telefono': {
|
||||
'found': False,
|
||||
'optional': True,
|
||||
'value': None}}
|
||||
|
||||
def parse(self):
|
||||
|
||||
soup = BeautifulSoup(self.html, 'html5lib' )
|
||||
|
||||
|
||||
|
||||
if soup.findall('link', {'rel': 'canonical'}) is not None:
|
||||
self.ad_fields['referencia']['value'] = re.findall(r'[0-9]{5,20}',
|
||||
str(soup.findall('link', {'rel': 'canonical'})[0]))[0]
|
||||
self.ad_fields['referencia']['found'] = True
|
||||
|
||||
if sopa.find_all('strong', {'class': 'price'}) is not None:
|
||||
self.ad_fields['precio']['value'] = ''.join(re.findall(r'[0-9]',
|
||||
str(sopa.find_all('strong', {'class': 'price'})[0])))
|
||||
self.ad_fields['precio']['found'] = True
|
||||
|
||||
if soup.find('div', {'class':'info-features'}) is not None:
|
||||
self.ad_fields['tamano_categorico']['value'] = sopa.find('div',
|
||||
{'class':'info-features'}).find('span').find('span').text
|
||||
self.ad_fields['tamano_categorico']['found'] = True
|
||||
|
||||
#TODO Seguir con los metodos de parseo
|
||||
|
||||
|
||||
|
||||
def validate(self):
|
||||
#TODO Implementar validacion para aquellos campos que lo necesiten
|
||||
|
||||
|
||||
def fields_missing(self):
|
||||
#TODO Iterar el diccionario para ver que todos los campos obligatorios estan
|
||||
|
||||
|
||||
|
||||
def __init__(self, target_parameters):
|
||||
self.name = target_parameters['name']
|
||||
self.search_method = target_parameters['search_method']
|
||||
self.validation_method = target_parameters['validation_method']
|
||||
|
||||
def exists(self, html):
|
||||
"""
|
||||
Busca el dato en un HTML
|
||||
"""
|
||||
if self.search_method(html) is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def validate_value(self, dato):
|
||||
"""
|
||||
Comprueba el valor y valida con la norma respectiva que sea lo esperado
|
||||
"""
|
||||
return self.validation_method(dato)
|
||||
|
||||
def get_value(self, html):
|
||||
"""
|
||||
Busca en un HTML el dato
|
||||
"""
|
||||
return self.search_method(html)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import requests
|
||||
|
||||
|
||||
class GeocodingCache:
|
||||
|
||||
cache_max_size = 1000
|
||||
|
||||
def __init__(self):
|
||||
self.geocoded_addresses = []
|
||||
|
||||
def address_in_cache(self, address):
|
||||
"""
|
||||
Comprueba si la direccion ya esta en la cache
|
||||
"""
|
||||
for geocoded_address in self.geocoded_addresses:
|
||||
if geocoded_address['address'] == address:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_coordinates(self, address):
|
||||
"""
|
||||
Recupera los datos asociados a la direccion
|
||||
"""
|
||||
for geocoded_address in self.geocoded_addresses:
|
||||
if geocoded_address['address'] == address:
|
||||
return geocoded_address['latitude'], \
|
||||
geocoded_address['longitude'], \
|
||||
geocoded_address['precision']
|
||||
return None
|
||||
|
||||
def add_address(self, address, latitude, longitude, precision):
|
||||
"""
|
||||
Añade la direccion a la cache y le hace sitio si es necesario
|
||||
"""
|
||||
if len(self.geocoded_addresses) >= cache_max_size:
|
||||
self.geocoded_addresses.pop()
|
||||
|
||||
self.geocoded_addresses.insert(0, {'address': address,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'precision': precision})
|
||||
|
||||
|
||||
class GeocodingTask:
|
||||
|
||||
url = 'https://maps.googleapis.com/maps/api/geocode/json'
|
||||
|
||||
request_parameters = {'region': 'es',
|
||||
'key': 'AIzaSyCnKj0WnsxVZcaoxeAYkuRw3cKRNGiISYA'}
|
||||
|
||||
geocoding_status_success = ['OK']
|
||||
geocoding_status_surrender = ['ZERO_RESULTS']
|
||||
geocoding_status_retry = ['OVER_QUERY_LIMIT',
|
||||
'REQUEST_DENIED',
|
||||
'INVALID_REQUEST',
|
||||
'UNKNOWN_ERROR']
|
||||
|
||||
def __init__(self, address):
|
||||
request_paremeters['address'] = address
|
||||
|
||||
def geocode(self):
|
||||
"""
|
||||
Lanza la peticion de gecoding al servicio de google
|
||||
"""
|
||||
self.response = requests.get(url, request_parameters)
|
||||
self.response_json = self.response.json()
|
||||
|
||||
def get_request_status(self):
|
||||
"""
|
||||
Devuelve el status HTTP de la request
|
||||
"""
|
||||
return self.response.status_code
|
||||
|
||||
def success_surrender_retry(self):
|
||||
"""
|
||||
Devuelve el estado del resultado desde el punto de vista de Google
|
||||
"""
|
||||
if self.response_json['status'] in geocoding_status_success:
|
||||
return "Success"
|
||||
elif self.response_json['status'] in geocoding_status_surrender:
|
||||
return "Surrender"
|
||||
else:
|
||||
return "Retry"
|
||||
|
||||
def get_results(self):
|
||||
"""
|
||||
Extrae los resultados del JSON de respuesta y los devuelve
|
||||
"""
|
||||
results = {'latitude': self.response_json['results'][0]['geometry']['location']['lat'],
|
||||
'longitude': self.response_json['results'][0]['geometry']['location']['lon'],
|
||||
'precision': self.response_json['results'][0]['geometry']['location_type']}
|
||||
|
||||
return results
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue