<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Посмотрев на свой предыдущий ноутбук, я ощутила острое желание все переделать и реструктурировать.
Прошлая версия по сути была больше изготовлением кирпичиков, из которых сейчас уже я соберу полноценный парсер. Как функцию, а не последовательность ячеек.
Все необходимое я перенесла сюда, также добавила что-то новое.
В результате я хочу получить функцию cianParser(), которая возвращает DataFrame.
Step1:  Тут я соберу все ссылки на квартиры по каждому округу, запишу их в словарь links.
Я хочу хранить ссылки в словаре. Ключами будут округи, значениями - все найденные ссылки для округа.
Step2:  Cian предлагает для просмотра не более 30 страниц выдачи для каждого запроса. Всего у нас получится не более 30 страниц х 25 квартир на странице х 9 округов = 6750 объектов. Вроде нормально. Но практика показала, что если выбросить дубликаты, то останется около 900 объектов. С чем это может быть связано? Я заметила, что некоторые объявления фигурируют больше, чем в одном округе (я даже видела дом по Бирюлевской улице, отнесенный не только к своему, но и к Тверскому району ЦАО), какие-то объявления появляются в топе выдачи не только первой страницы запроса. Но причины тут не так важны, их наверняка побольше. Важно другое - мне захотелось в итоге иметь побольше датасет.
Тогда я решила сделать свои запросы чуть более подробными и пройтись не по 9 округам, а по всем районам (в каждом округе их довольно много, порядка 15-20). Итого получилось прохождение по результатам 120 поисковых запросов.
Step3:  Здесь идет блок функций, наработанных и объясненных в прошлом ноутбуке. В этом я буду лишь указывать их предназначение.
Цена
По сравнению с предыдущим ноутбуком тут появилась обработка ситуации цены в долларах, которая мне встретилась только на этот раз
Step4:  Расстояние до центра города (вспомогательные
Step5:  Количество комнат
Тут я сменила значение mult на 6, так как решила все же впоследствии многокомнатным квартирам присваивать это число в это поле.
Step6:  Расстояние до метро
Step7:  До метро пешком/на машине
Step8:  Тип дома
Step9:  Этаж, этажность
Step10:  Общая площадь, жилая, площадь кухни (+ вспомогательный конвертор strToFloat() для чисел, для которых cian (в отличие от python) использует в качестве разделителя запятую, а не точку)
Немного подправила конвертор, который ошибался на данных, которых не бывает на Циане, но всё же
Step11:  Добавила обработку ситуации с прочерком
Step12:  Отсутствие балкона/их количество, наличие телефона
Step13:  Теперь новые функции
Эти выведены в отдельные исключительно для удобства при чтении бОльших, содержащих эти части кода функций
Step14:  getInfo() возвращает полную информацию по квартире, вызывая внутри себя вышеперечисленные функции для определения значений признаков
Step15:  Мне видится удобной схема из двух функций
Step16:  Схема готова. Теперь поработаем со ссылками
Выше была вырезана часть, где я сохраняла собранные ссылки по округам в соответствующие csv. Теперь я подгружу оттуда данные.
Step17:  Проделаю это для всех остальных округов, соединю все вертикально, тк дальше буду удалять дубликаты.
Step18:  Nan был нулевым элементом для списка ссылок каждого округа. Удалим их (первая размерность должна стать на 9 меньше)
Step19:  Теперь удалим дубликаты
Step20:  Вот данных и поубавилось. Но нам хватит.
Приведем в порядок ссылки
Step21:  Теперь можно запускать парсер
Step22:  Данные получены, сохраним их и приступим к очистке и визуализации.
<ASSISTANT_TASK:>
Python Code:
import requests
import re
from bs4 import BeautifulSoup
import pandas as pd
import time
import numpy as np
def html_stripper(text):
    return re.sub('<[^<]+?>', '', str(text))
Explanation: Посмотрев на свой предыдущий ноутбук, я ощутила острое желание все переделать и реструктурировать.
Прошлая версия по сути была больше изготовлением кирпичиков, из которых сейчас уже я соберу полноценный парсер. Как функцию, а не последовательность ячеек.
Все необходимое я перенесла сюда, также добавила что-то новое.
В результате я хочу получить функцию cianParser(), которая возвращает DataFrame.
End of explanation
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])])
Explanation: Тут я соберу все ссылки на квартиры по каждому округу, запишу их в словарь links.
Я хочу хранить ссылки в словаре. Ключами будут округи, значениями - все найденные ссылки для округа.
End of explanation
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)
Explanation: Cian предлагает для просмотра не более 30 страниц выдачи для каждого запроса. Всего у нас получится не более 30 страниц х 25 квартир на странице х 9 округов = 6750 объектов. Вроде нормально. Но практика показала, что если выбросить дубликаты, то останется около 900 объектов. С чем это может быть связано? Я заметила, что некоторые объявления фигурируют больше, чем в одном округе (я даже видела дом по Бирюлевской улице, отнесенный не только к своему, но и к Тверскому району ЦАО), какие-то объявления появляются в топе выдачи не только первой страницы запроса. Но причины тут не так важны, их наверняка побольше. Важно другое - мне захотелось в итоге иметь побольше датасет.
Тогда я решила сделать свои запросы чуть более подробными и пройтись не по 9 округам, а по всем районам (в каждом округе их довольно много, порядка 15-20). Итого получилось прохождение по результатам 120 поисковых запросов.
End of explanation
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)
Explanation: Здесь идет блок функций, наработанных и объясненных в прошлом ноутбуке. В этом я буду лишь указывать их предназначение.
Цена
По сравнению с предыдущим ноутбуком тут появилась обработка ситуации цены в долларах, которая мне встретилась только на этот раз
End of explanation
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 
def getCoords(flat_page):
    coords = flat_page.find('div', attrs={'class':'map_info_button_extend'}).contents[1]
    coords = re.split('&amp|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
def getDistance(coords):
    MSC_POINT_ZERO = (55.755831, 37.617673)
    return haversine(MSC_POINT_ZERO, coords)
Explanation: Расстояние до центра города (вспомогательные: гаверсинус - расстояние между двумя точками на сфере, получение координат)
End of explanation
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)
Explanation: Количество комнат
Тут я сменила значение mult на 6, так как решила все же впоследствии многокомнатным квартирам присваивать это число в это поле.
End of explanation
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
Explanation: Расстояние до метро
End of explanation
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
Explanation: До метро пешком/на машине
End of explanation
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
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
Explanation: Тип дома: материал, новостройка/вторичка
End of explanation
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
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
Explanation: Этаж, этажность
End of explanation
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
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
Explanation: Общая площадь, жилая, площадь кухни (+ вспомогательный конвертор strToFloat() для чисел, для которых cian (в отличие от python) использует в качестве разделителя запятую, а не точку)
Немного подправила конвертор, который ошибался на данных, которых не бывает на Циане, но всё же
End of explanation
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
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
Explanation: Добавила обработку ситуации с прочерком
End of explanation
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
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
Explanation: Отсутствие балкона/их количество, наличие телефона
End of explanation
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
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
Explanation: Теперь новые функции
Эти выведены в отдельные исключительно для удобства при чтении бОльших, содержащих эти части кода функций
End of explanation
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
Explanation: getInfo() возвращает полную информацию по квартире, вызывая внутри себя вышеперечисленные функции для определения значений признаков
End of explanation
def districtParser(links):
    apartments = []
    for link in links:
        apartment = getInfo(link)
        apartment.append(link)
        apartments.append(apartment)
        
    
    return apartments
districts
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
Explanation: Мне видится удобной схема из двух функций: парсер по округу и большой парсер. Большой парсер внутри себя вызывает парсер по округу для всех интересующих нас округов (их 9, я не рассматриваю Зеленоград, Новую Москву и т.д.)
End of explanation
full_links = pd.read_csv('/Users/tatanakuzenko/lbNW.csv')
full_links['Distr'] = 'NW'
full_links.head()
Explanation: Схема готова. Теперь поработаем со ссылками
Выше была вырезана часть, где я сохраняла собранные ссылки по округам в соответствующие csv. Теперь я подгружу оттуда данные.
End of explanation
districts_cut = {4: 'C', 5:'N', 6:'NE', 7:'E', 8:'SE', 9:'S', 10:'SW', 11:'W'}
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)
full_links.shape
Explanation: Проделаю это для всех остальных округов, соединю все вертикально, тк дальше буду удалять дубликаты.
End of explanation
full_links = full_links.dropna()
full_links.shape
Explanation: Nan был нулевым элементом для списка ссылок каждого округа. Удалим их (первая размерность должна стать на 9 меньше)
End of explanation
full_links = full_links.drop_duplicates()
full_links.shape
Explanation: Теперь удалим дубликаты
End of explanation
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)
full_links.head()
Explanation: Вот данных и поубавилось. Но нам хватит.
Приведем в порядок ссылки: установим верные индексы, переименуем колонку из 0 в link и переведем значения этой колонки в int.
End of explanation
data = cianParser(districts, full_links)
data.head()
data.shape
Explanation: Теперь можно запускать парсер
End of explanation
data.to_csv('cian_full_data.csv', index = False)
Explanation: Данные получены, сохраним их и приступим к очистке и визуализации.
End of explanation
<END_TASK>