- PVSM.RU - https://www.pvsm.ru -

Вы получите навыки для скрейпинга сложных сайтов и решения проблем, которые касаются ограничений частоты запросов, блокировок и генерируемых при помощи JavaScript страниц.
Python широко используется в веб-скрейпинге благодаря своим преимуществам:
В отличие от него, языки наподобие C++ требуют прикладывать больше усилий для выполнения простых задач скрейпинга. JavaScript-платформы наподобие Node.js могут быть слишком сложны для начинающих.
Простота, мощь и совместимость Python делают его идеальным языком для реализации задач сбора веб-данных. Его высококачественные библиотеки позволяют быстро приступить даже к крупномасштабному скрейпингу.
Вот некоторые из самых популярных и надёжных библиотек Python:
BeautifulSoup
Scrapy
Selenium
lxml
pyquery
Чтобы повторять примеры кода из статьи, вам понадобятся:
Виртуальное окружение (рекомендуется)
Хоть это и не обязательно, мы крайне рекомендуем создать для проекта виртуальное окружение:
python -m venv my_web_scraping_env
Библиотеки
Мы будем в основном использовать библиотеки Requests, BeautifulSoup и OS:
pip install requests beautifulsoup4
Эта команда загрузит библиотеки из PyPI и установит их локально.
Установив всё необходимое, мы готовы к работе! Давайте приступим к скрейпингу.
В целях демонстрации мы будем скрейпить страницу Википедии List of dog breeds [6] (список пород собак) и извлекать информацию о различных породах.
Я выбрал эту страницу из следующих соображений:
Мы будем работать вот с этой страницей:

Есть и другие замечательные страницы, походящие для веб-скрейпинга:
Рассмотренные в статье концепции будут применимы к любому сайту.
Давайте подробно изучим полный код, чтобы понять, как систематично скрейпить данные со страницы о породах собак.
# Полный код
import os
import requests
from bs4 import BeautifulSoup
url = '<https://commons.wikimedia.org/wiki/List_of_dog_breeds>'
# Заголовки, чтобы замаскироваться под браузер
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
# Скачиваем HTML страницы при помощи requests
response = requests.get(url, headers=headers)
# Проверяем валидность полученного ответа
if response.status_code == 200:
# Парсим HTML при помощи Beautiful Soup
soup = BeautifulSoup(response.text, 'html.parser')
# CSS-селектор для основных таблиц
table = soup.find('table', {'class': 'wikitable sortable'})
# Инициализируем списки данных для хранения полученной скрейпингом информации
names = []
groups = []
local_names = []
photographs = []
# Создаём папку для хранения изображений
os.makedirs('dog_images', exist_ok=True)
# Обходим строки в цикле, пропуская заголовок
for row in table.find_all('tr')[1:]:
# Извлекаем данные каждого столбца при помощи CSS-селекторов
columns = row.find_all(['td', 'th'])
name = columns[0].find('a').text.strip()
group = columns[1].text.strip()
# Извлекаем локальное имя, если оно существует
span_tag = columns[2].find('span')
local_name = span_tag.text.strip() if span_tag else ''
# Извлекаем url фотографии, если она существует
img_tag = columns[3].find('img')
photograph = img_tag['src'] if img_tag else ''
# Скачиваем + сохраняем изображение, если url существует
if photograph:
response = requests.get(photograph)
if response.status_code == 200:
image_filename = os.path.join('dog_images', f'{name}.jpg')
with open(image_filename, 'wb') as img_file:
img_file.write(response.content)
names.append(name)
groups.append(group)
local_names.append(local_name)
photographs.append(photograph)
print(names)
print(groups)
print(local_names)
print(photographs)
import включают в себя стандартные библиотеки Python, предоставляющие функциональность HTTP-запросов (requests), функцию парсинга (BeautifulSoup) и доступ к файловой системе (os); всё это мы будем использовать.
Библиотека requests позволяет нам создавать HTTP-запросы к веб-странице и проверять перед парсингом, валиден ли ответ.
Затем BeautifulSoup позволяет нам парсить полностью содержимое HTML и выделить основную таблицу данных при помощи CSS-селекторов. Наконец, os предоставляет доступ к файловой системе для локального сохранения изображений.
Всё это вместе создаёт очень удобный инструмент для скрейпинга!
Сначала мы собираем целевой URL и инициализируем Session запросов, которая позволяет многократно и эффективно использовать подключение при выполнении множества HTTP-запросов к одному домену:
url = '<https://commons.wikimedia.org/wiki/List_of_dog_breeds>'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
response = requests.get(url, headers=headers)
Также мы создаём специальный HTTP-заголовок User-Agent, чтобы замаскироваться под браузер Chrome. Это помогает избегать блокировки со стороны серверов, пытающихся бороться со скрейпингом.
Получив ответ, мы можем проверить код состояния, чтобы убедиться в получении настоящего HTML-документа:
if response.status_code == 200:
# Успешно!
print(response.text)
В случае возникновения ошибок (например, 404 или 500) мы не переходим к скрейпингу и обрабатываем сбой.
Так как мы получили валидный HTML-ответ, можно спарсить текстовое содержимое при помощи Beautiful Soup:
soup = BeautifulSoup(response.text, 'html.parser')
BeautifulSoup принимает сырой текст HTML и опциональный парсер, например, lxml или встроенный html.parser, предоставляет простые методы и Pythonic-идиомы для навигации, поиска и модификации дерева парсинга.
Beautiful Soup превращает запутанный HTML в дерево парсинга, отражающее DOM-структуру тэгов, атрибутов и текста. Мы можем использовать CSS-селекторы и методы обхода для быстрого выделения нужных нам данных из этого дерева.
Один из самых волшебных аспектов веб-скрейпинга при помощи Python-библиотеки BeautifulSoup — это применение CSS-селекторов для извлечения нужного контента из HTML-страниц.
Селекторы позволяют нам визуально выбирать тэги, содержащие данные, которые мы хотим скрейпить. BeautifulSoup существенно упрощает выбор элементов.
Допустим, нам нужно извлечь названия книг из этого фрагмента:
<div class="book-listing">
<img align="center" src="/covers/harry-potter.jpg">
<span class="title">Harry Potter and the Goblet of Fire</span>
<span class="rating">9.1</span>
</div>
<div class="book-listing">
<img align="center" src="/covers/lord-of-the-rings.jpg">
<span class="title">The Fellowship of the Ring</span>
<span class="rating">9.3</span>
</div>
Можно напрямую выбрать span с классом title при помощи CSS-селектора:
soup.select("div.book-listing > span.title")
Этот код приказывает «найди все тэги span с классом title, являющиеся прямыми дочерними элементами любого тэга div с CSS-классом book-listing».
И вуаля, мы выбрали только нужные нам названия:
[<span class="title">Harry Potter and the Goblet of Fire</span>,
<span class="title">The Fellowship of the Ring</span>]
Можно прицепить .text, чтобы извлекать только читаемый текст, находящийся внутри тэгов:
[Harry Potter and the Goblet of Fire, The Fellowship of the Ring]
Селекторы позволяют невероятно точно извлекать данные, используя внутреннюю иерархию окружающих их структурированных HTML-тэгов.
Вот ещё несколько примеров селекторов:
# Выбор атрибута id
soup.select("#book-title")
# Сопоставление равенства атрибута
soup.select('a[href="/login"]')
# Частичное сопоставление атрибута
soup.select('span[class^="title"]')
# Выбор непосредственного потомка
soup.select("ul > li")
Как видите, освоив различные типы селекторов и по необходимости их сочетая, можно обрести невероятную мощь в точном поиске и извлечении данных из любого HTML-документа практически без необходимости гадать. Но давайте вернёмся к нашей задаче…

Изучив сырой HTML, мы заметим, что основные данные о породах содержит тэг table с CSS-классом wikitable sortable.
Мы можем легко выбрать его следующим образом:
table = soup.find('table', {'class': 'wikitable sortable'})
Этот код ищет дерево парсинга для любого тэга table, содержащего атрибут class, совпадающий с wikitable sortable. Beautiful soup сильно упрощает выбор при помощи CSS-селекторов!
Выделив таблицу, мы обходим каждую строку tr после строки заголовка, чтобы извлекать данные о каждой из пород:
for row in table.find_all('tr')[1:]:
columns = row.find_all(['td', 'th'])
name = columns[0].find('a').text.strip()
group = columns[1].text.strip()
Здесь .find_all() помогает найти все дочерние тэги строки для всех элементов td или th, обозначающих ячейки таблицы. Мы выбираем их в список columns.
При помощи индексов из этого списка столбцов мы можем извлечь только сами данные в каждой ячейке:
name = columns[0].find('a').text.strip()
Здесь мы берём тэг якоря a внутри первой ячейки таблицы, получаем свойство .text, чтобы извлечь сырое содержимое строк, и подключаем .strip(), чтобы удалить пробелы. Beautiful Soup удобным образом объединяет такие операции в цепочки.
Аналогично для ячеек, содержащих только текст:
group = columns[1].text.strip()
Мы получаем свойство .text непосредственно от элемента ячейки таблицы.
Мощь CSS-селекторов заключается в выделении конкретных тэгов, идентификаторов, классов или атрибутов; это превращает извлечение данных при помощи Beautiful Soup в очень точный и лёгкий процесс.
После скрейпинга текстовых данных наподобие названий, групп и так далее в каждой из строк мы проверяем последнюю ячейку на наличие ссылки на изображение:
img_tag = columns[3].find('img')
photograph = img_tag['src'] if img_tag else ''
Этот код пытается обнаружить и получить атрибут src для любого существующего тэга изображения.
Если этот url существует, мы можем скачать и сохранить изображения:
if photograph:
response = requests.get(photograph)
image_filename = os.path.join('dog_images', f'{name}.jpg')
with open(file_path, 'wb') as img_file:
img_file.write(response.content)
Мы снова пользуемся библиотекой requests, чтобы сделать ещё один запрос GET, на этот раз для скачивания двоичного содержимого изображения и его локального сохранения при помощи встроенных возможностей работы с файлами. Очень удобно!
Вот и всё! Использовав requests и BeautifulSoup вместе с интуитивно понятной стандартной библиотекой Python, мы смогли создать готовый веб-скрейпер для извлечения сложных данных!
Библиотеки requests и BeautifulSoup — это, конечно, самая популярная комбинация, но существуют и альтернативы, которые стоит учитывать:
Scrapy
Опенсорсный модульный фреймворк, предназначенный для крупномасштабного скрейпинга. Он автоматически обрабатывает троттлинг, куки и ротацию прокси. Рекомендован для сложных задач.
Selenium
Выполняет автоматизацию браузера, управляя Chrome, Firefox и так далее. Позволяет скрейпить динамический контент, который рендерится через JavaScript. Сложнее в настройке.
pyppeteer
Безголовая автоматизация браузеров, похожая на Selenium и управляемая кодом на Python. Подходит для веб-сайтов с javascript-рендерингом.
pyquery
Обеспечивает выбор элементов в стиле jQuery. Код скрейпинга выглядит очень чистым благодаря сцеплённому синтаксису, напоминающему jQuery.
lxml
Очень быстрый парсер XML/HTML. Отлично подходит для случаев, когда очень важна производительность сырого парсинга.
Несмотря на простоту базового веб-скрейпинга, при создании надёжных масштабируемых краулеров для продакшена возникают трудности:
Многие веб-сайты активно используют JavaScript для динамического рендеринга контента, поэтому статичный скрейпинг на них выполнить не удаётся.
Решения: использовать инструменты автоматизации браузеров наподобие Selenium или решения для конкретных скрейперов, например, интеграцию splash для Scrapy.
Вот простой пример по работе с динамическим контентом при помощи автоматизации браузеров Selenium:
from selenium import webdriver
from selenium.webdriver.common.by import By
# Инициализация chrome webdriver
driver = webdriver.Chrome()
# Загрузка страницы
driver.get("<https://example.com>")
# Ждём, пока загрузится заголовок при динамическом исполнении JS
driver.implicitly_wait(10)
# Selenium может извлечь динамически загруженные элементы
print(driver.title)
# Selenium позволяет нажимать на кнопки, запуская события JS
driver.find_element(By.ID, "dynamicBtn").click()
# Ввод тоже можно обрабатывать
search = driver.find_element(By.NAME, 'search')
search.send_keys('Automate using Selenium')
search.submit()
# Уничтожаем браузер после завершения
driver.quit()
Основные возможности Selenium, показанные здесь:
Всё вместе это позволяет работать со сложными сайтами, преимущественно использующими JavaScript для динамического контента. Selenium предоставляет полный программный контроль для непосредственной автоматизации браузеров, обеспечивая корректный скрейпинг.
Веб-сайты часто блокируют скрейперы при помощи заблокированных интервалов IP-адресов или препятствуя характерной активности ботов при помощи эвристик.
Решения: снижение частоты запросов, точная имитация браузеров, ротация user agent и прокси.
Ограничение частоты
Серверы борются с чрезмерными нагрузками, ограничивая количество обрабатываемых запросов за единицу времени. Превышение этих пределов приводит к временным банам или к отклонению запросов.
Решения: учитывать задержки между краулингом, использовать прокси и распределять запросы.
Вот пример кода работы с ограничениями частоты при скрейпинге:
У многих веб-сайтов есть защитные механизмы, временно блокирующие скрейперы, когда от одного IP-адреса поступает слишком много частых запросов.
Мы можем бороться с блокировками при превышении частоты, добавив в код троттлинг, прокси и произвольные задержки.
import time
import random
import requests
from urllib.request import ProxyHandler, build_opener
# Список бесплатных публичных прокси
PROXIES = ["104.236.141.243:8080", "104.131.178.157:8085"]
# Пауза в 5-15 секунд между запросами
def get_request():
time.sleep(random.randint(5, 15))
proxy = random.choice(PROXIES)
opener = build_opener(ProxyHandler({'https': proxy}))
resp = opener.open("<https://example.com>")
return resp
for i in range(50):
response = get_request()
print("Request Success")
В этом коде каждый запрос сначала ожидает в течение случайного интервала времени. Это предотвращает непрерывные быстрые запросы.
Кроме того, мы перенаправляем каждый запрос через случайно выбранный прокси-сервер, обеспечивая ротацию IP-адресов.
В целом снижение общей скорости краулинга и распределение запросов по различным IP-адресам прокси позволяет не выйти за пределы накладываемых сайтом ограничений частоты.
Можно ещё больше повысить надёжность краулера, добавив автоматическое распознавание предупреждений об ограничениях частоты в ответах и реагируя на них соответствующим образом.
Веб-сайты пытаются распознавать и блокировать ботов, отслеживая характерные строки user agent.
Чтобы избежать блокировок, следует выполнять случайную ротацию множества хорошо замаскированных user agent для имитации работы реальных браузеров.
Вот пример кода для выбора случайного десктопного user agent из заранее созданного списка при помощи Python-библиотеки random перед выполнением каждого запроса:
import requests
import random
# Список десктопных user agent
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991"
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7"
]
# Выбор случайной строки user agent
user_agent = random.choice(user_agents)
# Указываем user agent в заголовках запроса перед выполнением запроса
headers = {"User-Agent": user_agent}
response = requests.get(url, headers=headers)
Изменяя user agent между запросами, вы усложните веб-сайтам профилирование трафика как приходящего от автоматизированного бота со статичным user agent. Это позволяет скрейперу оставаться неопознанным и предотвращает блокировки.
Дополнительные улучшения:
Благодаря эффективной ротации user agent и расширению списка строк скрейперам дольше удаётся оставаться незамеченными, прежде чем администраторы сайта спрофилируют и заблокируют их.
Наряду с простыми проверками user agent веб-сайты применяют для выявления ботов сложные методики фингерпринтинга браузеров.
Для этого выполняется профилирование атрибутов браузера — сбор информации о размере экрана устройства, установленных шрифтах, браузерных плагинах и так далее. Всё вместе это называется фингерпринтами («отпечатками пальцев») браузера или его характерными чертами. У стандартных ботов и ПО автоматизации эти свойства остаются по большей мере постоянными, стабильными и уникальными.
Динамические веб-сайты отслеживают фингерпринты получающих доступ к ним скрейперов. Распознавая признаки известных краулеров, они могут блокировать их даже при постоянной ротации user agent.
Минимизация рисков обнаружения
Вот некоторые из способов минимизации выявления признаков скрейперов:
По сути, имитируя естественную случайность и вариативность настоящих пользователей браузеров, можно избежать простого фингерпринтинга скрейпера сайтами, притворившись обычным браузером.
Вот пример кода для динамического изменения атрибутов браузера с целью избежать фингерпринтинга:
from selenium import webdriver
import random
# Список распространённых разрешений экрана
screen_res = [(1366, 768), (1920, 1080), (1024, 768)]
# Список распространённых семейств шрифтов
font_families = ["Arial", "Times New Roman", "Verdana"]
#Выбор случайного разрешения
width, height = random.choice(screen_res)
#Создание опций chrome
opts = webdriver.ChromeOptions()
# Установка случайного разрешения экрана
opts.add_argument(f"--window-size={width},{height}")
# Установка случайного user agent
opts.add_argument("--user-agent=Mozilla/5.0...")
# Установка случайного списка шрифтов
random_fonts = random.choices(font_families, k=2)
opts.add_argument(f'--font-list="{random_fonts[0]};{random_fonts[1]}"')
# Инициализация драйвера с опциями
driver = webdriver.Chrome(options=opts)
# Доступ к веб-странице
driver.get(target_url)
# Веб-страница видит, что каждый запрос скрейпера поступает
# от уникального непредсказуемого профиля браузера
Здесь мы случайным образом конфигурируем в каждом запросе управляемый через Selenium инстанс Chrome различными разрешениями экрана, user agent и наборами шрифтов.
А вот как это сделать при помощи Python Requests:
import requests
import random
# Профили устройств
desktop_config = {
'user-agent': 'Mozilla/5.0...',
'accept-language': ['en-US,en', 'en-GB,en'],
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'accept-encoding': 'gzip, deflate, br',
'upgrade-insecure-requests': '1',
'sec-fetch-site': 'none',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
'cache-control': 'max-age=0'
}
mobile_config = {
'user-agent': 'Mozilla/5.0... Mobile',
'accept-language': ['en-US,en', 'en-GB,en'],
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'x-requested-with': 'mark.via.gp',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
'referer': '<https://www.example.com/>',
'accept-encoding': 'gzip, deflate, br',
'cache-control': 'max-age=0'
}
device_profiles = [desktop_config, mobile_config]
def build_headers():
profile = random.choice(device_profiles)
headers = {
'User-Agent': random.choice(profile['user-agent']),
'Accept-Language': random.choice(profile['accept-language']),
# Другие заголовки
...
}
return headers
Теперь вместо жёстко прописанных значений скрейпер случайным образом выбирает один из правдоподобных профилей конфигурации, в том числе из множества идентифицирующих заголовков запроса, обеспечивая реалистичные изменения, необходимые для противодействия отслеживанию фингерпринтов.
Цели скрейпинга часто состоят из сложных HTML-структур, обфусцированных тэгов и продвинутой логики упаковки кода на стороне клиента, что ломает парсеры.
Решения: внимательное изучение отрендеренного источника, использование надёжных парсеров наподобие lxml и совершенствование селекторов.
Вот несколько примеров плохого поведения целей для скрейпинга и методик работы с ними:
Неправильная вложенность
В HTML часто бывают некорректно вложенные тэги:
<b><font color="#000">Latest News <p>Impact of oil prices fall...</font></b></p>
Решение: использование парсера наподобие lxml, надёжнее обрабатывающего плохую вложенность и нечёткие тэги.
Поломанная разметка
Тэги могут быть не закрыты:
<div>
<span class="title">Python Web Scraping <span>
Lorem ipsum...
</div>
Решение: указывать в явном виде закрытие тэгов при парсинге:
title = soup.find("span", class_="title").text
Нестандартные элементы
Могут существовать нераспознаваемые специализированные тэги:
<album>
<cisco:song>Believer</cisco:song>
</album>
Решение: искать стандартные тэги в пространстве имён:
song = soup.find("cisco:song").text
Нетекстовое содержимое
Таблицы и изображения, встроенные между тэгов:
<p>
Trending Now
<table>...</table>
</p>
Решение: выбирать конкретно дочерние тэги:
paras = soup.select("p > text()")
Этот код выбирает в качестве дочерних только текстовые узлы, игнорируя другие элементы, находящиеся внутри тэга <p>.
Как видите, вольное применение селекторов наряду с использованием мощных парсеров даёт нам инструменты для работы даже с плохо спроектированным HTML и для надёжного извлечения нужных данных.
robots.txt.Следование этим практикам гарантирует надёжный, устойчивый и ответственный скрейпинг.
В этом руководстве мы подробно рассмотрели веб-скрейпинг при помощи Python. Мы рассказали:
Изучив базовые парадигмы скрейпинга, правильно структурировав код и применив техники оптимизации, вы сами сможете извлекать точные веб-данные на Python!
Эти примеры отлично подходят для обучения, однако при скрейпинге сайтов уровня продакшена могут возникнуть такие сложности, как CAPTCHA, блокировки по IP-адресам и распознавание ботов. В их решении может помочь ротация прокси и автоматизированное решение CAPTCHA.
Автор:
ru_vds
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/beautiful-soup/390285
Ссылки в тексте:
[1] Документация BeautifulSoup.: https://www.crummy.com/software/BeautifulSoup/bs4/doc/
[2] Документация Scrapy.: https://scrapy.org/
[3] Документация Selenium.: https://www.selenium.dev/documentation/overview/
[4] Документация lxml.: https://lxml.de/
[5] Документация pyquery.: https://pythonhosted.org/pyquery/
[6] List of dog breeds: https://commons.wikimedia.org/wiki/List_of_dog_breeds
[7] Lists of films: https://en.wikipedia.org/wiki/Lists_of_films
[8] книг Amazon: https://www.amazon.com/books-used-books-textbooks/b?ie=UTF8&node=283155
[9] арендуемые дома на Zillow: https://www.zillow.com/homes/for_rent/
[10] Источник: https://habr.com/ru/companies/ruvds/articles/796885/?utm_source=habrahabr&utm_medium=rss&utm_campaign=796885
Нажмите здесь для печати.