CoderCastrov logo
CoderCastrov
Питон

Овладение парсингом веб-страниц на Python: от нуля до героя

Овладение парсингом веб-страниц на Python: от нуля до героя
просмотров
13 мин чтение
#Питон

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

Предварительные требования

Для работы кода вам понадобится установленный python3. На некоторых системах он уже установлен. После этого установите все необходимые библиотеки, запустив pip install.

pip install requests beautifulsoup4 pandas

Получение HTML с URL-адреса легко с помощью библиотеки requests. Затем передайте содержимое в BeautifulSoup, и мы можем начать получать данные и выполнять запросы с помощью селекторов. Мы не будем вдаваться в подробности. Вкратце, вы можете использовать CSS-селекторы для получения элементов и контента страницы. Некоторые требуют другого синтаксиса, но об этом мы узнаем позже.

import requests
from bs4 import BeautifulSoup

response = requests.get("https://zenrows.com")
soup = BeautifulSoup(response.content, 'html.parser')

print(soup.title.string) _# Web Data Automation Made Easy - ZenRows_

Чтобы избежать запроса HTML каждый раз, мы можем сохранить его в файл HTML и загрузить BeautifulSoup из него. Для простой демонстрации мы можем сделать это вручную. Простой способ сделать это - просмотреть исходный код страницы, скопировать и вставить его в файл. Важно посетить страницу без авторизации, как это сделал бы веб-сканер.

Получение HTML здесь может показаться простой задачей, но это далеко не так. Мы не будем рассматривать это в данном блоге, но это заслуживает отдельного руководства. Наш совет - использовать этот статический подход, так как многие веб-сайты перенаправят вас на страницу входа после нескольких запросов. Некоторые другие покажут капчу, и в худшем случае ваш IP-адрес будет заблокирован.

with open("test.html") as fp:
    soup = BeautifulSoup(fp, "html.parser")

print(soup.title.string) _# Web Data Automation Made Easy - ZenRows_

После загрузки статически из файла мы можем тестировать столько раз, сколько хотим, без каких-либо проблем с сетью или блокировкой.

Исследование перед написанием кода

Перед тем, как мы начнем писать код, нам необходимо понять содержимое и структуру страницы. Для этого самым простым способом является использование инструментов разработчика в браузере. Мы будем использовать инструменты разработчика Chrome, но в других браузерах есть аналогичные инструменты.

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

Будьте осторожны при использовании инструментов разработчика Chrome или аналогичных средств. Вы увидите содержимое только после того, как JavaScript и сетевые запросы его (возможно) изменят. Это может быть утомительно, но иногда нам нужно исследовать исходный HTML, чтобы избежать выполнения JavaScript. Нам не понадобится запускать безголовый браузер - например, Puppeteer - если мы найдем все, что нам нужно, это сэкономит время и потребление памяти.

Отказ от ответственности: мы не будем включать URL-запрос в кодовые фрагменты для каждого примера. Все они выглядят как первый. И помните, сохраняйте локально файл HTML, если вы собираетесь тестировать его несколько раз.

Скрытые входы

Скрытые входы позволяют разработчикам включать поля ввода, которые конечные пользователи не могут видеть или изменять. Многие формы используют их для включения внутренних идентификаторов или защитных токенов.

В продуктах Amazon мы можем видеть, что их гораздо больше. Некоторые из них будут доступны в других местах или форматах, но иногда они являются уникальными. В любом случае, имена скрытых входов обычно более стабильны, чем классы.

Метаданные

Хотя некоторый контент виден через пользовательский интерфейс, его может быть проще извлечь, используя метаданные. Вы можете получить количество просмотров в числовом формате и дату публикации в формате ГГГГ-мм-дд в видео на YouTube. Оба значения можно получить из видимой части, но нет необходимости в этом. Потратив несколько минут на использование этих техник, можно сэкономить время.

interactionCount = soup.find('meta', itemprop="interactionCount")
print(interactionCount['content']) _# 8566042_

datePublished = soup.find('meta', itemprop="datePublished")
print(datePublished['content']) _# 2014-01-09_

Запросы XHR

Некоторые другие веб-сайты решают загружать пустой шаблон и получать все данные через запросы XHR. В таких случаях проверка только исходного HTML недостаточна. Нам нужно изучать сетевую активность, в частности запросы XHR.

Так и происходит на сайте Auction. Заполните форму любым городом и выполните поиск. Вас перенаправит на страницу результатов с заготовкой страницы, пока выполняются запросы для введенного вами города.

Это заставляет нас использовать безголовый браузер, который может выполнять JavaScript и перехватывать сетевые запросы, но мы также увидим его преимущества. Иногда можно обратиться к конечной точке XHR напрямую, но обычно для этого требуются файлы cookie или другие методы аутентификации. Или они могут мгновенно заблокировать вас, так как это необычный путь для пользователя. Будьте осторожны.

Мы нашли золото. Посмотрите на изображение еще раз.

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

Рецепты и трюки для извлечения надежного контента

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

Мы пытаемся дать вам больше инструментов и идей. Затем это будет вашим решением каждый раз.

Получение внутренних ссылок

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

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

internalLinks = [
	a.get('href') for a in soup.find_all('a')
	if a.get('href') and a.get('href').startswith('/')]
print(internalLinks)

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

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

Извлечение ссылок на социальные сети и электронной почты

Еще одна распространенная задача парсинга - это извлечение ссылок на социальные сети и электронной почты. Нет точного определения для "ссылок на социальные сети", поэтому мы будем получать их на основе домена. Что касается электронной почты, есть два варианта: ссылки "mailto" и проверка всего текста.

Для этой демонстрации мы будем использовать тестовый сайт для парсинга.

В первом фрагменте кода мы получим все ссылки, аналогично предыдущему примеру. Затем мы пройдемся по каждой ссылке, проверяя, есть ли в ней какие-либо домены социальных сетей или "mailto". В этом случае добавим этот URL в список и, наконец, выведем его.

links = [a.get('href') for a in soup.find_all('a')]
to_extract = ["facebook.com", "twitter.com", "mailto:"]
social_links = []
for link in links:
	for social in to_extract:
		if link and social in link:
			social_links.append(link)
print(social_links)
_# ['mailto:****@webscraper.io',_
_# 'https://www.facebook.com/webscraperio/',_
_# 'https://twitter.com/webscraperio']_

Второй фрагмент кода немного сложнее, если вы не знакомы с регулярными выражениями. Вкратце, они будут пытаться найти совпадение с заданным шаблоном поиска.

В данном случае они будут пытаться найти некоторые символы (в основном буквы и цифры), за которыми следует символ [@], затем снова символы - домен - символ [точка] и, наконец, два до четырех символов - домены верхнего уровня Интернета или TLD. Они найдут, например, test@example.com.

Обратите внимание, что это регулярное выражение не является полным, потому что оно не будет соответствовать составным TLD, таким как co.uk.

Мы можем запустить это выражение на всем содержимом (HTML) или только на тексте. Мы используем HTML для полноты, хотя мы также дублируем электронную почту, так как она отображается в тексте и href.

emails = re.findall(
	r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
	str(soup))
print(emails) _# ['****@webscraper.io', '****@webscraper.io']_

Автоматический разбор таблиц

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

Используя Список самых продаваемых альбомов на Википедии в качестве примера, мы извлечем все значения в массив и pandas dataframe. Это простой пример, но вы должны обрабатывать все данные так, как будто они поступают из набора данных.

Мы начинаем с поиска таблицы и перебора всех строк ("tr"). Для каждой из них находим ячейки ("td" или "th"). Следующие строки удаляют примечания и сворачиваемый контент из таблиц Википедии, что не является обязательным. Затем добавляем очищенный текст ячейки в строку и строку в конечный вывод. Выводим результат, чтобы проверить, что все выглядит нормально.

table = soup.find("table", class_="sortable")
output = []
for row in table.findAll("tr"):
    new_row = []
    for cell in row.findAll(["td", "th"]):
        for sup in cell.findAll('sup'):
            sup.extract()
        for collapsible in cell.findAll(
                class_="mw-collapsible-content"):
            collapsible.extract()
        new_row.append(cell.get_text().strip())
    output.append(new_row)

print(output)
_# [_
_#     ['Исполнитель', 'Альбом', 'Год выпуска', ...],_
_#     ['Майкл Джексон', 'Thriller', '1982', ...]_
_# ]_

Другой способ - использовать pandas и импортировать HTML напрямую, как показано ниже. Он будет обрабатывать все за нас: первая строка будет соответствовать заголовкам, а остальные будут вставлены как содержимое с правильным типом. read_html возвращает массив, поэтому мы берем первый элемент, а затем удаляем столбец, который не содержит контента.

После преобразования в dataframe мы можем выполнять любые операции, например, сортировку по продажам, так как pandas преобразовал некоторые столбцы в числа. Или сложить все заявленные продажи. Здесь это не совсем полезно, но вы понимаете идею.

import pandas as pd

table_df = pd.read_html(str(table))[0]
table_df = table_df.drop('Ссылки', 1)
print(table_df.columns) _# ['Исполнитель', 'Альбом', 'Год выпуска' ..._
print(table_df.dtypes) _# ... Год выпуска int64 ..._
print(table_df['Заявленные продажи*'].sum()) _# 422_
print(table_df.loc[3])
_# Исполнитель			Pink Floyd_
_# Альбом				The Dark Side of the Moon_
_# Год выпуска			1973_
_# Жанр				Прогрессивный рок_
_# Всего сертифицированных копий...	24.4_
_# Заявленные продажи*		45_

Извлечение из метаданных вместо HTML

Как уже видели ранее, есть способы получить основные данные, не полагаясь на визуальное содержимое. Давайте рассмотрим пример с сериалом Ведьмак на Netflix. Мы попробуем получить информацию об актерах. Просто, верно? Одна строка кода справится.

actors = soup.find(class_="item-starring").find(
	class_="title-data-info-item-list")
print(actors.text.split(','))
_# ['Генри Кавилл', 'Аня Чалотра', 'Фрея Аллан']_

А что, если я скажу вам, что там четырнадцать актеров и актрис? Будете пытаться получить информацию о всех? Если хотите попробовать сами, не прокручивайте дальше. Я подожду.

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

Netflix включает сниппет Schema.org со списком актеров и актрис, а также множеством других данных. Как и в примере с YouTube, иногда это более удобный подход. Даты, например, обычно отображаются в "машинном" формате, что удобнее при парсинге.

import json 
 
ldJson = soup.find("script", type="application/ld+json") 
parsedJson = json.loads(ldJson.contents[0]) 
print([actor['name'] for actor in parsedJson['actors']]) 
_# [... 'Джоди Мэй', 'МайАнна Бьюринг', 'Джои Бэйти' ...]_

Иногда это практический подход, если мы не хотим рендерить JavaScript. Мы покажем пример, используя профиль Билли Айлиш в Instagram. Известно, что они блокируют запросы. После посещения нескольких страниц вы будете перенаправлены на страницу входа. Будьте осторожны при парсинге Instagram и используйте локальный HTML для тестирования.

Мы рассмотрим, как избежать этих блокировок или перенаправлений в будущем посте. Следите за обновлениями!

Обычным способом было бы искать по классу, в нашем случае "Y8-fY". Мы не рекомендуем использовать такие классы, так как они могут измениться. Они выглядят автоматически сгенерированными. Многие современные веб-сайты используют такой тип CSS, и он генерируется при каждом изменении, что означает, что мы не можем полагаться на них.

План Б: "header ul > li", верно? Это сработает. Но для этого нам понадобится рендеринг JavaScript, так как он отсутствует при первой загрузке. Как уже упоминалось ранее, мы должны стараться избегать этого.

Взгляните на исходный HTML: заголовок и описание включают количество подписчиков, подписок и публикаций. Это может быть проблемой, так как они представлены в виде строки, но мы можем справиться с этим. Если нам нужны только эти данные, нам не понадобится безголовый браузер. Отлично!

metaDescription = soup.find("meta", {'name': 'description'})
print(metaDescription['content'])
_# 87.9m Подписчиков, 0 Подписок, 493 Публикаций ..._

Скрытая информация о продукте в электронной коммерции

Комбинируя некоторые уже рассмотренные техники, мы стремимся извлечь информацию о продукте, которая не видна. Наш первый пример - это электронная коммерция Shopify, Spigen.

Сначала посмотрите сами, если хотите.

Подсказка: ищите бренд 🤐.

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

Вы нашли их? В этом случае они используют "itemprop" и включают Product и Offer из schema.org. Мы, вероятно, сможем определить, есть ли товар в наличии, посмотрев на форму или кнопку "Добавить в корзину". Но нет необходимости, мы можем положиться на itemprop="availability". Что касается бренда, то используется тот же фрагмент, что и для YouTube, но с изменением имени свойства на "brand".

brand = soup.find('meta', itemprop="brand")
print(brand['content']) _# Tesla_

Еще один пример Shopify: nomz. Мы хотим извлечь количество и средний рейтинг, доступные в HTML, но скрытые. Средний рейтинг скрыт от просмотра с помощью CSS.

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

Это простая задача, если вы изучите исходный код. Схема продукта будет первым, что вы увидите. Применяя те же знания, что и в примере с Netflix, получите первый блок "ld+json", разберите JSON, и весь контент будет доступен!

import json

ldJson = soup.find("script", type="application/ld+json")
parsedJson = json.loads(ldJson.contents[0])
print(parsedJson["aggregateRating"]["ratingValue"]) _# 4.9_
print(parsedJson["aggregateRating"]["reviewCount"]) _# 57_
print(parsedJson["weight"]) _# 0.492kg -> дополнительно, не видно в пользовательском интерфейсе_

Наконец, мы воспользуемся атрибутами данных, которые также распространены в электронной коммерции. При изучении Marucci Sports Wood Bats мы видим, что у каждого продукта есть несколько полезных данных. Цена в числовом формате, идентификатор, название продукта и категория. У нас есть все данные, которые нам могут понадобиться.

products = []
cards = soup.find_all(class_="card")
for card in cards:
	products.append({
		'id': card.get('data-entity-id'),
		'name': card.get('data-name'),
		'category': card.get('data-product-category'),
		'price': card.get('data-product-price')
	})
print(products)
_# [_
_#    {_
_#	"category": "Wood Bats, Wood Bats/Professional Cuts",_
_#	"id": "1945",_
_#	"name": "6 Bat USA Professional Cut Bundle",_
_#	"price": "579.99"_
_#    },_
_#    {_
_#	"category": "Wood Bats, Wood Bats/Pro Model",_
_#	"id": "1804",_
_#	"name": "M-71 Pro Model",_
_#	"price": "159.99"_
_#    },_
_#    ..._
_# ]_

Оставшиеся препятствия

Хорошо! Вы получили все данные с этой страницы. Теперь вам нужно воспроизвести их на второй и третьей страницах. Масштабирование важно. И также важно не быть заблокированным.

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

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

Заключение

Мы хотели бы предложить вам три урока:

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

Свяжитесь с нами, если у вас есть еще какие-либо хитрости парсинга веб-сайтов или есть сомнения в их применении.

Помните, мы рассмотрели парсинг, но есть еще многое другое: обход, избегание блокировки, преобразование и сохранение контента, масштабирование инфраструктуры и многое другое. Следите за обновлениями!

Не забудьте взглянуть на остальные публикации в этой серии.+ Масштабирование до распределенного обхода (4/4)+ Обход с нуля (3/4)+ Избегайте блокировки, будто ниндзя (2/4)

И если вам понравился контент, пожалуйста, поделитесь им. 👇


Опубликовано на https://www.zenrows.com