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

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

Предпосылки

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

pip install requests beautifulsoup4 pandas

Получить HTML-код из URL-адреса легко с помощью библиотеки запросов. Затем передайте контент в 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

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

Изучите перед кодированием

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

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

Остерегайтесь делать это с помощью Chrome DevTools или аналогичных средств. Вы увидите контент, как только 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.

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

Это заставляет нас использовать автономный браузер, который может выполнять 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. Он найдет, например, [email protected].

Обратите внимание, что это регулярное выражение не является полным, поскольку оно не соответствует составным 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. Это простой пример, но вы должны управлять всеми данными, как если бы они были получены из набора данных.

Мы начинаем с поиска таблицы и перебора всех строк («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)
# [
#     ['Artist', 'Album', 'Released', ...],
#     ['Michael Jackson', 'Thriller', '1982', ...]
# ]

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

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

import pandas as pd

table_df = pd.read_html(str(table))[0]
table_df = table_df.drop('Ref(s)', 1)
print(table_df.columns) # ['Artist', 'Album', 'Released' ...
print(table_df.dtypes) # ... Released int64 ...
print(table_df['Claimed sales*'].sum()) # 422
print(table_df.loc[3])
# Artist			Pink Floyd
# Album				The Dark Side of the Moon
# Released			1973
# Genre				Progressive rock
# Total certified copies...	24.4
# Claimed sales*		45

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

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

actors = soup.find(class_="item-starring").find(
	class_="title-data-info-item-list")
print(actors.text.split(','))
# ['Henry Cavill', 'Anya Chalotra', 'Freya Allan']

Что, если бы я сказал вам, что есть четырнадцать актеров и актрис? Вы попытаетесь получить их все? Не прокручивайте дальше, если хотите попробовать сами. Я подожду.

Пока ничего? Помните, есть больше, чем кажется на первый взгляд. Вы знаете троих из них; ищите их в исходном 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']]) 
# [... 'Jodhi May', 'MyAnna Buring', 'Joey Batey' ...]

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

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

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

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

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

metaDescription = soup.find("meta", {'name': 'description'})
print(metaDescription['content'])
# 87.9m Followers, 0 Following, 493 Posts ...

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

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

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

Подсказка: ищите марку 🤐.

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

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

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 -> extra, not visible in UI

И последнее, но не менее важное: мы воспользуемся атрибутами данных, которые также распространены в электронной коммерции. Изучая 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 или базы данных, что вам нужно. Вложенные поля нелегко экспортировать в любой из этих форматов.

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

Вывод

Мы хотим, чтобы вы прошли три урока:

  1. Селекторы CSS хороши, но есть и другие варианты.
  2. Некоторый контент скрыт или отсутствует, но доступен через метаданные.
  3. Старайтесь избегать загрузки Javascript и безголовых браузеров, чтобы повысить производительность.

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

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

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

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

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

Первоначально опубликовано на https://www.zenrows.com