Scrappeando propiedades con Python
Tags: python, telegram, scrapping
Published: 2020-03-04 14:16:28

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](https://gist.github.com/fernandezpablo85/7ecea67eda79ae474895a07840fb56b6), 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:

```python

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:

```python

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_ ](https://docs.python.org/3/library/dataclasses.html) `Parser`:

```python
@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](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) 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 `id`s de los avisos reportados:


```python
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

```python
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](https://telegram.org/).

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ws7ak2tot4cppya86sh9.png)


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](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc) 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:

```bash
# 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.