Зачем вам нужно учиться парсить веб-страницы? Если ваша работа не требует от вас изучения, позвольте мне дать вам некоторую мотивацию.

Что, если вы хотите создать веб-сайт, на котором будут продаваться самые дешевые товары с Amazon, Walmart и нескольких других интернет-магазинов? Многие из этих интернет-магазинов не предоставляют простой способ доступа к своей информации с помощью API. В отсутствие API ваш единственный выбор - создать парсер. Это позволяет автоматически извлекать информацию с этих веб-сайтов и упрощает использование этой информации.

Вот пример типичного ответа API в формате JSON. Это ответ Reddit:

Существует множество библиотек Python, которые могут помочь вам в парсинге веб-страниц. Есть lxml, BeautifulSoup и полноценный фреймворк под названием Scrapy.

В большинстве руководств обсуждаются BeautifulSoup и Scrapy, поэтому в этом посте я решил использовать lxml. Я научу вас основам XPath и тому, как вы можете использовать их для извлечения данных из HTML-документа. Я проведу вас через несколько разных примеров, чтобы вы могли быстро освоить работу с lxml и XPaths.

Получение данных

Если вы геймер, вы уже знаете (и, вероятно, полюбите) этот веб-сайт. Мы будем пытаться извлечь данные из Steam. В частности, мы будем выбирать из популярных новинок информацию.

Я превращаю этот процесс в серию из двух частей. В этой части мы будем создавать скрипт Python, который может извлекать названия игр, цены на игры, различные теги, связанные с каждой игрой и целевыми платформами. Во второй части мы превратим этот скрипт в API на основе Flask, а затем разместим его на Heroku.

Прежде всего, откройте страницу Популярные новые выпуски в Steam и прокрутите вниз, пока не увидите вкладку Популярные новые выпуски. На этом этапе я обычно открываю инструменты разработчика Chrome и смотрю, какие HTML-теги содержат необходимые данные. Я широко использую инструмент инспектора элементов (кнопка в верхнем левом углу инструментов разработчика). Он позволяет одним щелчком мыши увидеть разметку HTML за определенным элементом на странице.

Как общий обзор, все на веб-странице инкапсулируется в теги HTML, а теги обычно являются вложенными. Вам нужно выяснить, из каких тегов нужно извлекать данные, и все будет готово. В нашем случае, если мы посмотрим, мы увидим, что каждый отдельный элемент списка инкапсулирован в тег привязки (a).

Сами теги привязки инкапсулируются в div с идентификатором tab_newreleases_content. Я упоминаю идентификатор, потому что на этой странице есть две вкладки. Вторая вкладка - это стандартная вкладка «Новые выпуски», и мы не хотим извлекать информацию из этой вкладки. Итак, сначала мы извлечем вкладку «Популярные новые выпуски», а затем извлечем необходимую информацию из этого тега.

Это идеальное время для создания нового файла Python и начала записи нашего скрипта. Я собираюсь создать scrape.py файл. Теперь давайте продолжим и импортируем необходимые библиотеки. Первая - это библиотека requests, вторая - это библиотека lxml.html.

import requests import lxml.html

Если у вас не установлен requests, вы можете легко установить его, выполнив следующую команду в терминале:

$ pip install requests

Библиотека запросов поможет нам открыть веб-страницу на Python. Мы могли бы использовать lxml и для открытия HTML-страницы, но это не работает со всеми веб-страницами. На всякий случай я собираюсь использовать requests.

Извлечение и обработка информации

Теперь давайте откроем веб-страницу с помощью запросов и передадим этот ответ lxml.html.fromstring.

html = requests.get('https://store.steampowered.com/explore/new/') doc = lxml.html.fromstring(html.content)

Это дает нам объект типа HtmlElement. Этот объект имеет xpath метод, который мы можем использовать для запроса HTML-документа. Это дает нам структурированный способ извлечения информации из HTML-документа.

Теперь сохраните этот файл и откройте терминал. Скопируйте код из файла scrape.py и вставьте его в сеанс интерпретатора Python.

Мы делаем это для того, чтобы мы могли быстро протестировать наши XPath без постоянного редактирования, сохранения и выполнения нашего scrape.py файла.

Давайте попробуем написать XPath для извлечения div, который содержит вкладку «Популярные новые выпуски». Я объясню код по ходу дела:

new_releases = doc.xpath('//div[@id="tab_newreleases_content"]')[0]

Этот оператор вернет список всех divs на HTML-странице с идентификатором tab_newreleases_content. Теперь, поскольку мы знаем, что только один div на странице имеет этот идентификатор, мы можем извлечь первый элемент из списка ([0]), и это будет наш необходимый div. Давайте разберем xpath и попробуем понять его:

  • // эта двойная косая черта указывает lxml, что мы хотим искать все теги в документе HTML, которые соответствуют нашим требованиям / фильтрам. Другой вариант - использовать / (одинарная косая черта). Одиночная косая черта возвращает только непосредственные дочерние теги / узлы, которые соответствуют нашим требованиям / фильтрам.
  • div сообщает lxml, что мы ищем divs на странице HTML.
  • [@id="tab_newreleases_content"] сообщает lxml, что нас интересуют только те divs, которые имеют идентификатор tab_newreleases_content

Прохладный! У нас есть необходимый div. Теперь вернемся в Chrome и проверим, какой тег содержит названия релизов.

Заголовок содержится в div с классом tab_item_name. Теперь, когда у нас выделена вкладка «Популярные новые версии», мы можем выполнять дальнейшие запросы XPath на этой вкладке. Запишите следующий код в той же консоли Python, в которой мы ранее запускали наш код:

titles = new_releases.xpath('.//div[@class="tab_item_name"]/text()')

Это дает нам названия всех игр на вкладке «Популярные новые выпуски». Вот ожидаемый результат:

Давайте немного разберем этот XPath, потому что он немного отличается от предыдущего.

  • . сообщает lxml, что нас интересуют только теги, которые являются дочерними для тега new_releases
  • [@class="tab_item_name"] очень похож на то, как мы отфильтровывали divs на основе id. Единственная разница в том, что здесь мы выполняем фильтрацию по имени класса.
  • /text() сообщает lxml, что мы хотим, чтобы текст содержался в только что извлеченном теге. В этом случае он возвращает заголовок, содержащийся в div с именем класса tab_item_name.

Теперь нам нужно извлечь цены на игры. Это легко сделать, запустив следующий код:

prices = new_releases.xpath('.//div[@class="discount_final_price"]/text()')

Я не думаю, что мне нужно объяснять этот код, поскольку он очень похож на код извлечения заголовка. Единственное изменение, которое мы внесли, - это изменение имени класса.

Теперь нам нужно извлечь теги, связанные с заголовками. Вот разметка HTML:

Запишите следующий код в терминале Python для извлечения тегов:

tags = new_releases.xpath('.//div[@class="tab_item_top_tags"]')
total_tags = []
for tag in tags:
    total_tags.append(tag.text_content())

Итак, мы извлекаем divs, содержащий теги для игр. Затем мы перебираем список извлеченных тегов, а затем извлекаем текст из этих тегов с помощью метода text_content(). text_content() возвращает текст, содержащийся в теге HTML, без разметки HTML.

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

tags = [tag.text_content() for tag in new_releases.xpath('.//div[@class="tab_item_top_tags"]')]

Давайте также разделим теги в списке, чтобы каждый тег был отдельным элементом:

tags = [tag.split(', ') for tag in tags]

Теперь осталось только извлечь платформы, связанные с каждым тайтлом. Вот разметка HTML:

Основное отличие здесь в том, что платформы не содержатся в виде текстов в определенном теге. Они указаны как имя класса. С некоторыми играми связана только одна платформа, например:

<span class="platform_img win"></span>

Хотя с некоторыми играми связано 5 платформ, например:

<span class="platform_img win"></span>
<span class="platform_img mac"></span>
<span class="platform_img linux"></span>
<span class="platform_img hmd_separator"></span> 
<span title="HTC Vive" class="platform_img htcvive"></span> 
<span title="Oculus Rift" class="platform_img oculusrift"></span>

Как мы видим, эти spans содержат тип платформы в качестве имени класса. Единственное, что объединяет эти spans, - это то, что все они содержат класс platform_img. Прежде всего, мы извлечем divs с классом tab_item_details. Затем мы извлечем spans, содержащий класс platform_img. Наконец, мы извлечем имя второго класса из этих spans. Вот код:

platforms_div = new_releases.xpath('.//div[@class="tab_item_details"]')
total_platforms = []

for game in platforms_div:
    temp = game.xpath('.//span[contains(@class, "platform_img")]')
    platforms = [t.get('class').split(' ')[-1] for t in temp]
    if 'hmd_separator' in platforms:
        platforms.remove('hmd_separator')
    total_platforms.append(platforms)

В строке 1 мы начинаем с извлечения файла tab_item_details div. XPath в строке 5 немного отличается. Здесь у нас [contains(@class, "platform_img")] вместо простого [@class="platform_img"]. Причина в том, что [@class="platform_img"] возвращает те spans, с которыми связан только класс platform_img. Если у spans есть дополнительный класс, они не будут возвращены. В то время как [contains(@class, "platform_img")] фильтрует все spans, которые имеют класс platform_img. Не имеет значения, единственный ли это класс или с этим тегом связано больше классов.

В строке 6 мы используем анализ списка, чтобы уменьшить размер кода. Метод .get() позволяет нам извлекать атрибут тега. Здесь мы используем его для извлечения атрибута class из span. Получаем строку обратно из метода .get(). В случае первой игры возвращается строка platform_img win, поэтому мы разбиваем эту строку на основе запятой и пробела. Затем мы сохраняем последнюю часть (которое является фактическим названием платформы) разделенной строки в списке.

В строках 7–8 мы удаляем hmd_separator из списка, если он существует. Это потому, что hmd_separator не является платформой. Это просто вертикальная разделительная полоса, используемая для отделения реальных платформ от оборудования VR / AR.

Вот код, который у нас есть на данный момент:

import requests
import lxml.html

html = requests.get('https://store.steampowered.com/explore/new/')
doc = lxml.html.fromstring(html.content)

new_releases = doc.xpath('//div[@id="tab_newreleases_content"]')[0]

titles = new_releases.xpath('.//div[@class="tab_item_name"]/text()')
prices = new_releases.xpath('.//div[@class="discount_final_price"]/text()')

tags = [tag.text_content() for tag in new_releases.xpath('.//div[@class="tab_item_top_tags"]')]
tags = [tag.split(', ') for tag in tags]

platforms_div = new_releases.xpath('.//div[@class="tab_item_details"]')
total_platforms = []

for game in platforms_div:
    temp = game.xpath('.//span[contains(@class, "platform_img")]')
    platforms = [t.get('class').split(' ')[-1] for t in temp]
    if 'hmd_separator' in platforms:
        platforms.remove('hmd_separator')
    total_platforms.append(platforms)

Теперь нам просто нужно вернуть ответ JSON, чтобы мы могли легко превратить его в API на основе Flask. Вот код:

output = []
for info in zip(titles,prices, tags, total_platforms):
    resp = {}
    resp['title'] = info[0]
    resp['price'] = info[1]
    resp['tags'] = info[2]
    resp['platforms'] = info[3]
    output.append(resp)

Этот код не требует пояснений. Мы используем функцию zip для параллельного перебора всех этих списков. Затем мы создаем словарь для каждой игры и назначаем название, цену, теги и платформы в качестве отдельного ключа в этом словаре. Наконец, мы добавляем этот словарь в список вывода.

Подведение итогов

В следующем посте мы рассмотрим, как мы можем преобразовать это в API на основе Flask и разместить его на Heroku.

Я Ясуб из Python Tips. Надеюсь, вам понравился этот урок. Если вы хотите прочитать больше руководств аналогичного характера, перейдите в Советы по Python. Я регулярно пишу в этом блоге советы, рекомендации и учебные пособия по Python. А если вы заинтересованы в изучении Python среднего уровня, то, пожалуйста, ознакомьтесь с моей книгой с открытым исходным кодом здесь.

Просто оговорка: мы - лесозаготовительная компания here @ Timber. Мы были бы рады, если бы вы опробовали наш продукт (это действительно здорово!), Но вы здесь, чтобы узнать о парсинге веб-страниц на Python, и мы не хотели отказываться от этого.

Изначально опубликовано на timber.io.