[GitHub, Python, Алгоритмы, Веб-аналитика] Как проанализировать рынок фотостудий с помощью Python (1/3). Парсинг данных
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В интернете огромное количество открытых данных. При правильном сборе и анализе информации можно решить важные бизнес-задачи. Например, стоит ли открыть свой бизнес?
С таким вопросом ко мне обратились клиенты, желающие получить аналитику рынка услуг фотостудий. Для них было важно понять: стоит ли открывать фотостудию, где отрыть, какая площадь помещения, сколько залов открыть вначале, в какой месяц лучше стартовать и многие другие вопросы.
По итогу выполнения проекта написал серию статей с подробным поэтапным описанием выполняемых задач, используемых инструментов и полученных результатов.
В данной статье, первой из трех, опишу планирование и написание парсинга на Python.
Во второй статье опишу алгоритм взаимодействия парсинга с базой данных и обновления данных.
В третьей статье рассмотрю процесс анализа собранных данных и ответы на вопросы клиента, желающего открыть фотостудию.
В ходе изучения сайтов фотостудий сформулировал общую схему их работы:
- описать свои услуги на сайте;
- предоставить сервис бронирования залов через сайт или по телефону;
- сообщить контакты;
- принять посетителей в забронированное время.
Постановка задачи
Основная задача данного проекта: проанализировать рынок услуг фотостудий Москвы.
Для начала нужно понять какие компании есть на рынке и как они работают.
Самый распространенный сайт-агрегатор фотостудий: Studiorent. На нем видим примерное количество компаний, их сайты, тематики, цены и прочую общую информацию.
На сайте studiorent мы увидели важные данные для анализа рыночной ситуации: календарь бронирования. Собрав и проанализировав эти данные у разных фотостудий сможем ответить на огромное количество важных вопросов:
- какова сезонность бизнеса;
- какова загруженность имеющихся фотостудий;
- в какие дни недели бронируют чаще и на сколько;
- какой доход у фотостудий;
- какова часовая ставка аренды фотостудии;
- сколько залов у фотостудий и сколько их было при открытии;
- как влияет количество залов на величину дохода с одного зала;
- какова площадь залов
- и многие другие.
Первая задача обозначена: необходимо создать парсинг, собирающий данные календарей бронирования разных фотостудий.
Что будем парсить?
При внимательном рассмотрении сайтов разных фотостудий, видим следующие основные сервисы бронирования:
- Google-Календарь;
- приложение AppEvent;
- приложение Ugoloc;
- самописные календари бронирования (например);
- запись по телефону с отсутствием информации о бронировании на сайте.
Для парсинга самописных календарей необходим отдельный код для каждого из них. Поэтому не будем тратить на это время.
AppEvent неудобен для парсинга. В нем отсутствует информация о бронировании в прошлом. Поэтому AppEvent необходимо парсить ежедневно, что создает определенные неудобства, такие как: необходим длительный сбор данных (более года) для получения примерной картины рынка, необходим отдельный сервер (например, облачный) с ежедневным запуском парсера.
С Google-Календарем изначально было опасение блокировки работы парсера самим Google'ом. Есть вероятность, что придется использовать специальные платные proxy-сервисы (например), а это усложняет работу парсера и делает ее дороже.
Сервис Ugoloc показался идеальным для парсинга, благодаря небольшому сроку жизни проекта (с 2015 года) и значительному числу зарегистрированных в нем фотостудий (84 на момент написания статьи).
Легче всего будет сделать парсинг сервиса Ugoloc, т.к.:
- сможем получить доступ ко всей истории бронирования. Следовательно, можно будет парсить данные по мере необходимости;
- не потребуется использовать proxy. Значит, сможем использовать простые библиотеки (urllib);
- на сервисе зарегистрировано около трети всего количества фотостудий Москвы. Следовательно, получим достоверные данные о состоянии рынка.
Структура сайта
Полный список фотостудий ссылками на их страницы представлен на странице.
На странице каждой фотостудии можем найти данные по метражу студии, высоте потолков, количеству залов, специальному оборудованию и ссылки на каждый зал.
На странице зала есть данные по метражу зала, высоте потолков, минимальному срок брони и ссылку на календарь бронирования.
Ссылка на страницу студии строится по форме: ugoloc.ru/studios/show/id_студии
Ссылка на зал: ugoloc.ru/studios/hall/id_зала
Ссылка на календарь бронирования: ugoloc.ru/studios/booking/id_зала
Этапы парсинга:
- выгрузка списка фотостудий;
- выгрузка списка залов;
- выгрузка данных бронирования за выбранную неделю;
- выгрузка исторических данных бронирования;
- выгрузка будущих данных
- расшифровка выгруженных json-данных
1. Выгрузка списка фотостудий
Как выгрузить список фотостудий?
Полгода назад список фотостудий на странице выгружался как json-файл. Если вы первый раз сталкиваетесь с анализом работы сайта, содержание json файлов можно увидеть следующем образом: F12 -> «Network» -> «JS» -> выбор нужного файла -> «Response».
Таким образом обнаружил ссылку на список фотостудий: https://ugoloc.ru/studios/list.json
Для запроса используем библиотеку urllib.
Запрашиваем полные данные по списку фотостудий
SPL
url = 'https://ugoloc.ru/studios/list.json'
json_data = urllib.request.urlopen(url).read().decode()
Получили строковый формат json-данных. Для его расшифровки применим библиотеку json.
Список фотостудий находятся по ключу 'features'
SPL
json.loads(json_data)['features']
Перебирая список фотостудий и сохраняя необходимые данные, получаем следующий код процедуры
SPL
def studio_list():
url = 'https://ugoloc.ru/studios/list.json'
json_data = urllib.request.urlopen(url).read().decode()
id = list()
name = list()
metro = list()
address = list()
phone = list()
email = list()
for i in range(len(json.loads(json_data)['features'])):
id.append(json.loads(json_data)['features'][i]['studio']['id'])
name.append(json.loads(json_data)['features'][i]['studio']['name'])
metro.append(json.loads(json_data)['features'][i]['studio']['metro'])
address.append(json.loads(json_data)['features'][i]['studio']['address'])
phone.append(json.loads(json_data)['features'][i]['studio']['phone'])
email.append(json.loads(json_data)['features'][i]['studio']['email'])
return pd.DataFrame.from_dict({'studio_id': id,
'name': name,
'metro': metro,
'address': address,
'phone': phone,
'email': email}).set_index('studio_id')
На выходе процедуры получили таблицу с id студии, названием, ближайшим метро, адресом, телефоном и e-mail'ом.
При необходимости можно вытянуть сайт, данные геолокации, текстовое описание студии и прочие данные.
2. Выгрузка списка залов
Подробное описание фотостудии с указанием залов находится в папке «ugoloc.ru/studios/show» + id фотостудии.
На странице фотостудии находим список ссылок на залы. Список ссылок получаем с помощью библиотек BeautifulSoup и регулярных выражений re:
- вначале делаем get-запрос (urllib.request.urlopen) страницы студии;
- потом переводим полученные строковые данные в объект BeautifulSoup с разбором как html-код («html.parser');
- затем находим все ссылки, в которых есть указание папки „studios/hall/“.
Код запроса:
SPL
url_studio = 'https://ugoloc.ru/studios/show/' + str(584)
html = urllib.request.urlopen(url_studio).read()
soup = BeautifulSoup(html, "html.parser")
halls_html = soup.find_all('a', href=re.compile('studios/hall/'))
Получили список BeautifulSoup-объектов, содержащих ссылки на страницы залов. Дальнейшие действия:
- извлекаем ссылки на зал методом .get('href') или указанием индекса ['href'];
- проверяем, проходили ли по этой ссылке ранее (необходимо при работе цикла);
- выгружаем данные по названию зала, ссылки, площади, высоте потолков;
- проверяем, не является ли зал гримерным местом (в названии есть слог „грим“).
Код процедуры запроса списка залов
SPL
def hall_list(studio_id):
st_id = list()
hall_id = list()
name = list()
is_hall = list()
square = list()
ceiling = list()
for id in studio_id:
url_studio = 'https://ugoloc.ru/studios/show/' + str(id)
html = urllib.request.urlopen(url_studio).read()
soup = BeautifulSoup(html, "html.parser")
halls_html = soup.find_all('a', href=re.compile('studios/hall/'))
halls = dict()
for hall in halls_html:
if int(hall.get('href').replace('/studios/hall/','')) not in hall_id:
st_id.append(id)
name.append(hall['title'])
hall_id.append(int(hall.get('href').replace('/studios/hall/','')))
if 'грим' in str.lower(hall['title']):
is_hall.append(0)
else:
is_hall.append(1)
url_hall = 'https://ugoloc.ru/studios/hall/' + str(hall.get('href').replace('/studios/hall/',''))
html_hall = urllib.request.urlopen(url_hall).read()
soup_hall = BeautifulSoup(html_hall, "html.parser")
try:
square.append(int(soup_hall.find_all('div', class_='param-value')[0].contents[0]))
except:
square.append(np.nan)
try:
ceiling.append(float(soup_hall.find_all('div', class_='param-value')[1].contents[0]))
except:
ceiling.append(np.nan)
return pd.DataFrame.from_dict({'studio_id': st_id,
'hall_id': hall_id,
'name': name,
'is_hall': is_hall,
'square': square,
'ceiling': ceiling
}).set_index('hall_id')
3. Выгрузка данных бронирования за выбранную неделю
Для выгрузки данных по бронированию за неделю необходимо найти ссылку на выгрузку json-данных. В данном случае найти ее можно в коде страницы поиском по слову „json“. Первое совпадение (27 строка) содержит переменную:
var ajax_url = '/studios/calendar/975.json?week='
Проверим данную ссылку: https://ugoloc.ru/studios/calendar/975.json?week=
Работает! Видим данные по часам бронирования, по дням, по стоимости.
Значение параметра week по умолчанию равен 0. Для просмотра предыдущих недель берем отрицательные значения: -1 (предыдущая неделя), -2 (2 недели назад) и т.д., — для просмотра следующих, соответственно, положительные значения.
Выгружаем json-данные по бронированию
SPL
url_booking = 'https://ugoloc.ru/studios/calendar/' + str(id) + '.json?week=' + str(week)
json_booking = json.loads(urllib.request.urlopen(url_booking).read().decode())
Данные по датам можем увидеть по индексу 'days',
по рабочим часам — индекс 'hours',
по ценам — индекс 'prices',
по минимальном сроке бронирования — индекс 'min_hours',
по бронированию — индекс 'bookings'.
Собираем данные по бронированию за выбранную неделю
SPL
def get_week_booking(id, week=0):
url_booking = 'https://ugoloc.ru/studios/calendar/' + str(id) + '.json?week=' + str(week)
json_booking = json.loads(urllib.request.urlopen(url_booking).read().decode())
booking = {
'hall_id': id,
'week': week,
'monday_date': json_booking['days']['1']['date'],
'days': json_booking['days'],
'hours': json_booking['hours'],
'bookings': json_booking['bookings'],
'prices': json_booking['prices'],
'min_hours': json_booking['min_hours'],
'is_opened': 1 if np.sum([len(json_booking['bookings'][str(x)]) for x in range(1, 8)]) > 0 else 0
}
time.sleep(.1)
return booking
Отдельной строкой проверяем доступно ли хотя бы одно бронирование за неделю. Если доступно, то считаем студию открытой.
4. Выгрузка исторических данных бронирования
Для загрузки исторических данных загружаем данные бронирования за прошлую неделю, позапрошлую, 3 недели назад и т.д.
Основная проблема состоит в том, чтобы определить момент открытия зала. Если запросить данные 1000 недель назад, то API выгрузит нам корректные данные.
Формулируем критерий, подтверждающий, что зал закрыт: если зал ни разу не бронировался 2 месяца подряд (9 недель), то он не работал.
Используя критерий, напишем процедуру
SPL
def get_past_booking(id, weeks_ago = 500):
week = -1
null_period = 9
flag = 0
d = dict()
while flag != 1:
d[week] = get_week_booking(id, week)
if (len(d) > null_period
and 1 not in [d[-1 * x]['is_opened'] for x in range(len(d) - 9, len(d))]
):
flag = 1
for x in range(0, null_period + 1):
del d[week + x]
if week < weeks_ago * -1:
return d
week += -1
time.sleep(1)
return d
Отдельными параметрами обозначил срок в просмотре данных 10 лет (500 недель), означающий что исторические данные более 10 лет нам не интересны (притом, что агрегатор открыт с 2015 года).
Кроме того, устанавливаем срок ожидания между запросом данных в 0,1 секунду.
5. Выгрузка будущих данных
В выгрузке данных бронирования будущих периодов важно найти неделю, когда заканчивается основная часть бронирований.
Устанавливаем аналогичный с предыдущим пунктом критерий: если в течение 2 месяцев подряд (9 недель) не обнаружено ни одной брони, то дальнейшие периоды можем не просмартивать.
Получаем код процедуры
SPL
def get_future_booking(id):
week = 0
null_period = 9
flag = 0
d = dict()
while (flag != 1 and week <= 30):
d[week] = get_week_booking(id, week)
if (len(d) > null_period and 1 not in [d[x]['is_opened'] for x in range(len(d) - 9, len(d))]):
flag = 1
for x in range(0, null_period):
del d[week - x]
week += 1
time.sleep(1)
return d
6. Расшифровка выгруженных json-данных
Для расшифровки json-данных в своем часто использовал методы try, except: если не расшифровывается как привычный тип данных, например dictionary (try), то расшифровываем как другой ожидаемый тип, например list (except). Сейчас понимаю, лучше строить вычисления на проверке типа данных напрямую (функция type()) и дальнейшей их обработкой.
Мы написали процедуры по выгрузке данных по бронированию для выбранного зала. Следующей задачей необходимо перевести json-данные в табличный вид DataFrame для удобства дальнейшей обработки или записи в базу данных.
Для расшифровки перебираем каждый день недели.
Дату переводим из текстового формата в формат даты
SPL
cur_date = pd.Timestamp(datetime.datetime.strptime(d[week]['days'][str(weeks_day)]['date'], '%d.%m.%Y').isoformat())
Часы бронирования могут быть представлены в виде текста (»12:00"), числа (12), а при круглосуточной работе могут быть указаны конечным числом бронирования (24).
Для расшифровки часов работы использовал методы try, except
SPL
try:
try:
working_hour = list([int(x) for x in d[week]['prices'][str(weeks_day)].keys()])
working_hour_is_text = 1
except:
working_hour = list(d[week]['prices'][str(weeks_day)].keys())
except:
working_hour = list(range(len(d[week]['prices'][str(weeks_day)])))
Если цена бронирования установлена на одном уровне вне зависимости от дня недели и времени, то цена может храниться числом или строкой. Кроме того, цена может храниться в виде списка или словаря.
Для расшифровки цены бронирования использовал методы try, except
SPL
try:
price = list(
d[week]['prices'][str(weeks_day)].values()
if type(d[week]['prices'][str(weeks_day)].values()) != type(dict())
else d[week]['prices'][str(weeks_day)]
)
except:
price = list(
d[week]['prices'][str(weeks_day)]
if type(d[week]['prices'][str(weeks_day)]) != type(dict())
else d[week]['prices'][str(weeks_day)]
)
Период бронирования может быть указан как списком доступных для брони часов, так и единственным числом, указывающим на круглосуточный характер брони.
Расшифровка доступного для бронирования времени:
SPL
try:
booking_hours = sorted([int(x) for x in d[week]['bookings'][str(weeks_day)]])
duration = [d[week]['bookings'][str(weeks_day)][str(h)]['duration'] for h in booking_hours]
except:
booking_hours = 0
duration = 24
Забронированными являются рабочие часы, недоступные к бронированию. То есть, если зал работает с 10:00 до 22:00 и доступно к бронированию время с 10:00 до 18:00, то время с 18:00 до 22:00 считаем забронированными. Эту логику применяем для вычисление забронированного времени.
Общая процедура расшифровки json-данных:
SPL
def hall_booking(d):
hour = list(range(24))
df = pd.DataFrame(columns=['hour', 'date', 'is_working_hour', 'price', 'duration', 'week', 'min_hours'])
for week in d.keys():
for weeks_day in range(1, 8):
working_hour_is_text = 0
cur_date = pd.Timestamp(datetime.datetime.strptime(d[week]['days'][str(weeks_day)]['date'], '%d.%m.%Y').isoformat())
try:
try:
working_hour = list([int(x) for x in d[week]['prices'][str(weeks_day)].keys()])
working_hour_is_text = 1
except:
working_hour = list(d[week]['prices'][str(weeks_day)].keys())
except:
working_hour = list(range(len(d[week]['prices'][str(weeks_day)])))
try:
price = list(
d[week]['prices'][str(weeks_day)].values()
if type(d[week]['prices'][str(weeks_day)].values()) != type(dict())
else d[week]['prices'][str(weeks_day)]
)
except:
price = list(
d[week]['prices'][str(weeks_day)]
if type(d[week]['prices'][str(weeks_day)]) != type(dict())
else d[week]['prices'][str(weeks_day)]
)
try:
booking_hours = sorted([int(x) for x in d[week]['bookings'][str(weeks_day)]])
duration = [d[week]['bookings'][str(weeks_day)][str(h)]['duration'] for h in booking_hours]
except:
booking_hours = 0
duration = 24
min_hours = d[week]['min_hours']
df_temp = pd.DataFrame(hour, columns = ['hour'])
df_temp['date'] = cur_date
df_temp['is_working_hour'] = [1 if y else 0 for y in [x in working_hour for x in hour]]
df_temp['price'] = 0
if len(working_hour) == 24:
df_temp['price'] = price
else:
df_temp.loc[working_hour, 'price'] = price
df_temp['duration'] = 0
if duration != 24 and working_hour_is_text == 0:
df_temp.loc[[x in booking_hours for x in df_temp['hour']], 'duration'] = duration
elif duration != 24 and working_hour_is_text != 0:
df_temp.loc[[x in booking_hours for x in df_temp['hour']], 'duration'] = duration
else:
df_temp.loc[0, 'duration'] = 24
df_temp['week'] = week
df_temp['min_hours'] = min_hours
df = pd.concat([df, df_temp])
df = df.sort_values(by=['week', 'date', 'hour'])
df.index = list(range(len(df)))
df['is_booked'] = 0
for i in df.index:
if df.loc[i, 'duration'] != 0:
if i + df.loc[i, 'duration'] < df.index[-1]:
df.loc[i:(i + int(df.loc[i, 'duration'])) - 1, 'is_booked'] = 1
else:
df.loc[i:, 'is_booked'] = 1
df['hall_id'] = d[np.min(list(d.keys()))]['hall_id']
return df
Итог
Мы рассмотрели работу парсера, собирающего данные по бронированию залов московских фотостудий с сайта ugoloc.ru. В результате выгрузили список фотостудий, список залов, список забронированных часов и перевели в формат DataFrame. С полученными уже можно работать, но парсинг занимает продолжительное время и выгруженные данные необходимо где-то хранить.
Поэтому в следующей статье я опишу как сохранять полученную информацию в простую базу данных и выгружать их при необходимости.
Готовый проект вы можете найти на моей странице в github.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Профессиональная литература] Книга «Программируем с PyTorch: Создание приложений глубокого обучения»
- [C++, D, Алгоритмы, Высокая производительность] Триумфальное возвращение Ломуто (перевод)
- [Python, ООП] Динамическое определение класса в Python
- [Алгоритмы, Искусственный интеллект, Машинное обучение, Учебный процесс в IT] Positive-Unlabeled learning and where to find it
- [Python, Алгоритмы, Математика, Обработка изображений] Декодируем JPEG-изображение с помощью Python (перевод)
- [Python, Программирование] Создание нейросети по распознаванию лиц на фотографиях из Вконтакте
- [Python, Программирование, Промышленное программирование] Устройство CPython. Доклад Яндекса
- [Аналитика мобильных приложений, Дизайн, Дизайн мобильных приложений, Управление продуктом] Нужны ли дизайнеру метрики?
- [Python, Искусственный интеллект, Машинное обучение] Учим ИИ распределять пироги по магазинам с помощью обучения с подкреплением
- Обновление Python 3.8.5 с устранением уязвимостей
Теги для поиска: #_github, #_python, #_algoritmy (Алгоритмы), #_vebanalitika (Веб-аналитика), #_python, #_fotostudija (фотостудия), #_analiz_rynka (анализ рынка), #_analitika (аналитика), #_parsing_sajta (парсинг сайта), #_json, #_github, #_python, #_algoritmy (
Алгоритмы
), #_vebanalitika (
Веб-аналитика
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:56
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В интернете огромное количество открытых данных. При правильном сборе и анализе информации можно решить важные бизнес-задачи. Например, стоит ли открыть свой бизнес? С таким вопросом ко мне обратились клиенты, желающие получить аналитику рынка услуг фотостудий. Для них было важно понять: стоит ли открывать фотостудию, где отрыть, какая площадь помещения, сколько залов открыть вначале, в какой месяц лучше стартовать и многие другие вопросы. По итогу выполнения проекта написал серию статей с подробным поэтапным описанием выполняемых задач, используемых инструментов и полученных результатов. В данной статье, первой из трех, опишу планирование и написание парсинга на Python. Во второй статье опишу алгоритм взаимодействия парсинга с базой данных и обновления данных. В третьей статье рассмотрю процесс анализа собранных данных и ответы на вопросы клиента, желающего открыть фотостудию. В ходе изучения сайтов фотостудий сформулировал общую схему их работы:
Постановка задачи Основная задача данного проекта: проанализировать рынок услуг фотостудий Москвы. Для начала нужно понять какие компании есть на рынке и как они работают. Самый распространенный сайт-агрегатор фотостудий: Studiorent. На нем видим примерное количество компаний, их сайты, тематики, цены и прочую общую информацию. На сайте studiorent мы увидели важные данные для анализа рыночной ситуации: календарь бронирования. Собрав и проанализировав эти данные у разных фотостудий сможем ответить на огромное количество важных вопросов:
Первая задача обозначена: необходимо создать парсинг, собирающий данные календарей бронирования разных фотостудий. Что будем парсить? При внимательном рассмотрении сайтов разных фотостудий, видим следующие основные сервисы бронирования:
Для парсинга самописных календарей необходим отдельный код для каждого из них. Поэтому не будем тратить на это время. AppEvent неудобен для парсинга. В нем отсутствует информация о бронировании в прошлом. Поэтому AppEvent необходимо парсить ежедневно, что создает определенные неудобства, такие как: необходим длительный сбор данных (более года) для получения примерной картины рынка, необходим отдельный сервер (например, облачный) с ежедневным запуском парсера. С Google-Календарем изначально было опасение блокировки работы парсера самим Google'ом. Есть вероятность, что придется использовать специальные платные proxy-сервисы (например), а это усложняет работу парсера и делает ее дороже. Сервис Ugoloc показался идеальным для парсинга, благодаря небольшому сроку жизни проекта (с 2015 года) и значительному числу зарегистрированных в нем фотостудий (84 на момент написания статьи). Легче всего будет сделать парсинг сервиса Ugoloc, т.к.:
Структура сайта Полный список фотостудий ссылками на их страницы представлен на странице. На странице каждой фотостудии можем найти данные по метражу студии, высоте потолков, количеству залов, специальному оборудованию и ссылки на каждый зал. На странице зала есть данные по метражу зала, высоте потолков, минимальному срок брони и ссылку на календарь бронирования. Ссылка на страницу студии строится по форме: ugoloc.ru/studios/show/id_студии Ссылка на зал: ugoloc.ru/studios/hall/id_зала Ссылка на календарь бронирования: ugoloc.ru/studios/booking/id_зала Этапы парсинга:
1. Выгрузка списка фотостудий Как выгрузить список фотостудий? Полгода назад список фотостудий на странице выгружался как json-файл. Если вы первый раз сталкиваетесь с анализом работы сайта, содержание json файлов можно увидеть следующем образом: F12 -> «Network» -> «JS» -> выбор нужного файла -> «Response». Таким образом обнаружил ссылку на список фотостудий: https://ugoloc.ru/studios/list.json Для запроса используем библиотеку urllib. Запрашиваем полные данные по списку фотостудийSPLurl = 'https://ugoloc.ru/studios/list.json'
json_data = urllib.request.urlopen(url).read().decode() Получили строковый формат json-данных. Для его расшифровки применим библиотеку json. Список фотостудий находятся по ключу 'features'SPLjson.loads(json_data)['features']
Перебирая список фотостудий и сохраняя необходимые данные, получаем следующий код процедурыSPLdef studio_list():
url = 'https://ugoloc.ru/studios/list.json' json_data = urllib.request.urlopen(url).read().decode() id = list() name = list() metro = list() address = list() phone = list() email = list() for i in range(len(json.loads(json_data)['features'])): id.append(json.loads(json_data)['features'][i]['studio']['id']) name.append(json.loads(json_data)['features'][i]['studio']['name']) metro.append(json.loads(json_data)['features'][i]['studio']['metro']) address.append(json.loads(json_data)['features'][i]['studio']['address']) phone.append(json.loads(json_data)['features'][i]['studio']['phone']) email.append(json.loads(json_data)['features'][i]['studio']['email']) return pd.DataFrame.from_dict({'studio_id': id, 'name': name, 'metro': metro, 'address': address, 'phone': phone, 'email': email}).set_index('studio_id') На выходе процедуры получили таблицу с id студии, названием, ближайшим метро, адресом, телефоном и e-mail'ом. При необходимости можно вытянуть сайт, данные геолокации, текстовое описание студии и прочие данные. 2. Выгрузка списка залов Подробное описание фотостудии с указанием залов находится в папке «ugoloc.ru/studios/show» + id фотостудии. На странице фотостудии находим список ссылок на залы. Список ссылок получаем с помощью библиотек BeautifulSoup и регулярных выражений re:
Код запроса:SPLurl_studio = 'https://ugoloc.ru/studios/show/' + str(584)
html = urllib.request.urlopen(url_studio).read() soup = BeautifulSoup(html, "html.parser") halls_html = soup.find_all('a', href=re.compile('studios/hall/')) Получили список BeautifulSoup-объектов, содержащих ссылки на страницы залов. Дальнейшие действия:
Код процедуры запроса списка заловSPLdef hall_list(studio_id):
st_id = list() hall_id = list() name = list() is_hall = list() square = list() ceiling = list() for id in studio_id: url_studio = 'https://ugoloc.ru/studios/show/' + str(id) html = urllib.request.urlopen(url_studio).read() soup = BeautifulSoup(html, "html.parser") halls_html = soup.find_all('a', href=re.compile('studios/hall/')) halls = dict() for hall in halls_html: if int(hall.get('href').replace('/studios/hall/','')) not in hall_id: st_id.append(id) name.append(hall['title']) hall_id.append(int(hall.get('href').replace('/studios/hall/',''))) if 'грим' in str.lower(hall['title']): is_hall.append(0) else: is_hall.append(1) url_hall = 'https://ugoloc.ru/studios/hall/' + str(hall.get('href').replace('/studios/hall/','')) html_hall = urllib.request.urlopen(url_hall).read() soup_hall = BeautifulSoup(html_hall, "html.parser") try: square.append(int(soup_hall.find_all('div', class_='param-value')[0].contents[0])) except: square.append(np.nan) try: ceiling.append(float(soup_hall.find_all('div', class_='param-value')[1].contents[0])) except: ceiling.append(np.nan) return pd.DataFrame.from_dict({'studio_id': st_id, 'hall_id': hall_id, 'name': name, 'is_hall': is_hall, 'square': square, 'ceiling': ceiling }).set_index('hall_id') 3. Выгрузка данных бронирования за выбранную неделю Для выгрузки данных по бронированию за неделю необходимо найти ссылку на выгрузку json-данных. В данном случае найти ее можно в коде страницы поиском по слову „json“. Первое совпадение (27 строка) содержит переменную: var ajax_url = '/studios/calendar/975.json?week=' Проверим данную ссылку: https://ugoloc.ru/studios/calendar/975.json?week= Работает! Видим данные по часам бронирования, по дням, по стоимости. Значение параметра week по умолчанию равен 0. Для просмотра предыдущих недель берем отрицательные значения: -1 (предыдущая неделя), -2 (2 недели назад) и т.д., — для просмотра следующих, соответственно, положительные значения. Выгружаем json-данные по бронированиюSPLurl_booking = 'https://ugoloc.ru/studios/calendar/' + str(id) + '.json?week=' + str(week)
json_booking = json.loads(urllib.request.urlopen(url_booking).read().decode()) Данные по датам можем увидеть по индексу 'days', по рабочим часам — индекс 'hours', по ценам — индекс 'prices', по минимальном сроке бронирования — индекс 'min_hours', по бронированию — индекс 'bookings'. Собираем данные по бронированию за выбранную неделюSPLdef get_week_booking(id, week=0):
url_booking = 'https://ugoloc.ru/studios/calendar/' + str(id) + '.json?week=' + str(week) json_booking = json.loads(urllib.request.urlopen(url_booking).read().decode()) booking = { 'hall_id': id, 'week': week, 'monday_date': json_booking['days']['1']['date'], 'days': json_booking['days'], 'hours': json_booking['hours'], 'bookings': json_booking['bookings'], 'prices': json_booking['prices'], 'min_hours': json_booking['min_hours'], 'is_opened': 1 if np.sum([len(json_booking['bookings'][str(x)]) for x in range(1, 8)]) > 0 else 0 } time.sleep(.1) return booking Отдельной строкой проверяем доступно ли хотя бы одно бронирование за неделю. Если доступно, то считаем студию открытой. 4. Выгрузка исторических данных бронирования Для загрузки исторических данных загружаем данные бронирования за прошлую неделю, позапрошлую, 3 недели назад и т.д. Основная проблема состоит в том, чтобы определить момент открытия зала. Если запросить данные 1000 недель назад, то API выгрузит нам корректные данные. Формулируем критерий, подтверждающий, что зал закрыт: если зал ни разу не бронировался 2 месяца подряд (9 недель), то он не работал. Используя критерий, напишем процедуруSPLdef get_past_booking(id, weeks_ago = 500):
week = -1 null_period = 9 flag = 0 d = dict() while flag != 1: d[week] = get_week_booking(id, week) if (len(d) > null_period and 1 not in [d[-1 * x]['is_opened'] for x in range(len(d) - 9, len(d))] ): flag = 1 for x in range(0, null_period + 1): del d[week + x] if week < weeks_ago * -1: return d week += -1 time.sleep(1) return d Отдельными параметрами обозначил срок в просмотре данных 10 лет (500 недель), означающий что исторические данные более 10 лет нам не интересны (притом, что агрегатор открыт с 2015 года). Кроме того, устанавливаем срок ожидания между запросом данных в 0,1 секунду. 5. Выгрузка будущих данных В выгрузке данных бронирования будущих периодов важно найти неделю, когда заканчивается основная часть бронирований. Устанавливаем аналогичный с предыдущим пунктом критерий: если в течение 2 месяцев подряд (9 недель) не обнаружено ни одной брони, то дальнейшие периоды можем не просмартивать. Получаем код процедурыSPLdef get_future_booking(id):
week = 0 null_period = 9 flag = 0 d = dict() while (flag != 1 and week <= 30): d[week] = get_week_booking(id, week) if (len(d) > null_period and 1 not in [d[x]['is_opened'] for x in range(len(d) - 9, len(d))]): flag = 1 for x in range(0, null_period): del d[week - x] week += 1 time.sleep(1) return d 6. Расшифровка выгруженных json-данных Для расшифровки json-данных в своем часто использовал методы try, except: если не расшифровывается как привычный тип данных, например dictionary (try), то расшифровываем как другой ожидаемый тип, например list (except). Сейчас понимаю, лучше строить вычисления на проверке типа данных напрямую (функция type()) и дальнейшей их обработкой. Мы написали процедуры по выгрузке данных по бронированию для выбранного зала. Следующей задачей необходимо перевести json-данные в табличный вид DataFrame для удобства дальнейшей обработки или записи в базу данных. Для расшифровки перебираем каждый день недели. Дату переводим из текстового формата в формат датыSPLcur_date = pd.Timestamp(datetime.datetime.strptime(d[week]['days'][str(weeks_day)]['date'], '%d.%m.%Y').isoformat())
Часы бронирования могут быть представлены в виде текста (»12:00"), числа (12), а при круглосуточной работе могут быть указаны конечным числом бронирования (24). Для расшифровки часов работы использовал методы try, exceptSPLtry:
try: working_hour = list([int(x) for x in d[week]['prices'][str(weeks_day)].keys()]) working_hour_is_text = 1 except: working_hour = list(d[week]['prices'][str(weeks_day)].keys()) except: working_hour = list(range(len(d[week]['prices'][str(weeks_day)]))) Если цена бронирования установлена на одном уровне вне зависимости от дня недели и времени, то цена может храниться числом или строкой. Кроме того, цена может храниться в виде списка или словаря. Для расшифровки цены бронирования использовал методы try, exceptSPLtry:
price = list( d[week]['prices'][str(weeks_day)].values() if type(d[week]['prices'][str(weeks_day)].values()) != type(dict()) else d[week]['prices'][str(weeks_day)] ) except: price = list( d[week]['prices'][str(weeks_day)] if type(d[week]['prices'][str(weeks_day)]) != type(dict()) else d[week]['prices'][str(weeks_day)] ) Период бронирования может быть указан как списком доступных для брони часов, так и единственным числом, указывающим на круглосуточный характер брони. Расшифровка доступного для бронирования времени:SPLtry:
booking_hours = sorted([int(x) for x in d[week]['bookings'][str(weeks_day)]]) duration = [d[week]['bookings'][str(weeks_day)][str(h)]['duration'] for h in booking_hours] except: booking_hours = 0 duration = 24 Забронированными являются рабочие часы, недоступные к бронированию. То есть, если зал работает с 10:00 до 22:00 и доступно к бронированию время с 10:00 до 18:00, то время с 18:00 до 22:00 считаем забронированными. Эту логику применяем для вычисление забронированного времени. Общая процедура расшифровки json-данных:SPLdef hall_booking(d):
hour = list(range(24)) df = pd.DataFrame(columns=['hour', 'date', 'is_working_hour', 'price', 'duration', 'week', 'min_hours']) for week in d.keys(): for weeks_day in range(1, 8): working_hour_is_text = 0 cur_date = pd.Timestamp(datetime.datetime.strptime(d[week]['days'][str(weeks_day)]['date'], '%d.%m.%Y').isoformat()) try: try: working_hour = list([int(x) for x in d[week]['prices'][str(weeks_day)].keys()]) working_hour_is_text = 1 except: working_hour = list(d[week]['prices'][str(weeks_day)].keys()) except: working_hour = list(range(len(d[week]['prices'][str(weeks_day)]))) try: price = list( d[week]['prices'][str(weeks_day)].values() if type(d[week]['prices'][str(weeks_day)].values()) != type(dict()) else d[week]['prices'][str(weeks_day)] ) except: price = list( d[week]['prices'][str(weeks_day)] if type(d[week]['prices'][str(weeks_day)]) != type(dict()) else d[week]['prices'][str(weeks_day)] ) try: booking_hours = sorted([int(x) for x in d[week]['bookings'][str(weeks_day)]]) duration = [d[week]['bookings'][str(weeks_day)][str(h)]['duration'] for h in booking_hours] except: booking_hours = 0 duration = 24 min_hours = d[week]['min_hours'] df_temp = pd.DataFrame(hour, columns = ['hour']) df_temp['date'] = cur_date df_temp['is_working_hour'] = [1 if y else 0 for y in [x in working_hour for x in hour]] df_temp['price'] = 0 if len(working_hour) == 24: df_temp['price'] = price else: df_temp.loc[working_hour, 'price'] = price df_temp['duration'] = 0 if duration != 24 and working_hour_is_text == 0: df_temp.loc[[x in booking_hours for x in df_temp['hour']], 'duration'] = duration elif duration != 24 and working_hour_is_text != 0: df_temp.loc[[x in booking_hours for x in df_temp['hour']], 'duration'] = duration else: df_temp.loc[0, 'duration'] = 24 df_temp['week'] = week df_temp['min_hours'] = min_hours df = pd.concat([df, df_temp]) df = df.sort_values(by=['week', 'date', 'hour']) df.index = list(range(len(df))) df['is_booked'] = 0 for i in df.index: if df.loc[i, 'duration'] != 0: if i + df.loc[i, 'duration'] < df.index[-1]: df.loc[i:(i + int(df.loc[i, 'duration'])) - 1, 'is_booked'] = 1 else: df.loc[i:, 'is_booked'] = 1 df['hall_id'] = d[np.min(list(d.keys()))]['hall_id'] return df Итог Мы рассмотрели работу парсера, собирающего данные по бронированию залов московских фотостудий с сайта ugoloc.ru. В результате выгрузили список фотостудий, список залов, список забронированных часов и перевели в формат DataFrame. С полученными уже можно работать, но парсинг занимает продолжительное время и выгруженные данные необходимо где-то хранить. Поэтому в следующей статье я опишу как сохранять полученную информацию в простую базу данных и выгружать их при необходимости. Готовый проект вы можете найти на моей странице в github. =========== Источник: habr.com =========== Похожие новости:
Алгоритмы ), #_vebanalitika ( Веб-аналитика ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:56
Часовой пояс: UTC + 5