В этом туториале мы создадим надёжные веб-краулеры с использованием таких библиотек, как BeautifulSoup, изучим техники, позволяющие преодолевать реальные трудности при скрейпинге, а также представим рекомендации по крупномасштабному скрейпингу.
Вы получите навыки для скрейпинга сложных сайтов и решения проблем, которые касаются ограничений частоты запросов, блокировок и генерируемых при помощи JavaScript страниц.
Зачем использовать Python для веб-скрейпинга?
Python широко используется в веб-скрейпинге благодаря своим преимуществам:
- Простой синтаксис: интуитивно понятный синтаксис Python позволяет быстро писать код для скрейпинга.
- Встроенные библиотеки: Python имеет встроенные библиотеки и модули наподобие urllib и lxml, помогающие в скрейпинге.
- Качественные библиотеки скрейпинга: библиотеки наподобие Beautiful Soup и Scrapy упрощают скрейпинг любых масштабов.
- Универсальность: Python можно использовать для создания готовых конвейеров данных на основе скрейпинга.
- Совместимость: Python хорошо интегрируется с другими языками и имеет достаточную производительность.
В отличие от него, языки наподобие C++ требуют прикладывать больше усилий для выполнения простых задач скрейпинга. JavaScript-платформы наподобие Node.js могут быть слишком сложны для начинающих.
Простота, мощь и совместимость Python делают его идеальным языком для реализации задач сбора веб-данных. Его высококачественные библиотеки позволяют быстро приступить даже к крупномасштабному скрейпингу.
Лучшие библиотеки Python для веб-скрейпинга
Вот некоторые из самых популярных и надёжных библиотек Python:
BeautifulSoup
- Особенности: превосходный парсер HTML/XML, простой интерфейс веб-скрейпинга, гибкая навигация и поиск. Мы будем использовать эту библиотеку в нашем примере скрейпера.
- Сценарий использования: веб-скрейпинг мелкого или среднего масштаба.
- Документация BeautifulSoup.
Scrapy
- Особенности: быстрая и масштабируемая, middleware, функция распределённого скрейпинга.
- Сценарий использования: крупномасштабные сложные проекты веб-скрейпинга.
- Документация Scrapy.
Selenium
- Особенности: полная автоматизация браузера, работа с сайтами с большим объёмом javascript.
- Сценарий использования: сайты с динамическим контентом, загружаемым через JS.
- Документация Selenium.
lxml
- Особенности: очень быстрый парсер XML и HTML.
- Сценарий использования: сверхбыстрый парсинг данных XML/HTML.
- Документация lxml.
pyquery
- Особенности: синтаксис в стиле jQuery для доступа к HTML-элементам.
- Сценарий использования: делает код скрейпинга чище и читабельнее.
- Документация pyquery.
Необходимые требования
Чтобы повторять примеры кода из статьи, вам понадобятся:
Виртуальное окружение (рекомендуется)
Хоть это и не обязательно, мы крайне рекомендуем создать для проекта виртуальное окружение:
python -m venv my_web_scraping_env
Библиотеки
Мы будем в основном использовать библиотеки Requests, BeautifulSoup и OS:
pip install requests beautifulsoup4
Эта команда загрузит библиотеки из PyPI и установит их локально.
Установив всё необходимое, мы готовы к работе! Давайте приступим к скрейпингу.
Выберем целевой веб-сайт
В целях демонстрации мы будем скрейпить страницу Википедии List of dog breeds (список пород собак) и извлекать информацию о различных породах.
Я выбрал эту страницу из следующих соображений:
- Хорошо структурированный HTML, упрощающий скрейпинг.
- Удобная табличная структура, где в каждой строке находится по одной породе.
- Содержит множество полей данных по каждой из пород, включая названия, группы пород, альтернативные названия и изображения.
- Изображения позволят нам продемонстрировать и функцию скрейпинга двоичных файлов.
Мы будем работать вот с этой страницей:
Есть и другие замечательные страницы, походящие для веб-скрейпинга:
- Страницы категорий Википедии наподобие Lists of films.
- Списки товаров электронных маркетплейсов наподобие книг Amazon.
- Списки недвижимости, например, арендуемые дома на Zillow.
Рассмотренные в статье концепции будут применимы к любому сайту.
Пишем код скрейпинга
Давайте подробно изучим полный код, чтобы понять, как систематично скрейпить данные со страницы о породах собак.
# Полный код
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
Так как мы получили валидный 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, показанные здесь:
- Selenium запускает реальный браузер Chrome для загрузки JavaScript
- Находит элементы, доступные только после исполнения JS
- Может взаимодействовать со страницей при помощи нажатий, ввода текста и так далее, вызывая таким образом срабатывание событий JavaScript
- Имитирует реального пользователя, просматривающего динамически генерируемый контент
Всё вместе это позволяет работать со сложными сайтами, преимущественно использующими 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 для имитации работы реальных браузеров.
Вот пример кода для выбора случайного десктопного 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 для имитации атрибутов настоящего браузера
Благодаря эффективной ротации user agent и расширению списка строк скрейперам дольше удаётся оставаться незамеченными, прежде чем администраторы сайта спрофилируют и заблокируют их.
▍ Фингерпринтинг браузеров
Наряду с простыми проверками user agent веб-сайты применяют для выявления ботов сложные методики фингерпринтинга браузеров.
Для этого выполняется профилирование атрибутов браузера — сбор информации о размере экрана устройства, установленных шрифтах, браузерных плагинах и так далее. Всё вместе это называется фингерпринтами («отпечатками пальцев») браузера или его характерными чертами. У стандартных ботов и ПО автоматизации эти свойства остаются по большей мере постоянными, стабильными и уникальными.
Динамические веб-сайты отслеживают фингерпринты получающих доступ к ним скрейперов. Распознавая признаки известных краулеров, они могут блокировать их даже при постоянной ротации user agent.
Минимизация рисков обнаружения
Вот некоторые из способов минимизации выявления признаков скрейперов:
- Использование Selenium для автоматизации стандартного десктопного браузера наподобие Chrome или Firefox вместо собственных бот-агентов
- Динамическая генерация таких рандомизированных атрибутов, как размер окна просмотра, разрешения экрана, списков шрифтов в пределах разнообразия, свойственного настоящим браузерам
- Использование ротации прокси и резидентных прокси IP-адресов для защиты от слежения за конкретными атрибутами IP-адресов
- Ограничение количества параллельных запросов от одного прокси к сайту, чтобы объём трафика выглядел естественно
По сути, имитируя естественную случайность и вариативность настоящих пользователей браузеров, можно избежать простого фингерпринтинга скрейпера сайтами, притворившись обычным браузером.
Вот пример кода для динамического изменения атрибутов браузера с целью избежать фингерпринтинга:
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
Цели скрейпинга часто состоят из сложных 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
. - Проверяйте доступность API перед скрейпингом сайтов без разрешения.
- Выполняйте скрейпинг ответственно и в умеренных объёмах.
Следование этим практикам гарантирует надёжный, устойчивый и ответственный скрейпинг.
Заключение
В этом руководстве мы подробно рассмотрели веб-скрейпинг при помощи Python. Мы рассказали:
- Почему Python и библиотеки наподобие BeautifulSoup идеально подходят для скрейпинга большинства сайтов.
- О распространённых паттернах скрейпинга, например, об отправке запросов, парсинге ответов, обработке динамического контента при помощи Selenium.
- О лучших практиках мимикрии, обхода блокировок, учёта задержек скрейпинга и автоматического троттлинга.
- Как создавать надёжные масштабируемые скрейперы, готовые для продакшена.
Изучив базовые парадигмы скрейпинга, правильно структурировав код и применив техники оптимизации, вы сами сможете извлекать точные веб-данные на Python!
Эти примеры отлично подходят для обучения, однако при скрейпинге сайтов уровня продакшена могут возникнуть такие сложности, как CAPTCHA, блокировки по IP-адресам и распознавание ботов. В их решении может помочь ротация прокси и автоматизированное решение CAPTCHA.
Автор:
ru_vds