Как программисты ищут квартиры

Как программисты ищут квартиры

444
ПОДЕЛИТЬСЯ

На самом деле все происходит не так…

Один мой знакомый попросил меня написать эту статью. А он уехал. Насовсем. Потому пишу статью с его слов я. В статье пойдет рассказ о его похождениях , которые могут (могли) быть некорректно восприняты администрацией определенных веб-ресурсов. В Гондурас. И те, в свою очередь, могут (могли) посетовать на моего знакомого куда следует.

Неувязка
Оставалось ее отыскать. Пару лет назад (в конце концов-то!) наступил в моей жизни момент, когда мне необходимо можно было приобрести квартиру. А конкретно — она обязана была быть НА крайнем этаже. Ну чтоб никто по мозгу не прогуливался. Ну и плевать на всех удобнее. Дело осложнялось тем, что были у меня свои взоры на то, какой обязана быть моя безупречная квартира.

Центровой местный веб-сайт по поиску недвижимости, как бы это помягче огласить, «сделан был незначительно неудобно». При этом он, поиск, когда я его просил выдать мне квартиры с раздельным санузлом, время от времени выдавал квартиры с совмещенным. А раз он (поиск) время от времени выдает квартиры несоответствующие моему запросу, то, может быть, он не указывает и надлежащие. А в мою подборку (квартира на крайнем этаже, раздельный узел, этажность > 5, неподалеку от метро, и бла бла бла) уже по определению не могло попадать много квартир… С балконом была схожая история. Поиск квартир на нем содержал обычные для схожих сервисов опции: год постройки, этажность, стоимость, не (!) крайний/1-ый этаж и т.д.

Ковальски, варианты!
Оставалось лишь одно — выгрузить все квартиры с веб-сайта к для себя локально: сохраняешь их в какую-нибудь базу, берешь в руки SQL (ну либо помоднее что-нить) «и погнал» (с).

Первой идеей было поглядеть на движок веб-сайта, поискать в нем какие-нибудь дыры, достучаться до сервера, где хранится вся информация о квартирах, ну и скопировать ее оттуда. Но это плохо, видимо, квалификации моей на тот момент не хватило. Просто огласить, но трудно сделать.

Но доступа к ПО у меня не было, а агентством становиться не хотелось. Здесь-то моей квалификации, думаю, хватило бы. Сотрудничество, все дела. Там, при условии, что ты — агентство, можно было получить (приобрести?) доступ к спец ПО, которое, судя по аннотации к нему и скриншотам, дозволяло в автоматизированном виде подавать объявления от лица агентства (от спамеры, да?). На теоретическом уровне, в этом ПО тоже можно было отыскать информацию о серверной стороне и вынуть оттуда информацию о квартирах. На мотивированном веб-сайте нашелся раздел для агентств по недвижимости.

Потому ничего не оставалось, не считая как написать его…

Парсер
Писать парсер решил на Python — относительно новейший язык для меня на тот момент он был, а поднять уровень в нем было полезно (потому и код соответственный). Программно заходим на веб-сайт, «ищем» все квартиры и парсим результаты, сохраняя их в локальную БД.

Для скачки страничек применялась обычная urllib:

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

Для парсинга HTML решил применять (опосля активного гугления) библиотеку lxml:

from lxml.html import parse
… flatsPageDocument = parse(flatsPageFilePath).getroot()
if flatsPageDocument is not None:
flatsTables = flatsPageDocument.xpath(‘//*[@id="list"]’)
А увлекательным было другое. Все это банально и неинтересно.

А далековато ли метро?
Будучи человеком безлошадным и перемещающимся строго на публичном транспорте, для меня было критично близость метро к моей будущей квартире. А позже и реализация: Потому появилась мысль определения наиблежайшей станции метро к квартире, и ее расстояния к ней. Так, метров не больше 2х тыщ.

Незначительно кода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)

Почему не кто-то один? Потому два движка употребляются сразу, чтоб можно было зрительно отсеять очевидно неправильные результаты. Потому результаты «пробивания» адресов бережно сохранялись в базу для использования при следующих пусках парсера. Просто для новейших улиц (да и для старенькых, либо неправильно введенных) кто-то из источников мог выдать некорректные либо усредненные данные (координаты центра городка, к примеру). Понятное дело, что и у Google’а и Yandex’а была квота на количество запросов в день с IP. Как видно из кода, в качестве источника данных геокодирования употребляются Google и Yandex.

С помочью гугло-карт была набита таблица с координатами станций метро, в том числе еще строящихся. А расстояние определялось просто с помощью аксиомы Пифагора:

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
А вот и наиблежайшая станция метро:

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)

Отслеживание цены квартиры
Так вот у меня на эту тему было свое мировоззрение. Все мы, наверняка, не раз читали статьи типа «Цены на квартиры в городке N стали понижаться на X% в месяц».

Заглядывая в старенькую базу данных и находя информацию о извлекаемой квартире там, можно было рассчитать дельту ее цены: Раз все извлеченные квартиры сохранялись локально в базу, то можно было выслеживать конфигурации цен на их.

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 isFlatInfoUpdated = 0
flatPriceDelta = 0
if len(oldFlatsDBFilePath):
oldFlatsDBCursor.execute(»’SELECT flatPriceInfo FROM Flats WHERE flatPageURL =? AND flatAddress =?
Потому, каждый раз читая статьи с анализом рынка недвижимости, я улыбался, зная, что «мои» квартиры совсем не растут в стоимости. Может, они никому не считая меня не были необходимы?

Для вас раздельный либо совмещенный?
А разве такое может быть в совмещенном санузле? Я программер, а программеры много задумываются.

Неувязка была в том, что веб-сайт поиска недвижимости скрывал эту информацию снутри странички описания квартиры и не демонстрировал в перечне результатов поиска. А уже из их извлекалась доборная информация по санузлу и иному. Потому был добавлен особый режим работы парсера, названный «flatsDeepParseMode». Как говориться, «We need to go deeper» (с). Он разрешал парсеру закачивать не лишь странички результатов поиска квартир, но и конкретно странички описания квартир.

Отказоустойчивость
Это в свою очередь время от времени приводило к задумчивости сервера, а, бывало, и к его отказам делать запросы. Потому скрипт опосля таковых случаев стал поддерживать механизмы «переспроса» страничек с равномерно увеличивающимся таймаутом меж попытками. В режиме глубочайшего парсинга скрипт мог сильно нагружать сервер, долбя его просьбами на отдачу тыщ страничек.

Маскировка
В один прекрасный момент скрипт закончил работать. Посыпались сообщения, что сервер что-то там не может ответить, таймауты и бла бла бла. И мой скрипт попал под раздачу. Оказалось, что обладатели веб-сайта недвижимости наняли группу специально обученных людей, чтоб те впилили фильтрацию пользователь-агентов, подключающихся к серверу клиентов (чего же это они вдруг?). А отважилось это просто — скрипт представился браузером:

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
Но в один прекрасный момент случилось ужасное…

You are banned!
Ведь даже браузеры не могли открыть веб-сайт недвижимости… Да, весь наш статический IP забанили на стороне сервера. Подмена пользователь-агента на очередной браузерный не посодействовала. Может быть, «какая-то вирусная программа слала много запросов на сервер» либо сходу несколько 10-ов служащих компании решили поискать для себя жилище. Придя днем на работу (необходимо же было как-то на квартиру зарабатывать), я увидел уже знакомое сообщение о ошибке, что сервер что-то там, таймауты и бла-бла-бла. И не лишь меня, как оказалось. Да, меня забанили. Но, как бы то ни было, нас забанили. Уж не знаю, почему это вышло.

Никого не ддосили. Мы ведь ничего такового не совершали. В общем, нас разбанили. А вот они-то не дали спуску администрации обнаглевшего ресурса. Правда-правда. Так уж случилось, но юристам нашей компании необходимо было что-то поискать на том веб-сайте (может быть, квартиру для наших заокеанских коллег). Под честное слово.

Фичи, фичи, фичи…
Парсер еще много чего же умеет: парсит квартиры до определенной цены, отмечает удаленные квартиры и вновь добавленные, подсчитывает количество фото квартиры и т.д.

Мы берем ее, заверните
Отыскал бы я ее без написания парсера? Свою безупречную квартиру я все-таки отыскал. Крайний этаж, рядом с метро, все дела. Не знаю, может быть. Но это было бы неспортивно, не по-программистски как-то…

P.S.
А означает — это работает. И да, я помню про старенькую статью на хабре, где таковой же извращенец энтузиаст, как я, парсил и анализировал квартиры на языке R. И он тоже купил свою правильную квартиру.

Кстати мой парсер может уже и не работать или работать неправильно (в связи с возможными переменами на веб-сайте) т.к. употреблялся достаточно издавна. И поосторожнее с ним, а то могут забанить (бывали случаи).

А компот код?!
По просьбе знакомого выкладываю исходники парсера на bitbucket.org. В репе можно отыскать и файлик с достаточно огромным, выстраданным SQL запросом, визуализирующим все извлеченные данные. Код, естественно, располагается лишь для вашего ознакомления.

habrahabr.ru Всем спасибо за внимание.