Wie Programmierer nach Wohnungen suchen

    Bild

    Tatsächlich läuft alles schief ...

    Ein Freund von mir hat mich gebeten, diesen Artikel zu schreiben. Der Artikel wird über seine Abenteuer sprechen, die durch die Verwaltung bestimmter Internet-Ressourcen möglicherweise missverstanden wurden (könnten). Und diese wiederum können (könnten) sich über meinen Freund beschweren, wo sie sollten. Deshalb schreibe ich einen Artikel mit seinen Worten I. Und er ging. Nach Honduras. Für immer.

    Das Problem


    Vor ein paar Jahren (endlich!) Ich kam in meinem Leben , als ich brauchte war es möglich , eine Wohnung zu kaufen. Es blieb, sie zu finden. Die Sache wurde durch die Tatsache kompliziert, dass ich meine eigenen Ansichten darüber hatte, was meine ideale Wohnung sein sollte. Sie sollte nämlich im obersten Stock sein. Nun, damit niemand auf das Gehirn ging. Spucken überhaupt ist bequemer.

    Der zentrale Ort für die Suche nach Immobilien (die überwiegende Mehrheit der Agenturen und Eigentümer stellt ihre Wohnungen hier auf), sozusagen weicher: "Es wurde ein wenig unangenehm gemacht." Die Suche nach Wohnungen enthielt die Standardeinstellungen für solche Dienstleistungen: Baujahr, Anzahl der Stockwerke, Preis, nicht (!) Letzter / erster Stock usw. Und er, die Suche, als ich ihn bat, mir Wohnungen mit separatem Bad zu geben, gab er manchmal Wohnungen mit einer Kombination aus. Ähnlich war es mit dem Balkon. Und da er (Suche) manchmal Wohnungen ausgibt, die nicht meiner Bitte entsprechen, zeigt er vielleicht nicht die entsprechenden. Und in meiner Stichprobe (Wohnung im obersten Stockwerk, separate Einheit, Anzahl der Stockwerke> 5, nicht weit von der U-Bahn entfernt, und bla bla bla) konnten per Definition viele Wohnungen nicht in ...

    Kowalski, Optionen!


    Es blieb nur noch eines übrig - alle Wohnungen von der Site vor Ort für sich selbst zu entladen: Sie speichern sie in einer Datenbank, nehmen SQL in die Hand (oder, was bequemer ist, einen Thread) "und fuhren".

    Leicht zu sagen, aber schwer zu tun. Die erste Idee war, sich die Site-Engine anzusehen, nach Löchern zu suchen, den Server zu erreichen, auf dem alle Informationen zu den Apartments gespeichert sind, und sie von dort zu kopieren. Aber das ist schlecht , anscheinend waren meine damaligen Qualifikationen nicht genug.

    Auf dem Zielgelände gab es einen Bereich für Immobilienagenturen. Zusammenarbeit, alles. Dort könnten Sie, sofern Sie eine Agentur sind, Zugang zu spezialisierter Software erhalten (kaufen?), Die es nach den Anweisungen und Screenshots ermöglichte, automatisch Anzeigen im Namen der Agentur einzureichen (von Spammern, richtig?). Theoretisch war es in dieser Software auch möglich, Informationen über die Serverseite zu finden und von dort Informationen über Wohnungen abzurufen. Hier würde meine Qualifikation, denke ich, ausreichen. Aber ich hatte keinen Zugriff auf die Software und wollte keine Agentur werden.

    Daher blieb nichts anderes übrig, als es zu schreiben ...

    Parser


    Wir gehen programmgesteuert auf die Website, suchen nach allen Apartments, analysieren die Ergebnisse und speichern sie in einer lokalen Datenbank. Ich habe beschlossen, den Parser in Python zu schreiben - es war zu dieser Zeit eine relativ neue Sprache für mich, und es war nützlich, das Niveau darin zu erhöhen (daher ist der Code angemessen).

    Zum Herunterladen der Seiten wurde die Standard-Urllib verwendet:

    from urllib import FancyURLopener, quote_plus
    ...
    flatsPageContent = urlOpener.open(flatsPageURL).read()
    

    Für das Parsen von HTML wurde (nach aktivem Googeln) die lxml-Bibliothek verwendet:

    from lxml.html import parse
    ...
    flatsPageDocument = parse(flatsPageFilePath).getroot()
    if flatsPageDocument is not None:
    	flatsTables = flatsPageDocument.xpath('//*[@id="list"]')
    

    Das alles ist banal und uninteressant. Aber noch etwas war interessant.

    Ist die U-Bahn weit?


    Als pferdeloser Mann und ausschließlich mit öffentlichen Verkehrsmitteln unterwegs, war es für mich entscheidend, dass die U-Bahn in der Nähe meiner zukünftigen Wohnung lag. Solche Meter nicht mehr als zweitausend. Daher entstand die Idee, die nächstgelegene U-Bahnstation zur Wohnung und deren Entfernung zu dieser zu bestimmen. Und dann die Implementierung:

    Etwas Code
    def getFlatLocation(flatPageName, flatAddress, mode, geoDBCursor):
    	logging.info('Retrieving geo code info for flat \'%s\' (mode \'%s\')...' % (flatPageName, mode))
    	flatFullAddress = (flatBaseAddress + flatAddress).encode('utf8')
    	geoCodeResult = ''
    	isGeoCodeResultCached = 1
    	geoDBCursor.execute("SELECT geoCode FROM %s WHERE address = ?" % ("GeoG" if mode == 'G' else "GeoY"), (flatFullAddress,))
    	geoCodeResultRow = geoDBCursor.fetchone()
    	if geoCodeResultRow is not None:
    		geoCodeResult = geoCodeResultRow[0]
    	if geoCodeResult is None or len(geoCodeResult) == 0:
    		isGeoCodeResultCached = 0
    		geoCodeURL = ('http://maps.google.com/maps/api/geocode/json?sensor=false&address=' if mode == "G" else 'http://geocode-maps.yandex.ru/1.x/?format=json&geocode=') + quote_plus(flatFullAddress)
    		urlOpener = UrlOpener()
    		geoCodeResult = urlOpener.open(geoCodeURL).read()
    	if geoCodeResult is None:
    		geoCodeResult = ''
    	logging.info('Geo code result for flat \'%s\' was fetched (mode \'%s\', from cache - %d)' % (flatPageName, mode, isGeoCodeResultCached))
    	flatLocation = 0
    	geoCodeJson = json.loads(geoCodeResult)
    	if geoCodeJson is not None and (len(geoCodeJson['results']) if mode == 'G' else len(geoCodeJson['response'])):
    		if isGeoCodeResultCached == 0:
    			geoDBCursor.execute("INSERT INTO %s VALUES (?, ?)" % ("GeoG" if mode == 'G' else "GeoY"), (flatFullAddress, geoCodeResult))
    		if mode == "G":
    			geoCodeLocation = geoCodeJson['results'][0]['geometry']['location']
    			flatLocation = {'lat': float(geoCodeLocation['lat']), 'lng': float(geoCodeLocation['lng'])}
    		else:
    			geoCodeLocation = geoCodeJson['response']['GeoObjectCollection']['featureMember'][0]['GeoObject']['Point']['pos']
    			(flatLocationLng, flatLocationLat) = re.search('(.*) (.*)', geoCodeLocation).group(1, 2)
    			flatLocation = {'lat': float(flatLocationLat), 'lng': float(flatLocationLng)}
    		logging.info('Geo code info for flat \'%s\' was retrieved (mode \'%s\')' % (flatPageName, mode))
    	else:
    		logging.warning('Geo code info for flat \'%s\' was NOT retrieved (mode \'%s\')' % (flatPageName, mode))
    	return (flatLocation, isGeoCodeResultCached)
    


    Wie Sie dem Code entnehmen können, werden Google und Yandex als Quelle für Geokodierungsdaten verwendet. Warum nicht nur einer? Nur für neue Straßen (und für alte oder falsch eingegebene) könnte jemand aus den Quellen falsche oder gemittelte Daten angeben (z. B. Koordinaten des Stadtzentrums). Daher werden zwei Motoren gleichzeitig verwendet, so dass offensichtlich falsche Ergebnisse herausgesiebt werden können. Es ist klar, dass sowohl Google als auch Yandex ein Kontingent für die Anzahl der Anfragen pro Tag mit IP hatten. Daher wurden die Ergebnisse von "Punching" -Adressen sorgfältig in der Datenbank gespeichert, um sie bei nachfolgenden Parser-Starts zu verwenden.

    Mit Hilfe von Google Maps wurde eine Tabelle mit den Koordinaten der U-Bahn-Stationen gefüllt, einschließlich der noch im Bau befindlichen. Und die Entfernung wurde einfach mit dem Satz von Pythagoras bestimmt :

    def calculateDistance(location1, location2):
    	# haversine formula, see http://www.movable-type.co.uk/scripts/latlong.html for details
    	R = 6371 * 1000 # Radius of the Earth in m
    	dLat = (location2['lat'] - location1['lat']) * (math.pi / 180)
    	dLng = (location2['lng'] - location1['lng']) * (math.pi / 180) 
    	a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(location1['lat'] * (math.pi / 180)) * math.cos(location2['lat'] * (math.pi / 180)) * math.sin(dLng / 2) * math.sin(dLng / 2) 
    	c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    	d = R * c
    	return d
    

    Und hier ist die nächste U-Bahnstation:

    def getFlatDistanceInfo(flatLocation):
    	flatSubwayStationDistances = map(lambda subwayStationInfo: calculateDistance(flatLocation, subwayStationInfo['location']), subwayStationInfos)
    	flatNearestSubwayStationDistance = min(flatSubwayStationDistances)
    	flatNearestSubwayStationName = subwayStationInfos[flatSubwayStationDistances.index(flatNearestSubwayStationDistance)]['name']
    	flatTownCenterDistance = flatSubwayStationDistances[0]
    	return (flatNearestSubwayStationName, flatNearestSubwayStationDistance, flatTownCenterDistance)
    


    Den Preis einer Wohnung verfolgen


    Wahrscheinlich haben wir alle wiederholt Artikel wie "Die Preise für Wohnungen in der Stadt N sind um X% pro Monat gesunken" gelesen. Ich hatte also meine Meinung zu diesem Thema.

    Da alle extrahierten Wohnungen lokal in der Datenbank gespeichert waren, konnten Preisänderungen nachverfolgt werden. Durch einen Blick in die alte Datenbank und die Suche nach Informationen über die dort wiederherstellbare Wohnung konnte das Delta des Preises berechnet werden:

    isFlatInfoUpdated = 0
    flatPriceDelta = 0
    if len(oldFlatsDBFilePath):
    	oldFlatsDBCursor.execute('''SELECT flatPriceInfo FROM Flats WHERE flatPageURL = ? AND flatAddress = ? AND flatWholeSquare = ? AND flatLivingSquare  = ? AND flatKitchenSquare = ?''', (flatPageURL, flatAddress, flatWholeSquare, flatLivingSquare, flatKitchenSquare,))
    	oldFlatInfoRow = oldFlatsDBCursor.fetchone()
    	if oldFlatInfoRow is not None and oldFlatInfoRow[0] is not None:
    		isFlatInfoUpdated = 1
    		oldFlatPriceInfo = oldFlatInfoRow[0]
    		try:
    			flatPriceDelta = float(flatPriceInfo) - float(oldFlatPriceInfo)
    		except ValueError:
    			pass
    

    Daher lächelte ich jedes Mal, wenn ich Artikel mit einer Analyse des Immobilienmarktes las, in dem Wissen, dass „meine“ Wohnungen überhaupt nicht teurer wurden. Vielleicht wurden sie von niemandem außer mir gebraucht?

    Benötigen Sie getrennt oder kombiniert?


    Ich bin Programmierer und Programmierer denken viel nach. Ist das im kombinierten Badezimmer möglich?

    Das Problem war, dass die Immobiliensuchseite diese Informationen auf der Apartmentbeschreibungsseite versteckte und sie nicht in der Liste der Suchergebnisse anzeigte. Daher wurde eine spezielle Parser-Betriebsart namens "flatsDeepParseMode" hinzugefügt. Wie das Sprichwort sagt: "Wir müssen tiefer gehen" (c). Er erlaubte dem Parser, nicht nur die Seiten der Wohnungssuchergebnisse, sondern auch die Seiten der Wohnungsbeschreibung direkt herunterzuladen. Und schon wurden von ihnen zusätzliche Informationen über das Badezimmer und andere Dinge extrahiert.

    Fehlertoleranz


    Im Deep-Parsing-Modus könnte das Skript den Server stark belasten und ihn mit Anforderungen für die Rückgabe von Tausenden von Seiten überhäufen. Dies führte wiederum manchmal zur Nachdenklichkeit des Servers und manchmal zu seiner Weigerung, Anforderungen zu erfüllen. Daher begann das Skript nach solchen Fällen, die Mechanismen des "erneuten Fragens" von Seiten mit einem allmählich zunehmenden Zeitlimit zwischen den Versuchen zu unterstützen.

    Verkleidung


    Sobald das Skript nicht mehr funktioniert. Nachrichten duschten, dass der Server dort etwas nicht beantworten konnte, Timeouts und bla bla bla. Es stellte sich heraus, dass die Eigentümer der Immobilienseite eine Gruppe speziell geschulter Personen engagierten, um die Benutzeragenten zu filtern, die eine Verbindung zum Client-Server herstellen (warum sind sie plötzlich?). Und mein Skript kam unter die Distribution. Und es wurde einfach entschieden - das Skript gab vor, ein Browser zu sein:

    class UrlOpener(FancyURLopener, object):
        version = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11'
        pass
    

    Aber einmal passierte etwas Schreckliches ...

    Du bist verboten!


    Ja, ich wurde verboten. Und nicht nur ich, wie sich herausstellte. Als ich morgens bei der Arbeit ankam (ich musste irgendwie Geld verdienen), sah ich eine bekannte Fehlermeldung, dass der Server da war, Timeouts und bla bla bla. Das Ersetzen des Benutzeragenten durch einen anderen Browser hat nicht geholfen. Schließlich konnten selbst Browser keine Immobilienseite öffnen ... Ja, alle unsere statischen IP-Adressen wurden auf der Serverseite gesperrt. Ich weiß nicht, warum das passiert ist. Vielleicht haben „irgendeine Art von Virenprogramm viele Anfragen an den Server gesendet“ oder mehrere Dutzend Mitarbeiter des Unternehmens beschlossen, nach einer Unterkunft zu suchen. Aber wie auch immer, wir wurden verboten.

    Es ist einfach so passiert, aber die Anwälte unserer Firma mussten auf dieser Website nach etwas suchen (vielleicht nach einer Wohnung für unsere Kollegen in Übersee). Die Verwaltung der arroganten Ressource wurde jedoch nicht beeinträchtigt. So etwas haben wir nicht gemacht. Sie haben niemanden getötet. Richtig, richtig. Im Allgemeinen wurden wir verboten. Ehrlich gesagt.

    Merkmale, Merkmale, Merkmale ...


    Der Parser kann noch viele Dinge tun: Wohnungen zu einem bestimmten Preis analysieren, entfernte und neu hinzugefügte Wohnungen markieren, die Anzahl der Fotos der Wohnung zählen usw.

    ...

    Wir nehmen es, wickeln es ein


    Ich habe immer noch meine perfekte Wohnung gefunden. Dachgeschoss, neben der U-Bahn, alles. Würde ich es finden, ohne einen Parser zu schreiben? Ich weiß es vielleicht nicht. Aber das wäre unsportlich, irgendwie nicht programmatisch ...

    PS


    Und ja, ich erinnere mich an den alten Artikel über den Hub, in dem der gleiche perverse Enthusiast wie ich Wohnungen in R analysiert und analysiert hat. Und er hat auch seine richtige Wohnung gekauft. Und das heißt, es funktioniert.

    Übrigens funktioniert mein Parser möglicherweise nicht mehr oder nicht mehr richtig (aufgrund möglicher Änderungen auf der Website) für eine lange Zeit verwendet. Und seien Sie vorsichtig damit, sonst kann es verboten werden (es gab Fälle).

    Ein Kompottcode ?!


    Auf Wunsch eines Freundes poste ich den Quellcode des Parsers auf bitbucket.org . In Rüben finden Sie auch eine Datei mit einer ziemlich großen, leidenden SQL-Abfrage, die alle extrahierten Daten visualisiert. Der Code dient natürlich nur als Referenz.

    Vielen Dank für Ihre Aufmerksamkeit.

    Jetzt auch beliebt: