Веб-скрапинг преобразует публичные веб-страницы в структурированные данные для анализа, и от этапа разбора зависит чистота этих данных. У Python есть несколько библиотек для разбора, но Parsel выделяется тем, что она небольшая, быстрая и построена вокруг двух языков селекторов, которые большинство скраперов уже знает: XPath и CSS. Это тот же движок, что работает в Scrapy, и он так же хорошо работает самостоятельно, когда у вас есть сырой HTML и вы хотите извлечь из него поля в нескольких читаемых строках.
Это руководство представляет собой выполняемое пошаговое руководство. Вы устанавливаете Parsel, загружаете отрендеренную страницу через Crawling API, загружаете HTML в Selector и извлекаете данные с помощью XPath и CSS, используя .get() и .getall(). Затем вы перебираете список элементов, считываете текст и атрибуты, очищаете значения и экспортируете результат в JSON и CSV. Целевым примером служит books.toscrape.com, публичная песочница, специально созданная для практики скрапинга, поэтому вы можете запустить каждый фрагмент кода от начала до конца, не обращаясь к реальному рабочему сайту.
Что вы создадите
Небольшой Python-скрипт, который загружает страницу каталога, создаёт Parsel Selector из возвращённого HTML, перебирает карточки товаров и извлекает структурированную запись на каждый элемент. Из карточки каждой книги мы извлекаем следующие поля:
- Название название книги, считанное из атрибута ссылки.
- Цена указанная цена, очищенная в число.
- Наличие текст о наличии на складе, показанный на карточке.
- Рейтинг звёздный рейтинг, считанный из CSS-класса.
- Ссылка абсолютный URL страницы с подробной информацией о книге.
Почему Parsel для разбора на Python
Parsel, автономная библиотека для работы с селекторами. Вы передаёте ей строку HTML, она строит дерево, и вы запрашиваете это дерево с помощью выражений XPath или CSS. Она занимает полезное промежуточное положение: легче полноценного фреймворка, такого как Scrapy, и более ориентирована на селекторы, чем BeautifulSoup, которая делает ставку на цепочку методов Python вместо строк-селекторов. Причины, по которым она занимает своё место в наборе инструментов для скрапинга, просты:
- Два языка селекторов. Используйте XPath, когда нужно навигировать по структуре или искать совпадения по тексту, и CSS, когда короткий селектор по классу или тегу читается яснее. Parsel поддерживает оба на одном объекте.
- Небольшая и быстрая. Она построена на lxml, поэтому разбор больших документов остаётся быстрым, а настройки почти нет помимо импорта одного класса.
-
Чистый синтаксис.
.get()возвращает первое совпадение,.getall()возвращает все совпадения, а цепочки селекторов делают код извлечения коротким и простым в обслуживании.
Для более детального ознакомления с самими языками селекторов статья об XPath и CSS-селекторах подробно рассматривает синтаксис. Здесь мы сосредотачиваемся на применении их в работе с Parsel.
Зачем загружать через Crawling API
Parsel разбирает HTML; он не загружает страницы. Вам всё равно нужно что-то для предварительного получения разметки, и именно этот этап загрузки создаёт большинству скраперов проблемы. Обычный HTTP-запрос прекрасно работает на простой статической странице, но многие современные сайты рендерят своё содержимое с помощью JavaScript, поэтому в сыром ответе отсутствуют реальные данные. Другие отслеживают автоматический трафик и ограничивают скорость или блокируют запросы, не похожие на запросы от реального браузера.
Загрузка через Crawling API решает обе проблемы за один вызов. Вы отправляете ему URL, он при необходимости рендерит страницу, маршрутизирует запрос через надёжный ротирующий IP и возвращает готовый HTML, который вы передаёте непосредственно в Parsel Selector. Это разделяет проблемы загрузки (рендеринг, ротация, блокировка) и проблемы разбора (селекторы, поля), что и делает скрапер легко обслуживаемым.
Предварительные требования
Базовый Python. Вы должны уметь запускать скрипты и устанавливать пакеты с помощью pip. Предварительный опыт работы с Parsel не требуется; это руководство знакомит с API по ходу изложения.
Python 3.8 или выше. Проверьте версию с помощью python --version. Если Python не установлен, скачайте его с python.org и убедитесь, что он добавлен в PATH.
Аккаунт Crawlbase и токен. Зарегистрируйтесь, откройте панель управления и скопируйте токен запроса. Crawlbase включает до 20 000 бесплатных запросов для начала, что более чем достаточно для прохождения этого руководства. Обращайтесь с токеном как с паролем и не добавляйте его в систему контроля версий.
Настройка проекта
Создайте виртуальную среду, чтобы изолировать зависимости проекта, затем установите две библиотеки, необходимые скрипту.
python --version python -m venv parsel_env source parsel_env/bin/activate pip install parsel crawlbase
В Windows активируйте среду с помощью parsel_env\Scripts\activate вместо строки с source. parsel выполняет извлечение, а crawlbase, официальный клиент, который загружает отрендеренные страницы. Модули json и csv входят в стандартную библиотеку, поэтому для шага экспорта ничего больше устанавливать не нужно.
Шаг 1: Загрузить страницу и создать Selector
Начните с загрузки одной страницы каталога через Crawling API и загрузки её HTML в Parsel Selector. Импортируйте CrawlingAPI, инициализируйте его с вашим токеном, запросите URL и проверьте заголовок cb_status (legacy pc_status) перед разбором, чтобы сбои были заметны, а не происходили молча.
from crawlbase import CrawlingAPI from parsel import Selector api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) def fetch_html(page_url): response = api.get(page_url) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None if __name__ == "__main__": url = "https://books.toscrape.com/catalogue/page-1.html" html = fetch_html(url) if html: selector = Selector(text=html) print(selector.xpath("//title/text()").get())
Selector(text=html), это точка входа для всего, что следует дальше: он разбирает строку один раз и даёт вам объект, который вы запрашиваете с помощью .xpath() и .css(). Последняя строка считывает заголовок страницы с помощью выражения XPath, где /text() выбирает текстовый узел, а .get() возвращает первое совпадение как строку. Запустите файл и вы должны увидеть напечатанный заголовок страницы каталога, что подтверждает работоспособность как загрузки, так и разбора, прежде чем вы напишете хоть один селектор полей.
Шаг fetch_html выше, это то, что Parsel не может сделать самостоятельно, и на реальном целевом сайте именно здесь рендеринг и блокировки становятся сложными. Crawling API принимает ваш токен, при необходимости рендерит JavaScript-страницы, на стороне сервера ротирует через резидентные IP и возвращает готовый HTML, чтобы вы могли передать его прямо в Selector, не запуская свой парк безголовых браузеров или пул прокси. Начните с бесплатного уровня до 20 000 запросов.
Шаг 2: Извлечение с XPath и CSS
Parsel позволяет запрашивать один и тот же Selector на любом из языков. XPath расшифровывается как XML Path Language и навигирует по дереву документа по структуре, тогда как CSS-селекторы нацеливаются на элементы по тегу, классу или id так же, как таблица стилей. Два примера ниже извлекают аналогичные значения, чтобы вы могли непосредственно сравнить стили.
# XPath: select the text of the first h1 heading = selector.xpath("//h1/text()").get() # CSS: select the text inside a known element price = selector.css("p.price_color::text").get() # Attributes: @attr in XPath, ::attr() in CSS link_xpath = selector.xpath("//article//h3/a/@href").get() link_css = selector.css("article h3 a::attr(href)").get()
Два паттерна охватывают большую часть работы. Для чтения текста используйте /text() в XPath или ::text в CSS. Для чтения атрибута, такого как href или src, используйте @attribute в XPath или ::attr(attribute) в CSS. В каждом случае .get() возвращает первое совпадение или None, если ничего не найдено, поэтому отсутствующий элемент не вызывает ошибку.
.get() возвращает первое совпавшее значение в виде строки. .getall() возвращает список всех совпадений. Используйте .get(), когда ожидаете одно значение, например цену, и .getall(), когда хотите получить целый столбец, например все названия на странице.
Шаг 3: Перебор списка элементов
Реальные страницы содержат много повторяющихся элементов, а не один. Паттерн состоит в том, чтобы один раз выбрать повторяющийся контейнер, а затем итерироваться по нему, выполняя ограниченные селекторы против каждого элемента для построения одной записи на элемент. На книжной песочнице каждый товар, это <article class="product_pod">, так что именно по этому контейнеру мы итерируемся.
def parse_books(selector): books = [] for card in selector.css("article.product_pod"): title = card.css("h3 a::attr(title)").get() price = card.css("p.price_color::text").get() availability = card.css("p.instock.availability::text").getall() rating = card.css("p.star-rating::attr(class)").get() href = card.css("h3 a::attr(href)").get() books.append({ "title": title, "price": price, "availability": availability, "rating": rating, "href": href, }) return books
Вызов .css("article.product_pod") возвращает SelectorList, по которому можно итерироваться; каждая card сама является Selector, поэтому внутренние вызовы .css() выполняются только против этой одной карточки. Название хранится в атрибуте title ссылки, цена, в абзаце price_color, а рейтинг, в классе вида star-rating Three, поэтому мы считываем весь атрибут class и очищаем его на следующем шаге. Поле availability использует .getall(), потому что его текст разбит по пробельным узлам; объединение и удаление пробелов даёт одну чистую строку.
Шаг 4: Очистка и нормализация значений
Сырой вывод селектора обычно нуждается в лёгкой обработке, прежде чем он станет полезным. Цены содержат символ валюты, рейтинг возвращается как двухсловный класс, а текст наличия приходит с окружающими пробелами. Несколько стандартных строковых операций превращают каждое поле в чистое значение.
BASE = "https://books.toscrape.com/catalogue/" WORDS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5} def clean_book(card): price_text = card.css("p.price_color::text").get(default="") price = float(price_text.replace("£", "").strip() or 0) rating_class = card.css("p.star-rating::attr(class)").get(default="") rating_word = rating_class.replace("star-rating", "").strip() rating = WORDS.get(rating_word) stock = " ".join(card.css("p.instock.availability::text").getall()) href = card.css("h3 a::attr(href)").get(default="") return { "title": card.css("h3 a::attr(title)").get(), "price": price, "availability": stock.strip(), "rating": rating, "link": BASE + href, }
Две небольшие привычки делают этот код устойчивым. Во-первых, .get(default="") предоставляет запасное значение, чтобы отсутствующий элемент давал пустую строку, а не None, что предотвращает ошибки в последующих вызовах .replace() и .strip(). Во-вторых, разбор цены удаляет символ валюты (экранированный символ £, знак фунта стерлингов) и преобразует в float, так что значение можно сортировать и фильтровать как число. Рейтинг сопоставляет слово в классе с целым числом, а относительный href присоединяется к базовому URL для получения абсолютной ссылки.
Шаг 5: Сборка полного скрипта
Теперь соедините части в один выполняемый скрипт: загрузите страницу, создайте Selector, пройдите по карточкам через clean_book и экспортируйте записи как в JSON, так и в CSV.
import csv import json from crawlbase import CrawlingAPI from parsel import Selector api = CrawlingAPI({"token": "YOUR_CRAWLBASE_TOKEN"}) BASE = "https://books.toscrape.com/catalogue/" WORDS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5} def fetch_html(page_url): response = api.get(page_url) if response["headers"]["cb_status"] == "200": return response["body"].decode("utf-8") print(f"Request failed: {response['headers']['cb_status']}") return None def clean_book(card): price_text = card.css("p.price_color::text").get(default="") price = float(price_text.replace("£", "").strip() or 0) rating_class = card.css("p.star-rating::attr(class)").get(default="") rating = WORDS.get(rating_class.replace("star-rating", "").strip()) stock = " ".join(card.css("p.instock.availability::text").getall()) href = card.css("h3 a::attr(href)").get(default="") return { "title": card.css("h3 a::attr(title)").get(), "price": price, "availability": stock.strip(), "rating": rating, "link": BASE + href, } def parse_books(html): selector = Selector(text=html) return [clean_book(card) for card in selector.css("article.product_pod")] def save_outputs(records): with open("books.json", "w") as f: json.dump(records, f, indent=2) if not records: return with open("books.csv", "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=records[0].keys()) writer.writeheader() writer.writerows(records) def main(): url = "https://books.toscrape.com/catalogue/page-1.html" html = fetch_html(url) if not html: return records = parse_books(html) save_outputs(records) print(f"Saved {len(records)} books") if __name__ == "__main__": main()
parse_books один раз создаёт Selector и возвращает список очищенных записей через list comprehension по карточкам. save_outputs записывает JSON-файл и CSV, использующий ключи первой записи в качестве заголовка, так что вы получаете данные в том формате, который нужен вашему инструменту. Чтобы охватить весь каталог, оберните main в цикл по страницам от page-1.html до page-50.html и добавляйте результаты в один общий список; логика разбора при этом не меняется.
Как выглядит вывод
Запустите скрипт командой python books_scraper.py и вы получите чистую структурированную запись на каждую книгу, готовую для анализа, базы данных или таблицы.
[ { "title": "A Light in the Attic", "price": 51.77, "availability": "In stock", "rating": 3, "link": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html" }, { "title": "Tipping the Velvet", "price": 53.74, "availability": "In stock", "rating": 1, "link": "https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html" } ]
CSV содержит те же столбцы с одной строкой на книгу, что позволяет напрямую загрузить данные в pandas или любую таблицу для сортировки по цене или фильтрации по рейтингу.
Распространённые ошибки, которых следует избегать
Несколько привычек отличают скрапер, который держится, от того, который ломается при следующем запуске.
- Изучите страницу перед написанием селекторов. Откройте страницу в инструментах разработчика браузера и убедитесь в правильности имён классов и структуры. Селектор, нацеленный на несуществующий элемент, ничего не возвращает, и это наиболее распространённая причина пустого результата скрапинга.
-
Всегда обрабатывайте отсутствующие данные. Используйте
.get(default="")или защищайтесь отNone, чтобы одно отсутствующее поле не обрушило весь цикл. Страницы редко настолько единообразны, насколько кажутся. -
Очищайте и нормализуйте текст. Веб-текст содержит лишние пробелы и символы валют. Очищайте его с помощью
.strip()и.replace()во время разбора, чтобы хранимые значения были согласованными. - Соблюдайте темп запросов. Загрузка страниц в тесном цикле, самый быстрый способ получить ограничение скорости. Добавьте короткую паузу между запросами и держите объём разумным.
Ответственный скрапинг
Parsel только разбирает HTML, который у вас уже есть, но способ получения этого HTML по-прежнему важен. Несколько принципов позволяют любому скрапинговому проекту оставаться на правильной стороне, независимо от цели.
Проверьте условия использования сайта и его robots.txt перед сбором чего-либо и относитесь к обоим как к границам, а не предложениям. Оставайтесь на публичных данных, которые любой посетитель может видеть без входа в систему, и держите частоту запросов разумной, чтобы не перегружать серверы сайта. Когда проект затрагивает персональные данные, обязательства возрастают: такие нормативные акты, как GDPR и CCPA, регулируют, как может собираться и использоваться персональная информация, поэтому в таких случаях применяйте особую осторожность или вовсе избегайте их. Пример здесь использует песочницу, созданную специально для практики, именно для того, чтобы вы могли изучить механику без каких-либо из этих проблем, и та же дисциплина переносится, когда вы направляете скрапер на реальный сайт. Подробнее о работе в рамках ограничений сайта читайте в статье о скрапинге без блокировок.
Ключевые выводы
-
Parsel ориентирован на селекторы. Создайте один
Selector(text=html)и запрашивайте его с помощью XPath или CSS, в зависимости от того, что читается яснее для данного элемента. -
get и getall охватывают большинство случаев извлечения.
.get()возвращает первое совпадение как строку,.getall()возвращает все совпадения как список, а.get(default="")не даёт отсутствующим полям прерывать выполнение. -
Текст и атрибуты имеют фиксированный паттерн. Считывайте текст с помощью
/text()или::text, а атрибуты с помощью@attrили::attr()для XPath и CSS соответственно. - Итерируйтесь по контейнеру, а не по всей странице. Выберите повторяющийся элемент один раз, затем выполняйте ограниченные селекторы для каждого элемента, чтобы построить одну чистую запись, и экспортируйте в JSON и CSV.
- Разделяйте загрузку и разбор. Пусть Crawling API занимается рендерингом, ротацией и блокировками, а затем передавайте готовый HTML в Parsel, чтобы ваш код извлечения оставался простым.
Часто задаваемые вопросы
Что такое Parsel и почему его используют для веб-скрапинга?
Parsel, небольшая, быстрая Python-библиотека для извлечения данных из HTML и XML с использованием XPath и CSS-селекторов. Это тот же движок селекторов, что использует Scrapy, и он хорошо работает как автономный инструмент, когда у вас уже есть HTML и вы хотите извлечь из него поля. Его выбирают за чистый синтаксис, поддержку обоих языков селекторов на одном объекте и лёгкость встраивания в существующий конвейер.
В чём разница между Parsel и BeautifulSoup?
Обе библиотеки разбирают HTML, но различаются по стилю. Parsel ориентирован на селекторы: вы пишете выражения XPath или CSS и вызываете .get() или .getall(). BeautifulSoup использует цепочку методов Python, таких как find и find_all. Parsel также поддерживает XPath нативно, чего BeautifulSoup не делает. Выбирайте тот, который соответствует вашему предпочтительному способу выражения выборок.
В чём разница между get и getall в Parsel?
.get() возвращает первое совпавшее значение как строку или None, если ничего не найдено. .getall() возвращает список всех совпавших значений. Используйте .get() для одного поля, например цены или названия, и .getall(), когда хотите получить целый набор, например все ссылки на странице. Передача .get(default="value") обеспечивает запасное значение для отсутствующих элементов.
Как обрабатывать страницы, загружающие контент с помощью JavaScript?
Parsel разбирает любой переданный ему HTML, поэтому вопрос в том, как вы загружаете этот HTML. Если страница рендерит своё содержимое с помощью JavaScript, обычный запрос возвращает пустую оболочку с отсутствующими данными. Загрузка через Crawling API сначала рендерит страницу и возвращает готовый HTML, который затем загружается в Selector точно так же, как показано здесь. Код разбора при этом не меняется.
Можно ли экспортировать результаты Parsel в JSON или CSV?
Да. Parsel возвращает обычные значения Python, поэтому после того, как вы построили список словарей, вы записываете JSON стандартным модулем json и CSV с помощью csv.DictWriter, как это делает полный скрипт. После этого данные загружаются в pandas или базу данных без дополнительного преобразования.
Зачем использовать Parsel с Crawling API вместо обычного запроса?
Обычный запрос часто завершается неудачей ещё до того, как Parsel вступит в работу: страница может рендериться на стороне клиента, или сайт может блокировать трафик, не похожий на браузерный. Crawling API обрабатывает рендеринг, ротацию IP и CAPTCHA, а затем возвращает чистый HTML. Это выносит проблемы загрузки за пределы вашего кода разбора, чтобы Parsel мог сосредоточиться на том, что умеет делать хорошо: превращении HTML в структурированные поля.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
