Пицца для коллег. Часть 1.

В нашем рабочем коллективе принято на праздники, либо на памятные события, угощать коллег пиццей. Когда привычные комбо наборы уже порядком надоели, мы решили, что будем выбирать пиццы самостоятельно. Но как угодить всему коллективу из 20+ человек? Мы работает в ИТ-отделе, поэтому этот процесс нужно автоматизировать.

В качестве инструмента автоматизации выбран telegram-бот. Логика работы достаточно проста. Внутри нашего отдела есть свой чат, добавляем бота в него и запускаем голосование. Пользователи выбирают понравившиеся N пицц из заранее определенного списка. Список формируется парсингом сайта пиццерии или забирается по API. Далее выбранные пиццы ранжируются в соответствии с голосами пользователей и формируется итоговый список к заказу.

В этой статье рассмотрим момент парсинга сайта пиццерии на примере пермской сети Пиццбург.

Первым делом загружаем страницу с меню с помощью библиотеки requests:

        if need_load or not exists(PizzburgParser.MENU_PATH):
            r = requests.get(PizzburgParser.MENU_URL, headers=headers)
            with open(PizzburgParser.HTML_PATH, 'w', encoding='utf-8') as menu_file:
                menu_page = r.text
                menu_file.write(r.text)
        else:
            with open(PizzburgParser.HTML_PATH, 'r', encoding='utf-8') as menu_file:
                menu_page = menu_file.read()

Для парсинга используем стандартные инструменты — BeautifulSoup с парсером lxml.

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

В целом парсер простой. Отдельно выделю два момента реализации, которые были добавлены в функционал со временем. Первый — это выделение острой пиццы. На сайте это сделано картинками с перчиком. Я же добавил эту информацию в название пиццы, чтобы это можно было видеть в момент голосования.

Второй момент — это пицца недели. В меню на сайте это отдельный элемент со своими ценами. Нам этого не нужно, зато нам нужно на соответствующую пиццу назначить цену пиццы недели. Сама же пицца недели в меню выделена картинкой в блоке с классом product-unit__status_sale. По этому признаку её и идентифицируем.

Ниже приведен фрагмент кода для парсинга.

        soup = BeautifulSoup(menu_page, 'lxml')
        week_pizza = None
        week_pizza_prices = None
        pizzas = []
        items = soup.find_all('div', class_='shk-item')
        for x in items:
            title = x.find('div', class_='product-unit__title').text.strip()
            info = x.find('div', class_='product-unit__info').text.strip()
            variants = x.find_all('div', class_='radio_param')
            dia_pattern = re.compile(r'(\d+)\s+см')
            h_pattern = re.compile(r'(пышное|тонкое)\s+тесто')
            prices = []
            for y in variants:
                v_name = y.find('input', {'name': 'name'}).get('value')
                prices.append({
                    'dia': int(re.search(dia_pattern, v_name).groups()[0]),
                    'h': re.search(h_pattern, v_name).groups()[0],
                    'price': float(y.find('input', {'name': 'price'}).get('value'))
                })
            if title.lower() == 'пицца недели':
                week_pizza_prices = prices
                continue
            hot_level = 0
            hot_unit = x.find('div', class_='product-unit__hot')
            if hot_unit is not None:
                hot_classes = hot_unit.attrs.get('class')
                hot_classes.remove('product-unit__hot')
                hot_level = int(hot_classes[0].replace('product-unit__hot_', ''))
            hot_suffix = '\U0001F336' * hot_level
            title = (title.replace('Пицца', ' ').strip() + ' ' + hot_suffix).strip()
            pizza_item = {'title': title, 'info': info, 'hot': hot_level, 'variants': prices}
            if x.find('div', class_='product-unit__status_sale') is not None:
                week_pizza = pizza_item
            pizzas.append(pizza_item)
        # обрабатываем пиццу недели >>>
        if week_pizza is not None and week_pizza_prices is not None:
            prices = week_pizza.get('variants')
            for wpp in week_pizza_prices:
                for p in prices:
                    if wpp.get('dia') == p.get('dia') and wpp.get('h') == p.get('h'):
                        p['price'] = wpp.get('price')
        # обрабатываем пиццу недели <<<
        if not exists(PizzburgParser.DATA_PATH):
            mkdir(PizzburgParser.DATA_PATH)
        with open(PizzburgParser.MENU_PATH, 'w', encoding='utf-8') as menu_file:
            json.dump(pizzas, menu_file, indent=4, ensure_ascii=False)
        return pizzas

На выходе получаем унифицированное меню вида:

[
	{
		'title': 'Название для меню',
		'info': 'Состав',
		'hot': степень_остроты,
		'variants': [
			{
				'dia': диаметр,
				'h': 'тесто пышное|тонкое',
				'price': цена
			}
		]
	}
]

В итоге, чтобы добавить в голосование другую пиццерию, нам достаточно сделать парсер (или обращение к API пиццерии), который аналогичным образом сформирует меню.

Весь код проекта можно посмотреть на GitHub.