Привет!

В этом посте вы узнаете, как использовать Python для автоматизации рутинной работы по поиску отелей на booking.com. Мы поможем вам получить лучшее предложение для вашего следующего отпуска. Я недавно создал этот сценарий, чтобы помочь выбрать лучший вариант для моей летней поездки, поэтому решил поделиться им с сообществом.

Эта статья пригодится вам в случае, если:

  1. Вы изучаете Python и хотите применить свои навыки для решения реальных проблем.
  2. Вы уже знаете Python и хотите, чтобы шаблонный код контролировал booking.com, поэтому вам не нужно писать его самостоятельно.

Сценарий, который мы собираемся написать, работает лучше всего, если он запускается по расписанию автоматически, а вы не запускаете его вручную. Для этого есть несколько вариантов (например, вы можете настроить сервер и создать задание cron). Я рекомендую использовать seamlesscloud.io, инструмент, который я разрабатываю прямо сейчас, и он создан специально для этой цели.

Мы собираемся написать сценарий, который найдет три самых дешевых отеля с рейтингом 9+ и покажет нам цену на два номера, всего на четырех человек (потому что я путешествовал с друзьями). Хорошо, для тех из вас, кто просто хочет код, вот он:

import datetime
import urllib

import requests
from bs4 import BeautifulSoup

session = requests.Session()

REQUEST_HEADER = {
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.75 "
                  "Safari/537.36"}
BOOKING_URL = 'https://www.booking.com'

# https://core.telegram.org/bots
BOT_API_KEY = 'your-api-key'
CHANNEL_NAME = '@booking_monitoring'


class Hotel:
    raw_html = None
    name = None
    score = None
    price = None
    link = None
    details = None

    def __init__(self, raw_html):
        self.raw_html = raw_html
        self.name = get_hotel_name(raw_html)
        self.score = get_hotel_score(raw_html)
        self.price = get_hotel_price(raw_html)
        self.link = get_hotel_detail_link(raw_html)

    def get_details(self):
        if self.link:
            self.details = HotelDetails(self.link)


class HotelDetails:
    latitude = None
    longitude = None

    def __init__(self, details_link):
        detail_page_response = session.get(BOOKING_URL + details_link, headers=REQUEST_HEADER)
        soup_detail = BeautifulSoup(detail_page_response.text, "lxml")
        self.latitude = get_coordinates(soup_detail)[0]
        self.longitude = get_coordinates(soup_detail)[1]


def create_url(people, country, city, date_in, date_out, rooms, score_filter):
    url = f"https://www.booking.com/searchresults.en-gb.html?selected_currency=USD&checkin_month={date_in.month}" \
          f"&checkin_monthday={date_in.day}&checkin_year={date_in.year}&checkout_month={date_out.month}" \
          f"&checkout_monthday={date_out.day}&checkout_year={date_out.year}&group_adults={people}" \
          f"&group_children=0&order=price&ss={city}%2C%20{country}" \
          f"&no_rooms={rooms}"
    if score_filter:
        if score_filter == '9+':
            url += '&nflt=review_score%3D90%3B'
        elif score_filter == '8+':
            url += '&nflt=review_score%3D80%3B'
        elif score_filter == '7+':
            url += '&nflt=review_score%3D70%3B'
        elif score_filter == '6+':
            url += '&nflt=review_score%3D60%3B'
    return url


def get_search_result(people, country, city, date_in, date_out, rooms, score_filter):
    result = []
    data_url = create_url(people, country, city, date_in, date_out, rooms, score_filter)
    response = session.get(data_url, headers=REQUEST_HEADER)
    soup = BeautifulSoup(response.text, "lxml")
    hotels = soup.select("#hotellist_inner div.sr_item.sr_item_new")
    for hotel in hotels:
        result.append(Hotel(hotel))
    session.close()
    return result


def get_hotel_name(hotel):
    identifier = "span.sr-hotel__name"
    if hotel.select_one(identifier) is None:
        return ''
    else:
        return hotel.select_one(identifier).text.strip()


def get_hotel_score(hotel):
    identifier = "div.bui-review-score__badge"
    if hotel.select_one(identifier) is None:
        return ''
    else:
        return hotel.select_one(identifier).text.strip()


def get_hotel_price(hotel):
    identifier = "div.bui-price-display__value.prco-text-nowrap-helper.prco-inline-block-maker-helper"
    if hotel.select_one(identifier) is None:
        return ''
    else:
        return hotel.select_one(identifier).text.strip()[2:]


def get_hotel_detail_link(hotel):
    identifier = ".txp-cta.bui-button.bui-button--primary.sr_cta_button"
    if hotel.select_one(identifier) is None:
        return ''
    else:
        return hotel.select_one(identifier)['href']


def get_coordinates(soup_detail):
    coordinates = []
    if soup_detail.select_one("#hotel_sidebar_static_map") is None:
        coordinates.append('')
        coordinates.append('')
    else:
        coordinates.append(soup_detail.select_one("#hotel_sidebar_static_map")["data-atlas-latlng"].split(",")[0])
        coordinates.append(soup_detail.select_one("#hotel_sidebar_static_map")["data-atlas-latlng"].split(",")[1])
    return coordinates


def send_message(html):
    resp = requests.get(f'https://api.telegram.org/bot{BOT_API_KEY}/sendMessage?parse_mode=HTML&'
                        f'chat_id={CHANNEL_NAME}&'
                        f'text={urllib.parse.quote_plus(html)}')
    resp.raise_for_status()


def send_location(latitude, longitude):
    resp = requests.get(f'https://api.telegram.org/bot{BOT_API_KEY}/sendlocation?'
                        f'chat_id={CHANNEL_NAME}&'
                        f'latitude={latitude}&longitude={longitude}')
    resp.raise_for_status()


def main():
    search_params = {
        'people': 4,
        'rooms': 2,
        'country': 'United States',
        'city': 'New York',
        'date_in': datetime.datetime(2020, 8, 31).date(),
        'date_out': datetime.datetime(2020, 9, 2).date(),
        'score_filter': '9+'
    }

    print(f"Searching hotels using parameters: {search_params}")
    result = get_search_result(**search_params)
    top_3 = result[:3]
    send_message(
        f'Here are your search results for {search_params["people"]} people, {search_params["rooms"]} rooms in '
        f'{search_params["city"]}, {search_params["country"]} for dates from {search_params["date_in"]} to '
        f'{search_params["date_out"]} with {search_params.get("score_filter", "any")} rating')
    for hotel in top_3:
        send_message(f'<a href="{BOOKING_URL}{hotel.link}">{hotel.name} </a> ({hotel.score})\n'
                     f'Total price: {hotel.price}')
        hotel.get_details()
        send_location(hotel.details.latitude, hotel.details.longitude)
    print('Notifications were sent successfully')


if __name__ == '__main__':
    main()

Вы также можете найти полный код здесь.

Теперь для тех из вас, кто хотел бы получить некоторые объяснения, давайте углубимся в детали.

Парсинг веб-сайтов

Давайте посмотрим на функцию get_search_result. Внутри вы заметите, что мы сначала создаем URL.

def create_url(people, country, city, date_in, date_out, rooms, score_filter):
    url = f"https://www.booking.com/searchresults.en-gb.html?selected_currency=USD&checkin_month={date_in.month}" \
          f"&checkin_monthday={date_in.day}&checkin_year={date_in.year}&checkout_month={date_out.month}" \
          f"&checkout_monthday={date_out.day}&checkout_year={date_out.year}&group_adults={people}" \
          f"&group_children=0&order=price&ss={city}%2C%20{country}" \
          f"&no_rooms={rooms}"
    if score_filter:
        if score_filter == '9+':
            url += '&nflt=review_score%3D90%3B'
        elif score_filter == '8+':
            url += '&nflt=review_score%3D80%3B'
        elif score_filter == '7+':
            url += '&nflt=review_score%3D70%3B'
        elif score_filter == '6+':
            url += '&nflt=review_score%3D60%3B'
    return url

Это URL-адрес, который вы увидите в своем браузере, если будете просто искать отели вручную. Мы просто программно вставляем фильтры и генерируем URL из кода, вот и все.

Затем мы просто делаем запрос GET к URL-адресу и получаем результат.

response = session.get(data_url, headers=REQUEST_HEADER)

Затем мы используем библиотеку BeautifulSoup для анализа ответа.

soup = BeautifulSoup(response.text, "lxml")

BeautifulSoup - самая популярная библиотека Python, используемая для понимания веб-страниц (в нашем случае веб-страница booking.com). Библиотека помогает преобразовать текстовое представление страницы в объект с атрибутами и методами поиска, которые вы можете использовать в своем коде.

Вот как вы можете получить список отелей со страницы:

hotels = soup.select("#hotellist_inner div.sr_item.sr_item_new")

Что это значит? Что за странную строку мы выбираем? Если вы откроете созданный нами URL-адрес в браузере и воспользуетесь инструментами разработчика (я использую Chrome), вы увидите следующее:

hotellist_inner - это идентификатор HTML-элемента. Он выделен в моем браузере, и я вижу, что он соответствует списку отелей в результатах поиска.

div.sr_item.sr_item_new означает div элемент с классами sr_item и sr_item_new.

И это пример такого элемента. Мы фактически выбираем все элементы div, которые имеют классы sr_item и sr_item_new и расположены внутри элемента с id = hotellist_inner.

Наш следующий шаг - перебрать отели и проанализировать каждый из них индивидуально.

for hotel in hotels: 
    result.append(Hotel(hotel))

Если вы посмотрите на метод __init__ класса Hotel, вы увидите, что мы используем набор функций для получения различной информации из элемента отеля на веб-странице. Я не буду здесь вдаваться в подробности, но они работают аналогично логике выбора элемента отеля, которую я описал выше.

Отправка сообщений в Telegram

После того, как мы нашли отели, нам нужен способ получать уведомления. В этом разделе я объясню, как работает код, отправляющий информацию в мое приложение Telegram. Telegram - это мессенджер, которым я пользуюсь. Вы можете использовать другой. Однако также должна быть возможность отправить сообщение из Python. Сегодня у большинства мессенджеров есть API для ботов. Подробнее о ботах Telegram можно прочитать здесь.

Следуйте документации и создайте своего бота. Самый простой способ получать сообщения от бота - создать общедоступный канал, а затем добавить бота в этот канал. Почему публичный? Потому что тогда для отправки сообщений боту нужно только название канала. Есть способы отправлять сообщения на частный канал или вам напрямую, но они требуют дополнительных действий, для наших целей общедоступный канал - лучший подход.

Теперь, чтобы отправить сообщение на этот канал, мне нужно всего лишь объявить две константы:

BOT_API_KEY = 'you-api-key' CHANNEL_NAME = '@booking_monitoring'

BOT_API_KEY вы получите после того, как создадите своего бота. CHANNEL_NAME - это просто название публичного канала. Используйте имя созданного вами канала.

Код для отправки сообщения - это простой запрос на получение к API бота Telegram.

def send_message(html):
    resp = requests.get(f'https://api.telegram.org/bot{BOT_API_KEY}/sendMessage?parse_mode=HTML&chat_id={CHANNEL_NAME}&text={urllib.parse.quote_plus(html)}')

Для каждого отеля я также отправляю его местоположение следующим образом:

def send_location(latitude, longitude):
    resp = requests.get(f'https://api.telegram.org/bot{BOT_API_KEY}/sendlocation?chat_id={CHANNEL_NAME}&latitude={latitude}&longitude={longitude}')

В результате в своем приложении Telegram я получаю вот что:

Довольно удобно, так как я получаю общую информацию прямо в моем мессенджере, расположение отеля, которое я могу сразу изучить, и ссылку на booking.com со всей остальной информацией.

Собираем все вместе

Итак, в итоге у нас есть скрипт, который получает 3 самых дешевых отеля на основе поиска с использованием определенных вами фильтров (сортировка указывается в функции, которая создает URL-адрес).

result = get_search_result(**search_params) top_3 = result[:3]

Затем для каждого из 3 отелей мы отправляем сообщение:

for hotel in top_3:
    send_message(f'<a href="{BOOKING_URL}{hotel.link}">{hotel.name} </a> ({hotel.score})\n'
                 f'Total price: {hotel.price}')
    hotel.get_details()
    send_location(hotel.details.latitude, hotel.details.longitude)

Заставляем наш скрипт работать в фоновом режиме

Если этот скрипт не запускается по расписанию в фоновом режиме, он не очень полезен - вам лучше просто зайти на страницу booking.come в браузере, не используя Python и выполняя поиск вручную. Есть много способов заставить ваш сценарий работать. CRON, наверное, самая популярная вещь для этого.

Следует иметь в виду, что вы не хотите запускать этот сценарий на своем локальном компьютере, поскольку, когда вы закроете свой ноутбук или выключите рабочий стол, он остановится, и вы не получите никаких уведомлений. Вы можете где-нибудь развернуть сервер и использовать там CRON. Или вы можете…

Внимание, реклама внизу. Извините.

Или вы можете использовать seamlesscloud.io - инструмент, который я разрабатываю вместе с парой других инженеров. У него хорошо получается одно - запускать скрипты Python по расписанию. Не стесняйтесь проверить это.

Если у вас возникнут проблемы, напишите мне письмо, и я постараюсь вам помочь. Вы также можете найти полный код, использованный в этом посте, здесь.

Подробнее читайте в нашем блоге здесь.

Первоначально опубликовано на https://blog.seamlesscloud.io 7 августа 2020 г.