In [1]:
import requests
import re
from bs4 import BeautifulSoup
import pandas as pd
import time
import numpy as np
In [2]:
def html_stripper(text):
return re.sub('<[^<]+?>', '', str(text))
Я хочу хранить ссылки в словаре. Ключами будут округи, значениями - все найденные ссылки для округа.
In [6]:
links = dict([('NW' ,[np.nan]), ('C',[np.nan]), ('N',[np.nan]), ('NE',[np.nan]), ('E',[np.nan]), ('SE',[np.nan]), ('S',[np.nan]), ('SW',[np.nan]), ('W',[np.nan])])
Cian предлагает для просмотра не более 30 страниц выдачи для каждого запроса. Всего у нас получится не более 30 страниц х 25 квартир на странице х 9 округов = 6750 объектов. Вроде нормально. Но практика показала, что если выбросить дубликаты, то останется около 900 объектов. С чем это может быть связано? Я заметила, что некоторые объявления фигурируют больше, чем в одном округе (я даже видела дом по Бирюлевской улице, отнесенный не только к своему, но и к Тверскому району ЦАО), какие-то объявления появляются в топе выдачи не только первой страницы запроса. Но причины тут не так важны, их наверняка побольше. Важно другое - мне захотелось в итоге иметь побольше датасет.
Тогда я решила сделать свои запросы чуть более подробными и пройтись не по 9 округам, а по всем районам (в каждом округе их довольно много, порядка 15-20). Итого получилось прохождение по результатам 120 поисковых запросов.
In [9]:
districts = {1: 'NW', 4: 'C', 5:'N', 6:'NE', 7:'E', 8:'SE', 9:'S', 10:'SW', 11:'W'}
zone_bounds = {1: (125, 133), 4: (13, 23), 5: (23, 39), 6:(39, 56), 7:(56, 72), 8:(72, 84), 9:(84, 100), 10:(100, 112), 11:(112, 125)}
for i in districts.keys():
left = zone_bounds[i][0]
right = zone_bounds[i][1]
for j in range(left, right):
zone = 'http://www.cian.ru/cat.php?deal_type=sale&district%5B0%5D=' + str(j) + '&engine_version=2&offer_type=flat&room1=1&room2=1&room3=1&room4=1&room5=1&room6=1'
for page in range(1, 31):
page_url = zone.format(page)
search_page = requests.get(page_url)
search_page = search_page.content
search_page = BeautifulSoup(search_page, 'lxml')
flat_urls = search_page.findAll('div', attrs = {'ng-class':"{'serp-item_removed': offer.remove.state, 'serp-item_popup-opened': isPopupOpen}"})
flat_urls = re.split('http://www.cian.ru/sale/flat/|/" ng-class="', str(flat_urls))
for link in flat_urls:
if link.isdigit():
links[districts.get(i)].append(link)
По сравнению с предыдущим ноутбуком тут появилась обработка ситуации цены в долларах, которая мне встретилась только на этот раз
In [111]:
def getPrice(flat_page):
price = flat_page.find('div', attrs={'class':'object_descr_price'})
price = re.split('<div>|руб|\W', str(price))
price = "".join([i for i in price if i.isdigit()][-4:])
dollar = '808080'
if dollar in price:
price = price[6:]
return int(price)
In [30]:
from math import radians, cos, sin, asin, sqrt
AVG_EARTH_RADIUS = 6371
def haversine(point1, point2):
# извлекаем долготу и широту
lat1, lng1 = point1
lat2, lng2 = point2
# переводим все эти значения в радианы
lat1, lng1, lat2, lng2 = map(radians, (lat1, lng1, lat2, lng2))
# вычисляем расстояние по формуле
lat = lat2 - lat1
lng = lng2 - lng1
d = sin(lat * 0.5) ** 2 + cos(lat1) * cos(lat2) * sin(lng * 0.5) ** 2
h = 2 * AVG_EARTH_RADIUS * asin(sqrt(d))
return h
In [31]:
def getCoords(flat_page):
coords = flat_page.find('div', attrs={'class':'map_info_button_extend'}).contents[1]
coords = re.split('&|center=|%2C', str(coords))
coords_list = []
for item in coords:
if item[0].isdigit():
coords_list.append(item)
lat = float(coords_list[0])
lon = float(coords_list[1])
return lat, lon
In [32]:
def getDistance(coords):
MSC_POINT_ZERO = (55.755831, 37.617673)
return haversine(MSC_POINT_ZERO, coords)
Тут я сменила значение mult на 6, так как решила все же впоследствии многокомнатным квартирам присваивать это число в это поле.
In [33]:
def getRoom(flat_page):
rooms_n = flat_page.find('div', attrs={'class':'object_descr_title'})
rooms_n = html_stripper(rooms_n)
room_number = ''
flag = 0
for i in re.split('-|\n', rooms_n):
if 'много' in i:
flag = 1
break
elif 'комн' in i:
break
else:
room_number += i
if (flag):
room_number = '6'
room_number = "".join(room_number.split())
return int(room_number)
In [34]:
def getMetroDistance(flat_page):
metro = flat_page.find('div', attrs={'class':'object_descr_metro'})
metro = re.split('metro_name|мин', str(metro))
if (len(metro) > 2):
metro_dist = 0
power = 0
flag = 0
for i in range(0, len(metro[1])):
if metro[1][-i-1].isdigit():
flag = 1
metro_dist += int(metro[1][-i-1]) * 10 ** power
power += 1
elif (flag == 1):
break
else:
metro_dist = np.nan
return metro_dist
In [35]:
def getMetroWalking(flat_page):
metro = flat_page.find('div', attrs={'class':'object_descr_metro'})
metro = re.split('metro_name|мин', str(metro))
if (len(metro) > 2):
if 'пешк' in metro[2]:
walking = 1
elif 'машин' in metro[2]:
walking = 0
else:
walking = np.nan
else:
walking = np.nan
return walking
In [36]:
def getBrick(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
brick = np.nan
building_block = re.split('Этаж|Тип продажи', table)[1]
if 'Тип дом' in building_block:
if (('кирпич' in building_block) | ('монолит' in building_block)):
brick = 1
elif (('панельн' in building_block) | ('деревян' in building_block) | ('сталин' in building_block) |
('блочн' in building_block)):
brick = 0
return brick
In [37]:
def getNew(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
new = np.nan
building_block = re.split('Этаж|Тип продажи', table)[1]
if 'Тип дом' in building_block:
if 'новостр' in building_block:
new = 1
elif 'втор' in building_block:
new = 0
return new
In [38]:
def getFloor(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
floor_is = 0
building_block = re.split('Этаж|Тип продажи', table)[1]
floor_block = re.split('\xa0/\xa0|\n|\xa0', building_block)
for i in range(1, len(floor_block[2]) + 1):
if(floor_block[2][-i].isdigit()):
floor_is += int(floor_block[2][-i]) * 10**(i - 1)
return floor_is
In [39]:
def getNFloor(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
floors_count = np.nan
building_block = re.split('Этаж|Тип продажи', table)[1]
floor_block = re.split('\xa0/\xa0|\n|\xa0', building_block)
if floor_block[3].isdigit():
floors_count = int(floor_block[3])
return floors_count
Немного подправила конвертор, который ошибался на данных, которых не бывает на Циане, но всё же
In [40]:
def myStrToFloat(string):
delimiter = 0
value = 0
for i in range(0, len(string)):
if string[i] == ',':
delimiter = i
for i in range(0, delimiter):
value += int(string[delimiter - i - 1]) * 10 ** i
for i in range(1, len(string) - delimiter):
value += (int(string[delimiter + i]) * (10 ** (-i)))
return value
In [41]:
def getTotsp(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
space_block = re.split('Общая площадь', table)[1]
total = re.split('Площадь комнат', space_block)[0]
total_space = re.split('\n|\xa0', total)[2]
if total_space.isdigit():
total_space = int(total_space)
else:
total_space = myStrToFloat(total_space)
return total_space
Добавила обработку ситуации с прочерком
In [42]:
def getLivesp(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
space_block = re.split('Общая площадь', table)[1]
living = re.split('Жилая площадь', space_block)[1]
living_space = re.split('\n|\xa0', living)[2]
if living_space.isdigit():
living_space = int(living_space)
elif (living_space == '–'):
living_space = np.nan
else:
living_space = myStrToFloat(living_space)
return living_space
In [43]:
def getKitsp(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
space_block = re.split('Общая площадь', table)[1]
optional_block = re.split('Жилая площадь', space_block)[1]
kitchen_space = np.nan
if 'Площадь кухни' in optional_block:
kitchen_block = re.split('Площадь кухни', optional_block)[1]
if re.split('\n|\xa0', kitchen_block)[2] != '–':
if re.split('\n|\xa0', kitchen_block)[2].isdigit():
kitchen_space = int(re.split('\n|\xa0', kitchen_block)[2])
else:
kitchen_space = myStrToFloat(re.split('\n|\xa0', kitchen_block)[2])
return kitchen_space
In [44]:
def getBal(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
space_block = re.split('Общая площадь', table)[1]
optional_block = re.split('Жилая площадь', space_block)[1]
balcony = np.nan
if 'Балкон' in optional_block:
balcony_block = re.split('Балкон', optional_block)[1]
if re.split('\n', balcony_block)[1] != 'нет':
if re.split('\n', balcony_block)[1] != '–':
balcony = int(re.split('\n', balcony_block)[1][0])
else:
balcony = 0
return balcony
In [45]:
def getTel(flat_page):
table = flat_page.find('table', attrs = {'class':'object_descr_props'})
table = html_stripper(table)
space_block = re.split('Общая площадь', table)[1]
optional_block = re.split('Жилая площадь', space_block)[1]
telephone = np.nan
if 'Телефон' in optional_block:
telephone_block = re.split('Телефон', optional_block)[1]
if re.split('\n', telephone_block)[1] == 'да':
telephone = 1
elif re.split('\n', telephone_block)[1] == 'нет':
telephone = 0
return telephone
Эти выведены в отдельные исключительно для удобства при чтении бОльших, содержащих эти части кода функций
In [106]:
def getFlatPage(link):
flat_url = 'http://www.cian.ru/sale/flat/' + str(link) + '/'
flat_page = requests.get(flat_url)
flat_page = flat_page.content
flat_page = BeautifulSoup(flat_page, 'lxml')
return flat_page
In [47]:
def getFlatUrl(page):
page_url = district.format(page)
search_page = requests.get(page_url)
search_page = search_page.content
search_page = BeautifulSoup(search_page, 'lxml')
flat_url = search_page.findAll('div', attrs = {'ng-class':"{'serp-item_removed': offer.remove.state, 'serp-item_popup-opened': isPopupOpen}"})
flat_url = re.split('http://www.cian.ru/sale/flat/|/" ng-class="', str(flat_url))
return flat_url
getInfo() возвращает полную информацию по квартире, вызывая внутри себя вышеперечисленные функции для определения значений признаков
In [120]:
def getInfo(link):
flat_page = getFlatPage(link)
price = getPrice(flat_page)
coords = getCoords(flat_page)
distance = getDistance(coords)
rooms = getRoom(flat_page)
metrdist = getMetroDistance(flat_page)
metro_walking = getMetroWalking(flat_page)
brick = getBrick(flat_page)
new = getNew(flat_page)
floor = getFloor(flat_page)
nfloors = getNFloor(flat_page)
bal = getBal(flat_page)
kitsp = getKitsp(flat_page)
livesp = getLivesp(flat_page)
tel = getTel(flat_page)
totsp = getTotsp(flat_page)
walk = getMetroWalking(flat_page)
info = [bal, brick, distance, floor, kitsp, livesp, metrdist, new, nfloors, price, rooms, tel, totsp, walk]
return info
In [121]:
def districtParser(links):
apartments = []
for link in links:
apartment = getInfo(link)
apartment.append(link)
apartments.append(apartment)
return apartments
In [122]:
districts
Out[122]:
In [123]:
def cianParser(districts, links):
tmp = dict([(0 ,[np.nan]), (1,[np.nan]), (2,[np.nan]), (3,[np.nan]), (4,[np.nan]), (5,[np.nan]), (6,[np.nan]), (7,[np.nan]), (8,[np.nan]), (9,[np.nan]), (10,[np.nan]), (11,[np.nan]), ('Distr', [np.nan])])
data = pd.DataFrame(tmp)
for i in districts.keys():
district_name = districts.get(i)
tmp_links = links[links['Distr'] == district_name]
tmp_links = tmp_links['link']
data_tmp = pd.DataFrame(districtParser(tmp_links))
data_tmp['Distr'] = district_name
data = data.append(data_tmp)
print('district', districts.get(i), 'is done!')
return data
Выше была вырезана часть, где я сохраняла собранные ссылки по округам в соответствующие csv. Теперь я подгружу оттуда данные.
In [124]:
full_links = pd.read_csv('/Users/tatanakuzenko/lbNW.csv')
In [125]:
full_links['Distr'] = 'NW'
In [126]:
full_links.head()
Out[126]:
Проделаю это для всех остальных округов, соединю все вертикально, тк дальше буду удалять дубликаты.
In [127]:
districts_cut = {4: 'C', 5:'N', 6:'NE', 7:'E', 8:'SE', 9:'S', 10:'SW', 11:'W'}
In [128]:
for i in districts_cut.values():
links_append = pd.read_csv('/Users/tatanakuzenko/lb' + i + '.csv')
links_append['Distr'] = i
print(links_append.shape)
full_links = full_links.append(links_append)
In [129]:
full_links.shape
Out[129]:
Nan был нулевым элементом для списка ссылок каждого округа. Удалим их (первая размерность должна стать на 9 меньше)
In [130]:
full_links = full_links.dropna()
In [131]:
full_links.shape
Out[131]:
Теперь удалим дубликаты
In [132]:
full_links = full_links.drop_duplicates()
In [133]:
full_links.shape
Out[133]:
In [136]:
full_links.index = [x for x in range(len(full_links.index))]
full_links.rename(columns={'0' : 'link'}, inplace = True)
full_links['link'] = full_links['link'].astype(np.int32)
In [139]:
full_links.head()
Out[139]:
In [143]:
data = cianParser(districts, full_links)
In [144]:
data.head()
Out[144]:
In [145]:
data.shape
Out[145]:
In [146]:
data.to_csv('cian_full_data.csv', index = False)