DEV Community

Pablo
Pablo

Posted on

Scrappeando propiedades con Python

Hace un tiempo estaba buscando departamento para alquilar y me frustraba entrar periódicamente a las páginas de propiedades sólo para encontrarme una y otra vez con los mismos inmuebles.

Buscar un departamento es una tarea tediosa y estresante, por eso, se me ocurrió dar vuelta el problema y transformarlo en algo divertido: crear un bot que busque las propiedades y avise vía chat cuando aparece una nueva.

El código es relativamente sencillo, menos de 100 líneas de python, pero de todos modos vamos a seguirlo paso por paso para ver qué es lo que hace.

URLS y Main

Primero notamos que tenemos un set con las URLs de búsqueda:


urls = {
    "https://www.argenprop.com/departamento-alquiler-barrio-chacarita-hasta-25000-pesos-orden-masnuevos",
    "https://www.zonaprop.com.ar/departamento-alquiler-villa-crespo-con-terraza-orden-publicado-descendente.html",
}

Estas son simplemente URLs con resultados de búsqueda de los sitios soportados por nuestro script. En este caso departamentos en Chacarita hasta 25.000 pesos y en Villa Crespo con terraza (sin límite de precio). Ambas en distintos portales de búsqueda. Es importante que los resultados esten ordenados por "más nuevos"

La función principal (main) define los pasos a seguir:


for url in urls: # 1
  res = requests.get(url) # 2
  ads = list(extract_ads(url, res.text)) # 3
  seen, unseen = split_seen_and_unseen(ads) # 4

  print("{} seen, {} unseen".format(len(seen), len(unseen)))

  for u in unseen: # 5
    notify(u)

  mark_as_seen(unseen) # 6
  1. Para cada una de nuestras URLs definidas previamente
  2. Ir a buscar los resultados
  3. Extraer los links de los ads
  4. Dividir links en vistos y no vistos
  5. Para los no vistos, notificar por mensaje
  6. Marcar los no vistos como vistos

De acá nos enfocamos en las partes más interesantes:

Extraer links de los ads

Cada página tiene su estructura propia, para eso definimos la dataclass Parser:

@dataclass
class Parser:
    website: str
    link_regex: str

    def extract_links(self, contents: str):
        soup = BeautifulSoup(contents, "lxml")
        ads = soup.select(self.link_regex)
        for ad in ads:
            href = ad["href"]
            _id = sha1(href.encode("utf-8")).hexdigest()
            yield {"id": _id, "url": "{}{}".format(self.website, href)}


parsers = [
    Parser(website="https://www.zonaprop.com.ar", link_regex="a.go-to-posting"),
    Parser(website="https://www.argenprop.com", link_regex="div.listing__items div.listing__item a"),
    Parser(website="https://inmuebles.mercadolibre.com.ar", link_regex="li.results-item .rowItem.item a"),
]

Como vemos, cada Parser actúa sobre un dominio en particular de los que soportamos, porque cada página tiene su estructura de HTML propia.

Con beautifulsoup podemos usar CSS queries para movernos por el markup y obtener los links de los avisos que nos interesan.

Una vez extraídos los devolvemos junto con un id que generamos hasheando la URL del aviso (esto será útil para marcar los ya vistos).

Recordar Links vistos

El bot no sería muy útil si reportara todo el tiempo lo mismo, deberíamos buscar una forma de "recordar" qué avisos ya envíamos.

Una forma sencilla es un archivo de texto seen.txt con los ids de los avisos reportados:

def split_seen_and_unseen(ads):
    history = get_history()
    seen = [a for a in ads if a["id"] in history]
    unseen = [a for a in ads if a["id"] not in history]
    return seen, unseen


def get_history():
    try:
        with open("seen.txt", "r") as f:
            return {l.rstrip() for l in f.readlines()}
    except:
        return set()

También agregamos una función que nos permita distinguir los que ya mandamos (seen) de los nuevos (unseen).

Notificaciones

def notify(ad):
    bot = "YOUR_BOT_ID"
    room = "YOUR_CHAT_ROOM_ID"
    url = "https://api.telegram.org/bot{}/sendMessage?chat_id={}&text={}".format(bot, room, ad["url"])
    r = requests.get(url)

Luego, usando un simple request de HTTP posteamos la url del aviso a un grupo de Telegram.

Gracias a la función de "preview" de Telegram, los ads se muestran con una imagen de la propiedad:

Alt Text

Por último sólo nos queda agregar los avisos notificados a seen.txt y listo.

Hosting

Ahora sólo necesitamos una máquina donde correrlo periódicamente. Por suerte AWS tiene un free tier que nos brinda una máquina básica por un mes, gratis.

Si bien la virtual no es muy poderosa, para este simple script basta y sobra. Yo lo tengo corriendo con un cronjob que lo ejecuta cada 5 minutos:

# m h  dom mon dow   command
*/5 * * * * /home/ubuntu/propfinder/search_props.sh

Conclusión

Con 100 líneas de scripting pudimos hacer un scrapper multi-sitio, que nos envía propiedades por chat como máximo a 5 minutos de ser publicadas.

Mucho más divertido (y menos frustrante) que meterse periódicamente a los distintos sitios a ver si hay alguna novedad.

Top comments (3)

Collapse
 
ignaciokairuz profile image
Ignacio Kairuz

Pregunta de inexperto : No vi que hayas usado alguna linea para indicarle a la website que supuestamente estás accediendo desde un browser común. Te bloquearon la IP o algo si es que lo dejaste corriendo desde tu propia compu? Mande un mail a argenprop y zona prop y me tiraron que supuestamente eso te hacen y además en los "términos y condiciones" de la website había algo como "no usar de la manera que no está intencionada para usarla" . Nada, si no hay problema a mi me da lo mismo xq cualquiera... Si estuviera usando lo recolectado para armar una website, digamos, "clon" lo entiendo, no da... pero para uso personal ....

Collapse
 
escribela profile image
Dante

Hola, espectacular, felicitaciones! una pregunta, sabes como haher para que funcione zonaprop? no me anda el parseo

Muchas gracias de antemano!

Collapse
 
gasticastro profile image
gasticastro

Para el objeto Parser de ZonaProp, usá este link_regex:

"h2.sc-i1odl-10 a.sc-i1odl-11.hDkIKM"

El que estaba anteriormente ("a.go-to-posting") por lo visto era de una versión anterior de la página.

Espero que te sirva, abrazo!