В предыдущих двух сериалах я разбирался с ffmpeg (и генерил видео из картинок и текста) и погружался в YouTube API чтобы загружать те самые сгенеренные ролики на YouTube в полуавтоматическом режиме с заголовками, описаниями и тегами. Все это может быть полезно для создания видео из карточек товаров интернет-магазинов.
Зачем? Варианта два:
- вы собственник такого магазина (или работаете в нем) и видео ещё один канал привлечения аудитории и покупателей
- вы зарабатываете на партнерских программах магазинов, например в Адмитаде или ePN и видосики источник супернизкочастотки, при объеме и/или творческом подходе такие видео могут давать нормально трафика
Как бы то ни было вам нужны исходники — картинки и текст. Возможно цены, скидки и отзывы. Если вы работает в магазине, то что-то можно получить изнутри (но порой это сложно). А если вы генерите видео под партнерку, то нет никаких других вариантов кроме как Парсить Интернет-Магазин.
Описание работы кода в видео
Парсинг сайта магазина на Python
Я не буду глубоко погружаться в тему парсинга на данном этапе. Подходов к парсингу много. Не мало сложностей и способов решения этих сложностей.
Моя цель — создать приложение которое парсит картинки и текст, генерит видео и описания к видео и загружает все это на YouTube. Поэтому я НЕ буду парсить сложные магазины в этом сериале (такие как Алиэкспресс, например). Возьму попроще, в сегодняшнем примере Акушерство Ру и Технопарк Ру, оба они есть в Адмитаде.
Инструменты для парсинга магазина на Python
Я использую:
- библиотеку requests для получения страниц магазина
- lxml + xpath для разбора html содержимого страниц
- pillow для скачивания и обработки картинок
- также удобно использовать Developer Tools в Chrome для отладки xpath выражений
Как происходит парсинг
С помощью библиотеки requests я получаю страницу магазина. Если на этапе получения будут ошибки, то скрипт выбросит исключение. Затем ответ я передаю в библиотеку lxml для разбора дерева DOM (иерархия html тегов). Затем с помощью xpath я выделяю в html коде странице нужные мне элементы:
- название товара,
- цену
- картинку
- описание
- могут быть дополнительно — отзывы, скидки, характеристики, но в моем скриптк пока обойдемся без них
Картинку скачиваю с помощью связки библиотек requests + io + pillow и складываю в папку data, а текстовки возвращаю — их я буду использовать и при генерации видео и для описания видео при загрузке его на YouTube.
Исходный код скрипта
Весь исходный код скрипта будет доступен на гитхабе . По мере работы над проектом код будет дополняться, а функционал расширяться (см. соотв. ветки репозитория).
Часть кода ниже, пояснения в комментариях в коде.
Родительский класс ко всем классам магазинов. В нем все общее, не зависящее от верстки и особенностей каждого из магазинов. Функции http запросов, парсинга и обработки dom и т.д.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# coding=utf-8 import io import requests as requests from PIL import Image from lxml import html """ Родительский класс, обобщающий все остальные классы Магазинов позволяет легче добавлять классы для парсинга разных магазинов хорошо бы сделать его абстрактным, но в питоне с этим не очень """ class Shop: errors = [] def __init__(self, shop_name): self.shop_name = shop_name def add_error(self, message, print_error=True, ret=None): """ Add error message and ret value on error """ if print_error: print(message) self.errors.append(message) return ret def has_errors(self): """ Check if has errors """ return len(self.errors) > 0 def first_error(self): """ Get first error or None """ return self.errors[0] if self.has_errors() else None def query(self, url): """ Загружаем страницу и распарсиваем её в XPath :param url: :return: :rtype: requests.Response[] :raises: :exc: """ r = requests.get(url) if r.status_code != requests.codes.ok : raise Exception(f"http code == {r.status_code}") if not r.content or len(r.content) < 7: raise Exception(f"no content at {url}") # инициализирую lxml для парсинга xpath return html.fromstring(r.content.decode('utf-8')) def xp(self, dom, xpath, throw_exc=False, attr_name='text', index=0, ret_none=None): """ Поиск нужного элемента в доме и возврат нужного аттрибута с обработкой ошибок в нужном виде и выбросом исключением только если это нужно :param ret_none: :param dom: :param xpath: :param throw_exc: Можно указать что писать в искл. :param attr_name: :param index: :return: """ try: el = dom.xpath(xpath)[index] if not attr_name: return el v = el.text_content() if attr_name == 'text' else el.get(attr_name) return v except Exception as e: if throw_exc: raise e return self.add_error(str(e), ret_none) def download_image(self, url, filename): """ Сохраняю картинки в data/xxx.jpg :param url: :param filename: :return: """ r = requests.get(url) if r.status_code != requests.codes.ok : raise Exception(f"image downloading error: http code == {r.status_code}") path = f"data/{filename}.jpg" with Image.open(io.BytesIO(r.content)) as im: im.save(path) |
В этом коде пример класса для парсинга одного из магазинов. В данной версии парсинг только карточки товара.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import json import re from shops.shop import Shop class AkusherstvoRu(Shop): def __init__(self): Shop.__init__(self, "akusherstvo.ru") def get_lot_static(self, url='https://www.akusherstvo.ru/catalog/88971-avtokreslo-siger-egida-lyuks/'): """ Парсим карточку товара !!! Парсинг поплывет как только источник изменит верстку :param url: ссылка на карточку :return: """ lot = {} try: dom = self.query(url) # удобный тег - из аттр. content получю цену, а из id - lot_id el = self.xp(dom, '//span[@itemprop="price"]', throw_exc=True, attr_name=None) lot['lot_id'] = int(re.sub("[^0-9]", "", el.get("id"))) # из tover_price_123456 выделяю id товара lot['price'] = int(el.get('content')) lot['title'] = self.xp(dom, '//title', throw_exc=True) lot['img'] = "https:" + self.xp(dom, "//div[@id='itemCard']//div[contains(@class,'itemImg')]//img[1]", throw_exc=True, attr_name='src') self.download_image(lot['img'], f"{self.shop_name}-{lot['lot_id']}") t = self.xp(dom, '//div[@itemprop="description"]', throw_exc=False, ret_none="") lot['description'] = re.sub("(\t|\n|\r)", "", t)[:160] print("lot = ", json.dumps(lot, ensure_ascii=False)) return lot except Exception as e: print("err:", e) return self.add_error(e, True) |
Проблемы Парсинга
При парсинге я привязываюсь к аттрибутам тегов, соотв. если в магазине поменяют верстку, то парсинг сломается. Иногда бывает достаточно добавить в верстку дополнительный элемент чтобы все пошло не так. Для упрощения я в проекте выделил модули парсинга магазина каждый в отдельный класс. Так я могу править парсинг одного конкретного магазина не трогая другие магазины (а их может быть много).
Картинки бывают разных форматов и размеров. Особенно проблемны webp. А форматы и размеры нам будут важны при генерации видосиков. Поэтому я подключил Pillow — с ним будет проще разбираться с картинками дальше.
Все больше и больше магазинов используют js фреймворки или просто динамический фронтенд, а всю логику отдают в json. Способ в сегодняшнем примере не подойдет для парсинга такого магазина. Впрочем, если магазин не настроен серьезно защищаться от парсеров, разобраться с таким парсингом не сложно.