Skip to content

Instantly share code, notes, and snippets.

@fernandezpablo85
Created March 4, 2020 02:12
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fernandezpablo85/7ecea67eda79ae474895a07840fb56b6 to your computer and use it in GitHub Desktop.
Save fernandezpablo85/7ecea67eda79ae474895a07840fb56b6 to your computer and use it in GitHub Desktop.
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
requests = "*"
beautifulsoup4 = "*"
lxml = "*"
[requires]
python_version = "3.7"
import requests
from bs4 import BeautifulSoup
from hashlib import sha1
from urllib.parse import urlparse
from dataclasses import dataclass
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",
}
@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"),
]
def _main():
for url in urls:
res = requests.get(url)
ads = list(extract_ads(url, res.text))
seen, unseen = split_seen_and_unseen(ads)
print("{} seen, {} unseen".format(len(seen), len(unseen)))
for u in unseen:
notify(u)
mark_as_seen(unseen)
def extract_ads(url, text):
uri = urlparse(url)
parser = next(p for p in parsers if uri.hostname in p.website)
return parser.extract_links(text)
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()
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)
def mark_as_seen(unseen):
with open("seen.txt", "a+") as f:
ids = ["{}\n".format(u["id"]) for u in unseen]
f.writelines(ids)
if __name__ == "__main__":
_main()
@emafriedrich
Copy link

genial Pablo. A ver si lo adapto para busquedas de cualquier cosa. For example ahora busco macbooks usadas.

@ftvalentini
Copy link

Llegué acá por tuiter y me pareció genial! Gracias.
Consulta: qué ventaja tiene pensar el Parser como una clase en lugar de armar todo el programa con funciones? No vengo de la programacion y en gral me resulta mas intuitivo pensar en funciones que en clases.

@fernandezpablo85
Copy link
Author

Llegué acá por tuiter y me pareció genial! Gracias.
Consulta: qué ventaja tiene pensar el Parser como una clase en lugar de armar todo el programa con funciones? No vengo de la programacion y en gral me resulta mas intuitivo pensar en funciones que en clases.

Buenas. Absolutamente nada, se puede hacer con una función (que recibiría el body + los 2 argumentos que pasas en el constructor). Lo tenía hecho así pero lo cambié para probar (y capaz ayudar a que gente conozca) el concepto de dataclass que me pareció piola

@lipusal
Copy link

lipusal commented Mar 23, 2020

Hola! Un amigo me compartió tu blogpost hace unas semanas, está genial la idea. Decidí reimplementarlo en Ruby (porque me es más familiar) y agregarle algunas cosas, como que guarde los IDs en Drive para que sea independiente de la máquina donde se corre. Implementé también un sistema de puntaje que anda bastante mal 😆. Lo comparto por si a alguien le interesa =) https://github.com/lipusal/apts

@mauroeparis
Copy link

Muchisimas gracias por esto! Para los proximos que se pasen, Zonaprops ya no te deja acceder al catálogo mediante el uso de requests.get() pero usando cloudscraper anda joya!

@rulosant
Copy link

CloudScraper me da este error:
cloudscraper.exceptions.CloudflareChallengeError: Detected a Cloudflare version 2 Captcha challenge, This feature is not available in the opensource (free) version.

¿Hay que cambiar de Scraper o se puede usar ingresando algun token gratuito?

@mauroeparis
Copy link

CloudScraper me da este error: cloudscraper.exceptions.CloudflareChallengeError: Detected a Cloudflare version 2 Captcha challenge, This feature is not available in the opensource (free) version.

¿Hay que cambiar de Scraper o se puede usar ingresando algun token gratuito?

En su momento busqué algo gratuito y no encontré nada pero si resolves el captcha por tu cuenta (podés hacer que te aparezca ingresando usando Tor posiblemente), te agrega una cookie que indica que sos humano. Podés agregar esa cookie a la request y debería funcionar. Esto lo hice hace mucho tiempo con otra pág y funcionó, espero que tengas la misma suerte!

De paso te dejo este repo mío que tiene otros scrapers aparte del de Zonaprop y otras cosas que pueden servirte.
https://github.com/mauroeparis/scrappdept

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment