
¿Cómo conseguir una BBDD de propiedades en Chile?
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:
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:
1class GetDataChilePropiedades:23def __init__(self,region,4 type_searching,5 type_house,6 min_publish_date,7 max_publish_date):8 self.region = region9 self.type_searching = type_searching10 self.type_house = type_house11 self.min_publish_date = min_publish_date12 self.max_publish_date = max_publish_date1314def getdata(self):1516 extracted_data = []17 url = 'https://chilepropiedades.cl/propiedades/{}/{}/{}/0'.18 format(self.type_searching, self.type_house,self.region)1920 headers = {21 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)22 AppleWebKit/537.3623 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'24 }25 response = requests.get(url, headers=headers)26 soup = BeautifulSoup(response.content, 'html.parser')27 element_page = soup.find_all('div',28 class_='clp-results-text-container d-none d-sm-block col-sm-6 text-right')2930 max_count_page = element_page[0].text31 pattern = r"Total de páginas:\s+(\d+)"32 match = re.search(pattern, max_count_page)33 if match:34 total_pages = match.group(1)35 else:36 return {37 'response': extracted_data,38 'status': True39 }4041 table_count_page = [i for i in range(int(total_pages))]4243 for page in table_count_page:44 try:45 url =4647 headers = {48 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)49 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'50 }51 response = requests.get(url, headers=headers)52 soup = BeautifulSoup(response.content, 'html.parser')53 element_page = soup.find_all('p',54 class_='mt-3 p-3 clp-highlighted-container text-center')5556 time.sleep(3)5758 if element_page != []:59 continue6061 # Find all the publication elements62 elements = soup.find_all('div', class_='clp-publication-list')63 list_element_public = elements[0].find_all('div',64 class_='clp-publication-element clp-highlighted-container')656667 # Iterate over each publication element68 for element in list_element_public:6970 try:71 date_publish = element.find('div', class_='text-center clp-publication-date').text.strip()72 date_publish_datetime = datetime.strptime(date_publish, "%d/%m/%Y")73 date_publish_datetime = str(date_publish_datetime)[:10]74 except:75 date_publish_datetime = '1990-01-01'7677 ## Filter by date78 if (date_publish_datetime >= self.min_publish_date) and (date_publish_datetime <= self.max_publish_date):7980 # Images81 try:82 img_publish_list = element.find('a', class_='clp-listing-image-link')83 img_element = img_publish_list.find('picture').find('img')['src']84 except:85 img_element = ''8687 # Extract rooms88 try:89 rooms_span = element.find('span', title='Habitaciones')90 rooms = rooms_span.find('span', class_='clp-feature-value').text if rooms_span else None91 except:92 rooms = 09394 # Extract bathrooms95 try:96 bathrooms_span = element.find('span', title='Baños')97 bathrooms = bathrooms_span.find('span', class_='clp-feature-value').text if bathrooms_span98 else None99 except:100 bathrooms = 0101102 # Extract value price103 try:104 value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '1'})105 value_price_clp = value_spans[1].text.strip() if len(value_spans) > 1 else None106 except:107 value_price_clp = 0108109 # Extract value currency110 try:111 value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '1'})112 value_price_currency_clp = value_spans[0].text.strip() if len(value_spans) > 1 else None113 except:114 value_price_currency_clp = 0115116 # Extract value price117 try:118 value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '3'})119 value_price_uf = value_spans[1].text.strip() if len(value_spans) > 1 else None120 except:121 value_price_uf = 0122123 # Extract value currency124 try:125 value_spans = element.find_all('span', class_='clp-value-container', attrs={'valueunit': '3'})126 value_price_currency_uf = value_spans[0].text.strip() if len(value_spans) > 1 else None127 except:128 value_price_currency_uf = 0129130 # Extract parking lots131 try:132 parking_span = element.find('span', title='Estacionamientos')133 parking = parking_span.find('span', class_='clp-feature-value').text if parking_span else None134 except:135 parking = 0136137 try:138 h2_element = element.find('h2', class_='publication-title-list')139 a_tag = h2_element.find('a') if h2_element else None140 href = a_tag['href'] if a_tag else None141 url_element = 'https://chilepropiedades.cl' + href142 except:143 url_element = ''144145 try:146 data_element = element.find('div', class_='d-md-flex mt-2 align-items-center')147 data_element = data_element.find('h3', class_='sub-codigo-data').text.split('/')148 type_action = data_element[0].strip()149 type_property = data_element[1].strip()150 type_province = data_element[2].strip()151 except:152 type_action = ''153 type_property = ''154 type_province = ''155156 # code publication157 try:158 code_publication = element.find('div', class_='d-md-flex mt-2 align-items-center')159 code_publication = code_publication.find('span', class_='light-bold').next_sibling.strip()160 except:161 code_publication = ''162163 # get latitude and longitude164 response = requests.get(url_element, headers=headers)165 time.sleep(3)166 soup = BeautifulSoup(response.content, 'html.parser')167 script_element = soup.find_all('script')168169 for i in range(len(script_element)):170 location_pattern = r'var publicationLocation = \[\s*(-?\d+\.\d+),\s*(-?\d+\.\d+)\s*\];'171 matches = re.findall(location_pattern, str(script_element[i]))172 if matches:173 latitude, longitude = matches[0]174 break175 else:176 latitude = None177 longitude = None178179 # Add to the list180 extracted_data.append({181 'rooms': rooms,182 'bathrooms': bathrooms,183 'value_price_clp': value_price_clp,184 'value_price_currency_clp': value_price_currency_clp,185 'value_price_uf': value_price_uf,186 'value_price_currency_uf': value_price_currency_uf,187 'parking': parking,188 'url': url_element,189 'type_action': type_action,190 'type_property': type_property,191 'type_province': type_province,192 'latitude': latitude,193 'longitude': longitude,194 'page': page,195 'region': self.region,196 'type_searching': self.type_searching,197 'type_house': self.type_house,198 'date_publish': date_publish,199 'code_publication': code_publication,200 'image_picture': img_element,201 'web': 'chilepropiedades',202 })203 elif (date_publish_datetime < self.min_publish_date):204 return {205 'response': extracted_data,206 'status': True207 }208 except:209 pass210 return {211 'response': extracted_data,212 'status': True213 }
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:
1'https://chilepropiedades.cl/propiedades/{}/{}/{}/{}'.2format(self.type_searching, self.type_house, self.region,3page)
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:
1docker build -t my-flask-app .2docker 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:
1import requests23url = "http://localhost:8000/webscrapping"4data = {5"region": "santiago",6"type_searching": "arriendo-mensual",7"type_house": "departamento",8"min_publish_date": "2024-05-23",9"max_publish_date": "2024-05-24"10}1112response = 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