Руководства

Парсинг с Selenium на Python: сбор данных, пагинация и CSV

Парсинг с Selenium на Python: установка, ожидание загрузки данных, пагинация и сохранение в CSV. Готовые скрипты и проверка ошибок при сборе данных.

Gideon ReedGideon Reed12 мин. чтения

Для парсинга сайтов с Selenium откройте страницу в браузере, дождитесь нужных элементов, извлеките поля каждой записи и сохраните результат в CSV. Ниже — установка, два готовых скрипта на Python и проверка результата на учебном сайте Quotes to Scrape.

Selenium нужен, когда задача требует выполнения JavaScript или действий в браузере. Если нужные данные уже есть в доступном HTTP-ответе, проще использовать HTTP-клиент, например Requests, или подходящий API. На странице Quotes to Scrape JavaScript создаёт видимые карточки, но исходные данные также встроены в HTML: здесь браузер используется для обучения.

Начните с корректной выгрузки одной страницы. Затем добавьте пагинацию и проверяйте причину остановки: last_page — следующей ссылки нет, page_limit — достигнут заданный лимит, error — произошёл сбой. Совпадение числа записей в CSV с отчётом подтверждает выгрузку, но не полноту охвата сайта.

1. Установите Python, Selenium и Chrome

Для примера нужны Python 3.10 или новее и установленный Chrome. Команды ниже создают отдельное виртуальное окружение и устанавливают Selenium версии 4.51.0.

Требование Python 3.10+ указано на странице пакета Selenium в PyPI. Откройте терминал в папке проекта.

Windows PowerShell:

powershellpowershell
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install selenium==4.51.0

macOS и Linux:

bashbash
python3 -m venv .venv
.venv/bin/python -m pip install selenium==4.51.0

Активировать окружение необязательно: команды обращаются прямо к его интерпретатору. Если Windows не распознаёт py, но команда python --version работает, создайте окружение через python -m venv .venv. Команды для macOS и Linux эквивалентны по назначению; скриншоты показывают запуск в Windows.

Selenium Manager подбирает драйвер автоматически, если вы не указали его вручную. Вызов webdriver.Chrome(options=options) при необходимости запускает этот механизм. При первом запуске может потребоваться доступ в интернет. Ограничения сети, неподдерживаемая среда или конфликтующий драйвер в PATH иногда требуют ручной настройки.

Сохраните следующий скрипт в файл scrape_quotes.py. Не называйте файл selenium.py: он может перекрыть импорт установленного пакета.

2. Соберите данные с одной страницы и сохраните CSV

Первый скрипт ждёт появления карточек с заполненными обязательными полями и сохраняет каждую запись отдельной строкой CSV. Для извлечения данных достаточно Selenium и стандартной библиотеки Python.

pythonpython
import csv
from pathlib import Path

from selenium import webdriver
from selenium.common.exceptions import (
    NoSuchElementException, StaleElementReferenceException, WebDriverException
)
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

def read_records(driver):
    """Wait until the currently rendered cards have their required fields."""
    try:
        cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
        if not cards:
            return False
        records = []
        for card in cards:
            quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
            author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
            tags = [tag.text for tag in card.find_elements(By.CSS_SELECTOR, ".tag")]
            if not quote or not author:
                return False
            records.append({"quote": quote, "author": author,
                            "tags": " | ".join(tags)})
        return records
    except (NoSuchElementException, StaleElementReferenceException):
        return False


options = webdriver.ChromeOptions()
options.add_argument("--window-size=1280,900")
driver = webdriver.Chrome(options=options)
try:
    driver.set_page_load_timeout(30)
    driver.get("https://quotes.toscrape.com/js/")
    records = WebDriverWait(driver, 15).until(read_records)
    for record in records:
        record["source_url"] = driver.current_url
    output = Path("quotes.csv").resolve()
    with output.open("w", newline="", encoding="utf-8-sig") as file:
        writer = csv.DictWriter(
            file, fieldnames=["quote", "author", "tags", "source_url"]
        )
        writer.writeheader()
        writer.writerows(records)
    print(f"Page: {driver.current_url}")
    print(f"Saved {len(records)} records to {output}")
finally:
    try:
        driver.quit()
    except WebDriverException:
        pass

Сохраните код целиком. Проверьте, что файл называется scrape_quotes.py, а не scrape_quotes.py.txt, и откройте терминал в той же папке.

Запуск в Windows:

powershellpowershell
.\.venv\Scripts\python.exe scrape_quotes.py

Запуск в macOS и Linux:

bashbash
.venv/bin/python scrape_quotes.py

В терминал нужно вставить только команду запуска. Строки Page: и Saved — сообщения программы.

Chrome откроется и автоматически закроется после завершения работы. В терминале появятся адрес страницы, число сохранённых записей и полный путь к quotes.csv. Файл создаётся в текущей рабочей папке; повторный запуск перезаписывает его.

Эта версия обрабатывает только текущую страницу. Если браузер не запустится или данные не удастся извлечь, Python выведет исключение вместо сообщения об успешном сохранении. Проверяйте время изменения CSV: после ошибки в папке может остаться файл от предыдущего запуска.

Запуск Selenium в Windows PowerShell: 10 записей сохранены в CSV

3. Разберитесь в селекторах, ожиданиях и формате данных

Извлекайте связанные поля внутри одной карточки и ждите готовности данных через явное ожидание. Так меньше риск перепутать автора с цитатой или прочитать страницу до завершения её обновления.

Как подобрать CSS-селекторы

Откройте учебную страницу Quotes to Scrape с JavaScript в Chrome. Нажмите правой кнопкой мыши на цитату и выберите «Просмотреть код» или Inspect.

Элемент CSS-селектор Что извлекаем
Карточка .quote Контейнер одной записи
Текст .text Цитату
Автор .author Имя автора
Теги .tag Список тегов внутри карточки

Сначала find_elements() находит все контейнеры .quote. Затем find_element() ищет текст и автора внутри каждого из них. Если отдельно собрать все цитаты и всех авторов, а потом сопоставить их по позиции, пропущенное поле может незаметно сдвинуть соответствие.

В этом примере удобны CSS-селекторы. XPath тоже подходит, но длинный абсолютный XPath из DevTools часто зависит от случайной вложенности элементов. Выбирайте устойчивые атрибуты и связи между элементами.

Карточка цитаты и соответствующие элементы quote, text, author и tag в Chrome DevTools

Как дождаться загрузки данных в Selenium

Вызов driver.get() учитывает стратегию загрузки страницы, но не гарантирует завершения всех последующих обновлений JavaScript. В документации Selenium об ожиданиях также рекомендуется не смешивать неявные и явные ожидания.

Конструкция WebDriverWait(driver, 15).until(read_records) повторно вызывает функцию чтения с тайм-аутом 15 секунд, пока та не вернёт готовые записи. Условие выполнено, когда карточки найдены, а у всех проверяемых карточек заполнены текст и автор. Если элемент ещё не появился или уже заменён в DOM, функция возвращает False, и проверка повторяется.

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

Какие значения попадут в CSV

Свойство .text возвращает видимый текст. Метод get_attribute("href") нужен для адреса ссылки: надпись Next и URL следующей страницы — разные значения.

В CSV используются четыре поля:

  • quote — текст цитаты;
  • author — автор;
  • tags — теги, объединённые через |;
  • source_url — адрес страницы, с которой сохранена запись.

В версии с пагинацией ключ удаления дублей состоит из текста цитаты и автора. Поле source_url хранит адрес первого появления сохранённой записи. Для товаров обычно лучше использовать устойчивый ID товара, а не название.

Модуль csv корректно экранирует запятые, кавычки и переносы строк. Параметр newline="" соответствует рекомендациям Python по работе с CSV, а кодировка utf-8-sig помогает табличным редакторам распознать Unicode. Считайте записи CSV-парсером: физические строки файла не равны числу записей, если внутри поля есть перенос строки.

CSV с 10 записями и столбцами quote, author, tags и source_url

4. Добавьте пагинацию и отчёт о завершении

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

Когда проверите одностраничную выгрузку, сохраните следующий код в файл scrape_quotes_pages.py в той же папке.

pythonpython
import argparse
import csv
import json
from pathlib import Path
from urllib.parse import urljoin

from selenium import webdriver
from selenium.common.exceptions import (
    NoSuchElementException, StaleElementReferenceException, WebDriverException
)
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

START_URL = "https://quotes.toscrape.com/js/"
FIELDS = ["quote", "author", "tags", "source_url"]


def read_records(driver):
    """Wait until the currently rendered cards have their required fields."""
    try:
        cards = driver.find_elements(By.CSS_SELECTOR, ".quote")
        if not cards:
            return False
        records = []
        for card in cards:
            quote = card.find_element(By.CSS_SELECTOR, ".text").text.strip()
            author = card.find_element(By.CSS_SELECTOR, ".author").text.strip()
            tags = [tag.text for tag in card.find_elements(By.CSS_SELECTOR, ".tag")]
            if not quote or not author:
                return False
            records.append({"quote": quote, "author": author,
                            "tags": " | ".join(tags)})
        return records
    except (NoSuchElementException, StaleElementReferenceException):
        return False


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--pages", type=int, default=1)
    parser.add_argument("--headless", action="store_true")
    args = parser.parse_args()
    if args.pages < 1:
        parser.error("--pages must be at least 1")

    options = webdriver.ChromeOptions()
    options.add_argument("--window-size=1280,900")
    if args.headless:
        options.add_argument("--headless=new")
    driver = None
    rows, seen, visited = [], set(), set()
    raw_count = 0
    status, detail = "error", "Browser did not start"
    try:
        driver = webdriver.Chrome(options=options)
        driver.set_page_load_timeout(30)
        wait = WebDriverWait(driver, 15)
        driver.get(START_URL)
        for page_number in range(1, args.pages + 1):
            current_url = driver.current_url
            if current_url in visited:
                raise ValueError("Repeated page URL; pagination stopped")
            batch = wait.until(read_records)
            visited.add(current_url)
            raw_count += len(batch)
            for record in batch:
                key = (record["quote"], record["author"])
                if key not in seen:
                    seen.add(key)
                    rows.append({**record, "source_url": current_url})
            print(f"Page {page_number}: {len(batch)} records | {current_url}")

            links = driver.find_elements(By.CSS_SELECTOR, ".pager .next a")
            if not links:
                status, detail = "last_page", "No Next link on the processed page"
                break
            if page_number == args.pages:
                status, detail = "page_limit", "More pages exist; requested limit reached"
                break
            expected_url = urljoin(current_url, links[0].get_attribute("href"))
            old_card = driver.find_element(By.CSS_SELECTOR, ".quote")
            links[0].click()
            wait.until(EC.staleness_of(old_card))
            wait.until(EC.url_to_be(expected_url))
    except (WebDriverException, ValueError) as exc:
        status = "error"
        detail = f"{type(exc).__name__}: {str(exc).splitlines()[0]}"
        if driver is not None:
            try:
                driver.save_screenshot("failure.png")
            except WebDriverException:
                pass
    finally:
        if driver is not None:
            try:
                driver.quit()
            except WebDriverException:
                pass

    output = Path("quotes-pages.csv").resolve()
    with output.open("w", newline="", encoding="utf-8-sig") as file:
        writer = csv.DictWriter(file, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows(rows)
    report = {
        "status": status, "detail": detail,
        "pages_processed": len(visited), "raw_records": raw_count,
        "unique_records": len(rows), "duplicates_removed": raw_count - len(rows),
        "csv": str(output)
    }
    Path("run-status.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
    print(json.dumps(report, indent=2))
    if status == "error":
        raise SystemExit(1)


if __name__ == "__main__":
    main()

Запросите обработку не более 20 страниц.

Windows PowerShell:

powershellpowershell
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20

macOS и Linux:

bashbash
.venv/bin/python scrape_quotes_pages.py --pages 20

Активировать окружение не требуется. Скрипт создаёт quotes-pages.csv и run-status.json; файл quotes.csv от первого примера сохраняется. Повторный запуск перезаписывает результаты многостраничной версии. Число 20 — верхний предел обработки, а не количество страниц сайта.

После чтения страницы программа ищет .pager .next a. Если ссылка найдена, она:

  1. Запоминает адрес назначения и одну карточку старой страницы.
  2. Нажимает Next.
  3. Ждёт, пока старая карточка перестанет относиться к текущему документу: EC.staleness_of(old_card).
  4. Проверяет ожидаемый URL через EC.url_to_be(expected_url).
  5. На следующей итерации ждёт готовности новых записей.

Если сразу после клика проверять только наличие .quote, условие могут выполнить ещё не исчезнувшие карточки старой страницы. Проверка выше подходит для пагинации с заменой документа. Для SPA, где содержимое обновляется без такой навигации, нужен другой сигнал: например, новый номер страницы и изменившиеся ID записей.

Как проверить результат парсинга

Сначала прочитайте run-status.json, затем проверьте количество и содержимое строк CSV.

Статус Что произошло Как трактовать результат
last_page На обработанной странице нет ссылки Next по заданному селектору При верных селекторе и разметке достигнут конец пагинации; это не доказывает доступность всех записей на сервере
page_limit Ссылка Next есть, но достигнут лимит --pages Сбор остановился по ограничению; оставшиеся страницы не обработаны
error Сбой запуска, извлечения данных или навигации Код выхода — 1; скрипт пытается сохранить уже собранные записи, результат может быть частичным

Отчёт также содержит:

  • pages_processed — количество обработанных страниц;
  • raw_records — количество записей до удаления дублей;
  • unique_records — количество сохранённых уникальных записей;
  • duplicates_removed — количество удалённых дублей;
  • csv — полный путь к выгрузке;
  • detail — пояснение причины остановки.

Страница, на которой обязательные поля так и не стали доступны, не считается обработанной. Отсутствие дублей не подтверждает полноту данных: сравните охват с задачей и проверьте несколько записей вручную.

Фоновый запуск Selenium: 10 страниц, статус last_page, код выхода 0 и 100 записей в CSV

В Windows сразу после завершения скрипта проверьте код выхода и число записей:

powershellpowershell
Write-Output "Exit code: $LASTEXITCODE"
Write-Output "CSV records: $(@(Import-Csv .\quotes-pages.csv).Count)"

Код 0 означает, что программа не сообщила об ошибке. Статус page_limit тоже даёт код 0, поэтому отчёт всё равно нужно прочитать. Число записей в CSV должно совпадать с unique_records. Это проверка выгрузки, а не правильности каждого поля или полноты данных сайта.

5. Адаптируйте парсер под свой сайт

При переходе на другой сайт меняйте адрес, селекторы, поля и условия пагинации вместе. Новый URL со старыми селекторами .quote обычно приводит к пустому результату или тайм-ауту.

Проверьте настройки по порядку:

  1. Стартовая страница. Замените START_URL на страницу списка, которую разрешено обрабатывать. Проверьте итоговый адрес после перенаправлений.
  2. Контейнер записи. Вместо .quote укажите повторяющуюся карточку или строку таблицы. Сопоставьте найденные элементы с видимыми записями.
  3. Поля и дубли. Одновременно обновите извлечение полей, FIELDS и ключ уникальности. Необязательное поле не должно блокировать чтение всей страницы.
  4. Следующая порция данных. Замените .pager .next a и выберите признак завершения перехода или подгрузки.

Для кнопки «Показать ещё» сохраняйте уже собранные строки и ждите новых ID либо другого сигнала готовности. Ожидание замены всего документа здесь не подходит, если навигации нет.

При бесконечной прокрутке собирайте текущую порцию, прокручивайте страницу и ждите новых ID. Ограничьте длительность или число итераций. Если ожидание истекло, но явного признака конца списка нет, фиксируйте неопределённую причину остановки. Виртуализированные списки удаляют старые элементы из DOM: извлечение только после последней прокрутки может потерять ранние записи.

Если данные находятся в iframe, сначала переключитесь в нужный фрейм. Для Shadow DOM и содержимого canvas нужен отдельный способ извлечения: увеличение тайм-аута не превращает их в обычные элементы документа.

6. Исправьте пустые результаты, повторные страницы и ошибки драйвера

При сбое сначала проверьте текущий URL, фактический DOM и сообщение об ошибке. Увеличение времени ожидания помогает только тогда, когда нужные данные действительно появляются позже.

Симптом Что проверить и исправить
Нет записей, ожидание постоянно истекает Проверьте селектор, контекст фрейма и наличие нужного содержимого. Ожидание не исправит селектор без совпадений
StaleElementReferenceException Узел заменён или документ изменился. Найдите элемент заново после обновления
Повторяется одна страница Проверьте следующий URL или индикатор страницы. Оставьте защиту от повторного адреса; удаление дублей не исправляет навигацию
Chrome не запускается Разделите ошибку загрузки драйвера и ошибку запуска браузера. Проверьте исключение, установленный Chrome, драйвер в PATH и ограничения ОС
В CSV не хватает данных Проверьте статус запуска, сообщения терминала и время изменения файлов. Выгрузка может содержать только часть записей
PermissionError при сохранении Закройте CSV в Excel или другом редакторе, проверьте права записи в папку и повторите запуск

Справочник ошибок Selenium объясняет, почему сохранённая ссылка на элемент после изменения DOM не находит его заново автоматически.

Прокси браузера не исправляет автоматически проблемы сети, через которую Selenium Manager скачивает драйвер. Если среда требует ручной настройки, используйте доступную разрешённую сеть или явно укажите совместимый драйвер. Отключать защиту браузера ради любого сбоя запуска не следует.

У одностраничной версии проверяйте исключение в терминале и дату quotes.csv; у многостраничной — run-status.json и quotes-pages.csv. Ошибка записи файла может помешать созданию нового отчёта, поэтому старые файлы нельзя считать подтверждением текущего запуска.

Если вместо списка появилась CAPTCHA или страница отказа в доступе, остановите автоматические повторы и проверьте условия использования, разрешения и доступные API. Блокировка — не обычный пустой список.

7. Запустите Selenium без окна браузера

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

Windows PowerShell:

powershellpowershell
.\.venv\Scripts\python.exe scrape_quotes_pages.py --pages 20 --headless

macOS и Linux:

bashbash
.venv/bin/python scrape_quotes_pages.py --pages 20 --headless

В коротком одностраничном примере обработчика этого флага нет. В многостраничной версии он добавляет параметр Chrome --headless=new.

Оба режима используют размер окна 1280 × 900, чтобы уменьшить различия в вёрстке. В headless-режиме браузер продолжает работать, но не показывает окно. Подробности — в документации Chrome о headless-режиме.

Если сессия браузера уже создана, многостраничный скрипт при ошибке извлечения или навигации пытается сохранить failure.png. При сбое запуска изображения может не быть. Проверяйте дату снимка: файл мог остаться от предыдущего запуска. Не публикуйте диагностические снимки с учётными данными или персональной информацией.

Фоновый режим сам по себе не гарантирует ускорения, снижения затрат или успешного доступа. Перед запуском по расписанию сравните выгрузку и отчёт с результатом в обычном режиме.

8. Выберите прокси для Selenium с учётом региона и сессии

Прокси позволяет направить трафик браузера через выбранную точку выхода в сеть. Для мониторинга цен и регионального контента важны два параметра: нужная геолокация и время сохранения одного IP-адреса.

Задача Что проверить
Сравнение цен или контента в разных регионах Доступные локации и фактическое местоположение выходного IP
Сбор независимых страниц отдельными порциями Нужна ли смена IP между порциями и какие интервалы запросов предусмотрены
Пагинация или сценарий, зависящий от сессии Сохраняется ли один выходной IP на протяжении задачи
Регулярные проверки с одного адреса Срок аренды статического IP и условия его обслуживания

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

Параметры локации и сессии резидентных прокси Socks5.IO

Для настройки используйте руководство по подключению прокси в Selenium. Сначала проверьте внешний IP и его геолокацию. Затем запустите сбор одной страницы и протестируйте пагинацию с теми же параметрами сессии. Увеличивайте лимит только после проверки результата.

Для авторизации на прокси в Chrome нужен поддерживаемый способ настройки: добавить username:password в --proxy-server недостаточно. Соблюдайте разрешения сайта и ограничения частоты запросов.

Часто задаваемые вопросы

Установите Selenium, создайте браузер через webdriver.Chrome(), откройте страницу и дождитесь нужных записей. Извлеките поля внутри каждого контейнера и сохраните их модулем csv. При пагинации сначала подтвердите переход, затем читайте следующую порцию.

Последние статьи

Иллюстрация глобальной прокси-сети

Подключитесь к глобальной прокси-сети — каждое соединение будет быстрее и стабильнее

Начни бесплатно