.

FocoAnalytics.

¿Cómo conseguir una BBDD de propiedades en Chile? (Post N° 3)

Cover Image for ¿Cómo conseguir una BBDD de propiedades en Chile? (Post N° 3)
Francisco Macaya
Francisco Macaya

Actualmente, la industria inmobiliaria en Chile enfrenta una situación complicada debido a las altas tasas de interés y al significativo aumento del valor de la UF en los últimos años. Debido a esto, es relevante contar con la mayor cantidad de información posible sobre los precios de departamentos y viviendas para tomar decisiones. El Portal Inmobiliario es probablemente la fuente más confiable y extensa para obtener información sobre proyectos inmobiliarios. Sin embargo, existen otras alternativas que, aunque menos populares, pueden ofrecer una muestra representativa del comportamiento de precios y disponibilidad de proyectos en Chile.

Chilepropiedades es una página web donde se publican propiedades para comprar y arrendar en Chile. Esta plataforma proporciona información detallada sobre precios, número de dormitorios, baños y la ubicación de las propiedades. Este artículo entrega una API que permite extraer información de este sitio mediante web scraping, utilizando parámetros de entrada para la búsqueda de información, de la misma manera que lo haría un usuario en el navegador.

Es importante destacar que este proceso de web scraping depende de los elementos del HTML del sitio web. Si en el futuro se realizan modificaciones en el HTML, es probable que esta API falle o necesite ajustes para adaptarse a los cambios realizados, como ocurre con cualquier proceso de web scraping.

¿Cómo fue construida la base de datos?

ChilePropiedades es un portal que no tiene ningún elemento de JavaScript que obligue a usar Selenium (una biblioteca de Python más avanzada para web scraping), por lo que la extracción de la información es más simple y rápida. En este caso, se utilizó BeautifulSoup para extraer los elementos del HTML junto con sus valores. Para hacer esto, se tuvieron que entregar los parámetros de entrada en la búsqueda, de la misma manera que lo haría un usuario al buscar información en el sitio. Estos parámetros se muestran en la siguiente imagen:

Meta Developer Dashboard

El sitio web contiene tres parámetros de entrada, que son:

  • - Tipo de Búsqueda: Es el tipo de búsqueda del usuario. Sus valores pueden ser arrendar, comprar o arriendo diario.
  • - Tipo de Propiedad: Es el tipo de propiedad que el usuario quiere buscar en la página. Hay un conjunto variado de tipos de propiedades, desde bodegas hasta terrenos industriales. La gran mayoría de las propiedades se concentran en departamentos y casas.
  • - Ubicación: Es la ubicación de la propiedad que se quiere buscar en el sitio. La ubicación puede ser desde comunas hasta regiones de Chile.

Adicionalmente, para poder extraer la información de forma cronológica, se construyeron dos parámetros más como inputs de la API, permitiendo extraer la información en rangos de tiempo específicos.

  • - Fecha mínima de publicación: Define la fecha a partir de la cual se quiere comenzar a extraer la información de las propiedades publicadas.
  • - Fecha máxima de publicación: Define la fecha hasta la cual se quiere extraer la información de las propiedades publicadas.

Con todos estos parámetros, se construyó una función en Python que extrae la información de cada una de las propiedades de la siguiente manera:

class GetDataChilePropiedades:

def __init__(self,region, 
                  type_searching, 
                  type_house, 
                  min_publish_date, 
                  max_publish_date):
    self.region = region
    self.type_searching = type_searching
    self.type_house = type_house
    self.min_publish_date = min_publish_date
    self.max_publish_date = max_publish_date

def getdata(self):

    extracted_data = []
    url = 'https://chilepropiedades.cl/propiedades/{}/{}/{}/0'.
    format(self.type_searching, self.type_house,self.region)

    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
        AppleWebKit/537.36 
        (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
    }
    response = requests.get(url, headers=headers)
    soup = BeautifulSoup(response.content, 'html.parser')
    element_page = soup.find_all('div', 
    class_='clp-results-text-container d-none d-sm-block col-sm-6 text-right')
    
    max_count_page = element_page[0].text
    pattern = r"Total de páginas:\s+(\d+)"
    match = re.search(pattern, max_count_page)
    if match:
        total_pages = match.group(1)
    else:
        return {
            'response': extracted_data,
            'status': True
        }

    table_count_page = [i for i in range(int(total_pages))]
    
    for page in table_count_page:
        try:    
            url =   

            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) 
                AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
            }
            response = requests.get(url, headers=headers)
            soup = BeautifulSoup(response.content, 'html.parser')
            element_page = soup.find_all('p', 
            class_='mt-3 p-3 clp-highlighted-container text-center')

            time.sleep(3)

            if element_page != []:
                continue

            # Find all the publication elements
            elements = soup.find_all('div', class_='clp-publication-list')
            list_element_public = elements[0].find_all('div', 
            class_='clp-publication-element clp-highlighted-container')


            # Iterate over each publication element
            for element in list_element_public:
                
                try:
                    date_publish = element.find('div', class_='text-center clp-publication-date').text.strip()
                    date_publish_datetime = datetime.strptime(date_publish, "%d/%m/%Y")                        
                    date_publish_datetime = str(date_publish_datetime)[:10]
                except:
                    date_publish_datetime = '1990-01-01'
                
                ## Filter by date
                if (date_publish_datetime >= self.min_publish_date) and (date_publish_datetime <= self.max_publish_date):

                    # Images 
                    try:
                        img_publish_list = element.find('a', class_='clp-listing-image-link')
                        img_element = img_publish_list.find('picture').find('img')['src']
                    except:
                        img_element = ''

                    # Extract rooms
                    try:
                        rooms_span = element.find('span', title='Habitaciones')
                        rooms = rooms_span.find('span', class_='clp-feature-value').text if rooms_span else None
                    except:
                        rooms = 0

                    # Extract bathrooms
                    try:
                        bathrooms_span = element.find('span', title='Baños')
                        bathrooms = bathrooms_span.find('span', class_='clp-feature-value').text if bathrooms_span 
                        else None
                    except:
                        bathrooms = 0

                    # Extract value price
                    try:
                        value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '1'})
                        value_price_clp = value_spans[1].text.strip() if len(value_spans) > 1 else None
                    except:
                        value_price_clp = 0

                    # Extract value currency
                    try:
                        value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '1'})
                        value_price_currency_clp = value_spans[0].text.strip() if len(value_spans) > 1 else None
                    except:
                        value_price_currency_clp = 0

                    # Extract value price
                    try:
                        value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '3'})
                        value_price_uf = value_spans[1].text.strip() if len(value_spans) > 1 else None
                    except:
                        value_price_uf = 0

                    # Extract value currency
                    try:
                        value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '3'})
                        value_price_currency_uf = value_spans[0].text.strip() if len(value_spans) > 1 else None
                    except:
                        value_price_currency_uf = 0

                    # Extract parking lots
                    try:
                        parking_span = element.find('span', title='Estacionamientos')
                        parking = parking_span.find('span', class_='clp-feature-value').text if parking_span else None
                    except:
                        parking = 0  

                    try:
                        h2_element = element.find('h2', class_='publication-title-list')
                        a_tag = h2_element.find('a') if h2_element else None
                        href = a_tag['href'] if a_tag else None
                        url_element = 'https://chilepropiedades.cl' + href
                    except: 
                        url_element = ''      

                    try:
                        data_element = element.find('div', class_='d-md-flex mt-2 align-items-center')
                        data_element = data_element.find('h3', class_='sub-codigo-data').text.split('/')
                        type_action = data_element[0].strip()
                        type_property = data_element[1].strip()
                        type_province = data_element[2].strip()
                    except:
                        type_action = ''
                        type_property = ''
                        type_province = ''

                    # code publication
                    try: 
                        code_publication = element.find('div', class_='d-md-flex mt-2 align-items-center')
                        code_publication = code_publication.find('span', class_='light-bold').next_sibling.strip()
                    except:
                        code_publication = ''

                    # get latitude and longitude
                    response = requests.get(url_element, headers=headers)
                    time.sleep(3)
                    soup = BeautifulSoup(response.content, 'html.parser')
                    script_element = soup.find_all('script')

                    for i in range(len(script_element)):
                        location_pattern = r'var publicationLocation = \[\s*(-?\d+\.\d+),\s*(-?\d+\.\d+)\s*\];'
                        matches = re.findall(location_pattern, str(script_element[i]))
                        if matches:
                            latitude, longitude = matches[0] 
                            break
                        else:
                            latitude = None
                            longitude = None

                        # Add to the list
                    extracted_data.append({
                        'rooms': rooms,
                        'bathrooms': bathrooms,
                        'value_price_clp': value_price_clp,
                        'value_price_currency_clp': value_price_currency_clp,
                        'value_price_uf': value_price_uf,
                        'value_price_currency_uf': value_price_currency_uf,
                        'parking': parking,
                        'url': url_element,
                        'type_action': type_action,
                        'type_property': type_property,
                        'type_province': type_province,
                        'latitude': latitude,
                        'longitude': longitude,
                        'page': page,
                        'region': self.region,
                        'type_searching': self.type_searching,
                        'type_house': self.type_house,
                        'date_publish': date_publish,
                        'code_publication': code_publication,
                        'image_picture': img_element,
                        'web': 'chilepropiedades',
                    })
                elif (date_publish_datetime < self.min_publish_date):
                    return {
                        'response': extracted_data,
                        'status': True
                    }
        except:
            pass
    return {
        'response': extracted_data,
        'status': True
    }

Este código contiene la lógica de la extracción de la información desde la página, dividiéndose en dos principales secciones.

  • - Página por página: Para esta sección, se utiliza la biblioteca requests y los parámetros del usuario para extraer la información del html resultante de la consulta del usuario. El sitio web genera URLs paramétricas según la búsqueda del usuario, donde cada página de búsqueda si diferencia por el numero de pagina, sin haber elementos adicionales para su interpretación. La URL estandar de busqueda es:
'https://chilepropiedades.cl/propiedades/{}/{}/{}/{}'.
format(self.type_searching, self.type_house, self.region, 
page)

Cualquier búsqueda del usuario empieza en la página 0, y dependiendo de la cantidad de propiedades publicadas, dependerá la máxima cantidad de páginas. Esta información se encuentra al final de la pagina inicial, en el elemento que dice: “Total de páginas: X”. Para extraer todo las propiedades se realizo un ciclo for, el cual recorre todas las páginas de la búsqueda del usuario.

  • - Información de cada propiedad: Luego de haber extraído el HTML de la página, se procede a extraer la información de cada una de las propiedades, mediante una iteración de las propiedades obtenidas de la pagina. En esta parte, se identificaron cada uno de los características de una propiedad, tal como: precio, número de baños, habitaciones, moneda, estacionamientos, imágenes, código de publicación, latitud y longitud.

Junto con esto, el codigo en python incluye una manera para poder extraer solo propiedades entre rango de fecha, permitiendo extraer propiedades que fueron publicadas entre dos periodos de tiempo especificos.

¿Cómo empaquetar la función dentro de una API?

Para poder dejar esta función dentro de un componente más estable, se construyó un contenedor con API Flask, el cual almacena la función construida en un endpoint llamado webscraping. Esta implementación recibe los parametros region, type_searching, type_house, min_publish_date y max_publish_date, los cuales son los mismos que se utilizan en la función de Python.

Para poder levantar el contenedor, se debe tener instalado Docker y ejecutar los siguientes comandos:

docker build -t my-flask-app .
docker run -d -p 8000:8000 my-flask-app

Se habilitará un endpoint en el puerto 8000 con el nombre de "/webscrapping", el cual puede probarse usando las siguientes lineas de código:

import requests

url = "http://localhost:8000/webscrapping"
data = {
"region": "santiago",
"type_searching": "arriendo-mensual",
"type_house": "departamento",
"min_publish_date": "2024-05-23",
"max_publish_date": "2024-05-24"
}

response = requests.post(url, json=data)

Conclusiones

Espero que esta implementación le pueda servir a alguien que esté buscando información de propiedades. Sabiendo que esto es una implementación de web scraping, que depende mucho del HTML, tiene sus pro y contra, ya que es una manera factible de poder extraer información del mercado inmobiliario hoy en día, pero dependerá mucho de los cambios del sitio . Si quiere tener mayores detalles del código, no dude en revisar el repositorio del código.

Si quiere tener mayores detalles del código, no dude en revisar el repositorio del código.

Repositorio: Github