Часто случается так, что ваш личный или корпоративный парсер может остановиться где-то в середине процесса и выдать ошибку. А может просто начать записывать какой-то мусор в базу данных. И всё из-за того, что дизайнеры немного обновили вёрстку или чуть-чуть переработали структуру макета. Этого уже может быть достаточно для того, чтобы целевой сайт стал «нечитаемым» для скрипта.
Как избежать таких проблем? Этот материал о том, как сделать свой скрипт веб-парсинга на Python более устойчивым к незначительным обновлениям на целевых сайтах и страницах. Начнём с наиболее вероятных причин.
Распространённые причины, по которым скрипты для парсинга на Python перестают работать
Рассмотрим самые популярные проблемы, из-за которых процесс парсинга может прерваться:
- Незначительные или существенные изменения в структуре HTML-кода страниц. В том числе ошибки вёрстки.
- Переход со статического HTML на динамический, с использованием Ajax или JavaScript.
- Блокировки со стороны целевых сайтов (когда срабатывают механизмы их защиты).
- Изменение структуры URL-адресов, ответов по API и другие инфраструктурные проблемы.
Давайте расскажем о них немного поподробнее, чтобы иметь правильное представление о том, как их искать и реагировать в случае возникновения.
Изменения в структуре HTML
Большинство парсеров, если они не используют при своей работе компьютерное зрение или большие языковые модели (LLM, такие как ChatGPT), при разборе страницы на составляющие опираются на её DOM-структуру – это в первую очередь дерево HTML-тегов, а также CSS-стили, классы, идентификаторы и т.п. Так устроены и работают стандартные парсеры.
И если искомый элемент исчезает или меняются его атрибуты, извлечение данных становится невозможным – парсер не может найти нужный элемент на странице. Или, если находит что-то с похожими критериями, то данные внутри могут оказаться мусорными, то есть бесполезными.
Почему HTML-изменения ломают парсеры
Изменения HTML-кода страниц могут происходить по разным причинам:
- Поменялся дизайн или вёрстка. Достаточно переместить элемент из одного места страницы в другое, и его подчинённость в DOM-структуре меняется.
- Дополнительно были внедрены новые элементы (блоки, виджеты, разделы и т.п.).
- На сайте намеренно внедрена система защиты, которая рандомизирует названия классов и селекторов, сильно усложняя навигацию по DOM-структуре.
- Один или несколько HTML-тегов не имеют закрывающей пары. Могут иметься и другие ошибки в вёрстке, но именно незакрытые теги приводят к тому, что парсер начинает собирать массу ненужной информации или останавливается с ошибкой.
Чаще всего проблеме подвержены парсеры, которые опираются при поиске на абсолютные пути, а не на относительные.
Например, в синтаксисе XPath это может выглядеть так:
- Абсолютный путь – //*[@id="main-content"]/div/div[2]/div[1]/h2
- Относительный (более правильный подход) – //h2[contains(@class, 'title')] или //*[@data-qa="product-name"]
Чтобы минимизировать проблемы с неправильной вёрсткой, можно задействовать специальный модуль, который будет проверять работоспособность парсера перед масштабным запуском. А ещё лучше – наладить постоянный мониторинг на наличие ошибок. Но будьте внимательны: контролируемая «песочница» может отнимать много времени и вычислительных ресурсов.
Динамический контент и JavaScript
Современные сайты, созданные с помощью таких фреймворков, как React, Angular, Vue.js и т.п., часто не отдают готовую HTML-структуру. Браузер получает почти пустой HTML-файл и ссылки на JavaScript-скрипты. В итоге сайт «собирается» по кусочкам – по мере выполнения этих скриптов — это частая проблема парсинга динамических сайтов.
Так как страница не имеет нужного кода и выстраивается асинхронно, то классические парсеры, например, с использованием библиотек requests + BeautifulSoup, видят только функциональный «скелет» страницы, без итогового полезного содержимого. Соответственно, спарсить ничего не получится, так как искомых данных на странице буквально нет (они могут подтянуться позже, одним из JS-скриптов).
Выстроить и получить тот контент, который видит конечный пользователь, в данном случае можно только с помощью реальных браузеров или с помощью особых движков для рендеринга JavaScript (например, как в HtmlUnit). Для автоматизации браузеров нужно задействовать библиотеки-посредники, такие как Selenium, Playwright, Puppeteer. С недавних пор в Google есть встроенный протокол CDP (Chrome DevTool Protocol), но и для взаимодействия с ним может потребоваться библиотека-посредник.
Безголовые браузеры – это максимально надёжно, но очень медленно и ресурсоёмко. А ещё стоит помнить, что некоторые продвинутые антифрод-системы научились выявлять признаки headless-браузеров в общем потоке и успешно их блокировать.
Ограничения на сайтах и лимиты скорости
Владельцы сайтов не хотят, чтобы их парсили. В определённый момент паразитная нагрузка может привести к существенным расходам на хостинг. Поэтому администраторы и программисты прибегают к различным механизмам защиты. В их числе:
- Ограничения по числу запросов и/или сессий. Обычно они привязываются к конкретному IP-адресу.
- Проверка IP в чёрных списках (специализированные удалённые базы данных) и в своих стоп-листах.
- Сверка типа адреса (серверный, он домашний или мобильный), а также его расположения.
- Анализ поведения и цифровых отпечатков (браузерного профиля) пользователя.
Если система безопасности обнаруживает бота, то вместо положенной полноценной веб-страницы она отдаёт парсеру только код ошибки, экономя тем самым ресурсы сервера.
Логичный выход – включить в свой скрипт парсинга на Python обработчик ошибок сервера и логику для их обхода. Например,сразу задать ротацию прокси.
Изменения URL или API
Многие парсеры для обхода большого каталога статей, а также для навигации по сайту могут опираться на структуру URL-адресов. Например, для поиска в Google можно воспользоваться схемой вида «https://www.google.com/search?q=my%20query». Это простейший вариант, так как внутри можно задействовать специальные операторы и настройки. Это почти готовый API-интерфейс. А ведь у сайтов может быть и реальный API со своей структурой команд.
И если раньше, например, для обращения к странице товара использовалась конструкция вида «site.com/products/123», то после смены её на что-то типа «site.com/catalog/item/123» парсер уже не сможет найти страницу и выдаст ошибку.
Методы, которые помогут сделать ваш скрипт для парсинга на Python максимально устойчивым
С классификацией проблем разобрались, осталось рассказать о более эффективном использовании Python для веб-скрапинга. Ниже обозначим те подходы, которые помогут уменьшить количество сбоев и повысить стабильность работы вашего парсера.
Выбор надёжных библиотек Python для веб-скрапинга
Правильно выбранные библиотеки, фреймворки и другие инструменты – это половина успеха. Но стоит помнить, что у каждого технического решения своя область применения, ограничения и возможности, синтаксис команд, архитектура и т.п.
Не существует волшебных библиотек «на все случаи жизни». Поэтому подбирать их нужно строго под свои задачи и специфику целевых сайтов.
Например:
- Для классических сайтов с server-side рендерингом (SSR), когда браузеру отдаётся результирующий HTML-код, подойдёт связка из Python requests (библиотека для работы с HTTP-запросами) и BeautifulSoup4 (функциональный синтаксический анализатор). Если вам крайне важна скорость и производительность, то BeautifulSoup4 можно заменить на lxml, но в этом случае придётся освоить более сложный синтаксис поиска элементов на странице.
- Если нужно реализовать большое количество параллельных потоков в промышленных масштабах, то с задачей поможет справиться нишевый фреймворк, такой как Scrapy. Он же может работать с рендерингом JavaScript на отдельном сервере – через Scrapy-Splash.
- Для обработки динамических сайтов потребуются headless-браузеры и веб-драйвера, такие как Selenium или Playwright.
- Если вам нужно работать с небольшим количеством целевых сайтов, но с огромным количеством разных браузерных профилей, в том числе для распараллеливания процесса парсинга, то следует обратить внимание на антидетект-браузеры. Они лучше обходят системы защиты – их тяжелее обнаружить и заблокировать.
Написание устойчивых CSS-селекторов и XPath-запросов
Об абсолютных путях до селекторов мы уже упоминали, но это лишь одна из самых частых проблем. Чтобы конструкция для выборки данных в HTML-документах работала устойчиво, следует придерживаться следующих правил:
- Используйте относительные пути в синтаксисе запросов.
- По возможности идентифицируйте элементы структуры на основе атрибутов (data-*, например, data-qa="product-name"). Такие атрибуты меняются реже остальных элементов вёрстки.
- Если атрибутов нет, то старайтесь обращаться к идентификаторам (id) или к имени/классу элемента. Учтите, что имена классов меняются при работе с дизайном в первую очередь, поэтому нужно отдавать приоритет выборкам по совпадению. Например, вместо прямого класса div[class="price"] логично обращаться к вхождению ключевого слова div[class*="price"]. Для XPath конструкция может выглядеть так – //div[contains(@class, "price")]. В этом случае, даже если общая конструкция изменится, но в её составе останется обозначение «цены», то парсер продолжит работать.
- Наименее надёжные конструкции, которые описывают позицию элемента в DOM-структуре относительно других элементов – дочерние, родительские, на том же уровне (N-ый по счёту).
- Чтобы повысить точность идентификации, используйте сразу несколько правил и комбинируйте их условия. Например, //a[@href and @title] – тут выборка будет проводиться по всем элементам <a>, которые имеют атрибуты ссылки и заголовка.
Резидентные прокси
Идеальные прокси-серверы для доступа к ценным данным со всего мира.
Обработка ошибок и резервные стратегии
Не стоит тешить себя надеждой, что ваш парсер будет работать как часы, без ошибок и сбоев. Нужно предвидеть возникновение проблем и заранее готовиться к типовым сценариям реагирования. Какие-то ошибки могут быть новыми и нестандартными, но большинство можно предугадать даже без большого опыта.
Наиболее успешные практики:
- Используйте конструкции try...except, чтобы всегда иметь возможность быстрой обработки исключений.
- Проверяйте наличие нужного элемента до извлечения данных. Для этого вполне подходят конструкции if… else.
- Обязательно давайте данным второй шанс и предусматривайте логику повторных попыток парсинга. Для этого нужно анализировать коды ошибок сервера, к которому вы обращаетесь, и выставлять паузу между новыми обращениями к той же странице. Например, если сервер отдаёт ошибку 503 (сервис временно недоступен) или 429 (слишком много запросов), то можно выждать несколько секунд и снова повторить запрос. Чтобы повторные запросы не были зациклены навечно, следует устанавливать лимит на их количество. Для настройки логики можно задействовать вспомогательные библиотеки, такие как tenacity.
- Не забывайте сохранять прогресс парсинга, чтобы в случае ошибки или остановки не начинать всё заново.
Обратите внимание, что многие ошибки связаны с блокировкой парсера по IP или на основе анализа поведенческих факторов. Поэтому не забывайте рандомизировать время между отдельными запросами, а также используйте качественные прокси с гео-таргетингом.
Работа с динамическими страницами с помощью headless-браузеров
Мы уже говорили о том, что безголовые браузеры позволяют вашему скрипту парсинга на Python «видеть» итоговый код так, как его видят конечные пользователи. Более того, с помощью таких браузеров вполне можно имитировать действия юзеров: заполнять поля, двигать курсором, прокручивать страницу и прочее.
Но тут тоже есть ряд нюансов:
- Некоторые сервисы научились выявлять популярные headless-браузеры по их особым отпечаткам и атрибутам. Чтобы этого избежать, нужно углубиться в сферу имитирования реалистичных цифровых отпечатков: правильно указывать user-agent, позаботиться о наполнении файлов с куками, научиться отдавать правильный список шрифтов и т.п. Есть браузеры, в которых этот подход уже обыгран как надо – это антидетект-браузеры. Плюс для задач скрытия следов headless-браузеров можно использовать специальные библиотеки, такие как undetected_chromedriver, selenium-stealth, playwright-stealth, fake-useragent и пр.
- Используйте браузер без графического режима, чтобы сэкономить вычислительные ресурсы сервера/ПК. Графический режим стоит активировать только для отладки.
- Так как содержимое динамических страниц не формируется сразу, а по мере прогрузки или даже при определённых действиях пользователя, обязательно стоит подумать о логике ожидания нужных элементов. Например, в Playwright для этого можно использовать конструкции типа await page.wait_for_selector('h1.loaded'). Здесь скрипт парсинга на Python будет ждать появления заголовка h1.
- Используйте браузер в паре с качественными прокси. Лучше всего для задач веб-парсинга на Python подходят резидентные IP и мобильные прокси. В этом случае вы получаете трастовые IP-адреса реальных пользователей (домашних или мобильных). Чтобы прокси не улетели в чёрный список, нужно позаботиться об их ротации и замене в случае проблем. Например, у Froxy можно настроить подбор новых адресов в той же локации и у того же оператора связи. Если парсинг масштабный, то заменять IP можно хоть при каждом новом запросе. Но лучше всего это делать по таймеру.
Использование регулярных выражений и сверки с паттернами для повышения гибкости
Регулярные выражения (regex) – это один из самых мощных инструментов для извлечения данных из неструктурированного текста. Его возможности просто поражают. С помощью regex можно отслеживать повторяющиеся паттерны (префиксы, окончания, блоки кода и т.п.), а также очищать и нормализовать данные.
Регулярные выражения могут стать палочкой-выручалочкой и отвечать за реализацию резервных стратегий, когда ни один из предыдущих подходов не сработал.
Например, с помощью regex можно максимально быстро извлекать «узнаваемые» данные, такие как номера телефонов, адреса email, цены и т.п.
В отличие от синтаксических анализаторов, таких как Beautiful Soup, регулярки привязываются к структуре самих данных, а не к структуре HTML-документа.
Автоматизация мониторинга и оповещений об изменениях на веб-сайте
Масштабный парсинг может выполняться очень долго. А многие задачи запускаются повторно – с заданной периодичностью. Соответственно, в таких случаях стоит заранее подумать о:
- непрерывном логировании процесса. Скрипт веб-парсинга должен записывать ключевую информацию о своих действиях и максимально подробно освещать ошибки.
- настойке оперативных оповещений. Это как простейшая система реагирования на инциденты. Скрипт парсинга остановился – вы получили уведомление. Нашлась ошибка – вам снова уведомление. Если в небольших проектах вполне можно обойтись сообщениями на email или в мессенджер (Slack, Telegram), то в более крупных выстраиваются целые экосистемы на основе Prometheus или Grafana.
Но это касается непосредственно работы парсера. А ведь можно превентивно реагировать и на изменения в вёрстке целевого сайта. Например, можно настроить небольшую задачу для парсинга определённых страниц с целью выявления на них изменений. Если извлечение данных с них завершается с ошибкой – это отдельный сигнал разработчику о том, что нужно заранее поменять логику сбора данных и донастроить конструкции, на основе которых делаются выборки.
Предметное руководство по веб-скрапингу на Python – пример кода
Давайте лучше покажем пример кода скрипта Python-парсинга с обработкой ошибок и отказоустойчивостью.
#!/usr/bin/env python3
"""
Robust scraper: requests -> fallback Playwright (headless)
Features:
- requests session with Retry/backoff
- UA rotation + optional proxy rotation
- CSS selector -> XPath fallback parsing
- DOM snapshot + diff detection (canonicalization + SHA256 signature)
- Telegram notifications for alerts
- Save progress to resume
- Playwright fallback: waits for a specific selector to appear
"""
import os
import time
import random
import json
import logging
import hashlib
import difflib
from typing import Optional, List, Dict
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from bs4 import BeautifulSoup
from lxml import html
from playwright.sync_api import sync_playwright
# ---------------- НАСТРОЙКИ ----------------
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15",
# добавьте больше реалистичных строк User-Agent
]
PROXIES = [
# "http://user:pass@proxy1.example:3128",
# "http://proxy2.example:3128",
]
HEADERS_BASE = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
MAX_RETRIES = 3
BACKOFF_FACTOR = 1.0
SAVE_FILE = "scrape_progress.json"
LOG_FILE = "scraper.log"
SNAPSHOT_DIR = "snapshots"
# Настройки Telegram (укажите свои значения)
TELEGRAM_BOT_TOKEN = "" # например, "123456:ABC-DEF..."
TELEGRAM_CHAT_ID = "" # например, "987654321"
# Настройки Playwright
PLAYWRIGHT_TIMEOUT_MS = 20000 # таймаут по умолчанию 20с для навигации/селекторов
# ----------------------------------------
os.makedirs(SNAPSHOT_DIR, exist_ok=True)
# Логирование
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.FileHandler(LOG_FILE), logging.StreamHandler()]
)
# ---------- Telegram ----------
def telegram_send(text: str):
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
logging.debug("Telegram not configured; skipping send.")
return
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text}
try:
r = requests.post(url, json=payload, timeout=10)
if r.status_code != 200:
logging.warning("Telegram send failed: %s %s", r.status_code, r.text)
except Exception as e:
logging.exception("Telegram send exception: %s", e)
def send_alert(msg: str):
logging.warning("ALERT: %s", msg)
telegram_send(f"ALERT: {msg}")
# ---------- построение сессии ----------
def build_session(proxy: Optional[str] = None) -> requests.Session:
s = requests.Session()
retries = Retry(
total=MAX_RETRIES,
backoff_factor=BACKOFF_FACTOR,
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=frozenset(["GET", "POST"])
)
adapter = HTTPAdapter(max_retries=retries)
s.mount("https://", adapter)
s.mount("http://", adapter)
if proxy:
s.proxies.update({"http": proxy, "https": proxy})
return s
# ---------- загрузка через requests ----------
def fetch_url_requests(url: str, session: requests.Session, timeout: int = 20) -> Optional[requests.Response]:
headers = HEADERS_BASE.copy()
headers["User-Agent"] = random.choice(USER_AGENTS)
try:
# вежливая пауза (джиттер)
time.sleep(random.uniform(0.4, 1.6))
resp = session.get(url, headers=headers, timeout=timeout)
resp.raise_for_status()
# базовая проверка: тело не пустое и содержит <html> или <body>
if resp.text and ("<html" in resp.text.lower() or "<body" in resp.text.lower()):
return resp
logging.debug("Requests вернул небольшой/пустой ответ для %s", url)
except requests.HTTPError as e:
logging.error("HTTP ошибка для %s: %s", url, e)
except requests.RequestException as e:
logging.error("Ошибка запроса для %s: %s", url, e)
return None
# ---------- запасной вариант: Playwright ----------
def fetch_with_playwright(url: str, wait_selector: str = "body", timeout_ms: int = PLAYWRIGHT_TIMEOUT_MS) -> Optional[str]:
"""
Load page with Playwright (Chromium headless) and wait for wait_selector to appear.
Returns rendered HTML or None.
"""
try:
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(user_agent=random.choice(USER_AGENTS))
page = context.new_page()
# переходим и ждём DOMContentLoaded; затем ждём нужный селектор
page.goto(url, timeout=timeout_ms, wait_until="domcontentloaded")
# ждём селектор (если не найден -> TimeoutError)
page.wait_for_selector(wait_selector, timeout=timeout_ms)
content = page.content()
browser.close()
# проверка корректности
if content and ("<html" in content.lower() or "<body" in content.lower()):
return content
logging.debug("Playwright вернул неожиданный контент для %s", url)
except Exception as e:
logging.exception("Сбой Playwright для %s: %s", url, e)
send_alert(f"Playwright error for {url}: {e}")
return None
# ---------- комбинированная загрузка: requests + Playwright ----------
def robust_fetch_html(url: str, proxy: Optional[str], wait_selector: str) -> Optional[str]:
session = build_session(proxy=proxy)
resp = fetch_url_requests(url, session)
if resp:
return resp.text
logging.info("Requests не справился или вернул плохой HTML для %s — переключаемся на Playwright", url)
html_text = fetch_with_playwright(url, wait_selector=wait_selector)
if html_text:
logging.info("Playwright успешно загрузил %s", url)
else:
logging.error("И requests, и Playwright не справились для %s", url)
return html_text
# ---------- парсинг: помощники ----------
def parse_with_css(soup: BeautifulSoup, css_selector: str) -> Optional[str]:
try:
el = soup.select_one(css_selector)
return el.get_text(strip=True) if el else None
except Exception:
return None
def parse_with_xpath(text: str, xpath_expr: str) -> Optional[str]:
try:
tree = html.fromstring(text)
result = tree.xpath(xpath_expr)
if not result:
return None
if isinstance(result[0], html.HtmlElement):
return result[0].text_content().strip()
return str(result[0]).strip()
except Exception:
return None
def extract_item(html_text: str, css_selector: str, xpath_expr: str) -> Optional[str]:
soup = BeautifulSoup(html_text, "lxml")
val = parse_with_css(soup, css_selector)
if val:
return val
return parse_with_xpath(html_text, xpath_expr)
# ---------- каноникализация DOM + подпись ----------
def canonicalize_html_fragment(html_fragment: str) -> str:
soup = BeautifulSoup(html_fragment, "lxml")
for el in soup.find_all(True):
attrs = dict(el.attrs)
new_attrs = {}
for k, v in attrs.items():
if k.startswith("on"):
continue
if k in ("id", "class"):
if isinstance(v, list):
v = " ".join(v)
# удаляем цифры, чтобы снизить шум динамических id/class
normalized = "".join(ch for ch in v if not ch.isdigit())
new_attrs[k] = normalized
else:
new_attrs[k] = v
el.attrs = new_attrs
text = soup.prettify()
lines = [ln.rstrip() for ln in text.splitlines() if ln.strip()]
return "\n".join(lines)
def signature_of_fragment(html_fragment: str) -> str:
canonical = canonicalize_html_fragment(html_fragment)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
# ---------- хранилище прогресса ----------
def load_snapshot(url_key: str) -> Optional[Dict]:
path = os.path.join(SNAPSHOT_DIR, f"{url_key}.json")
if not os.path.exists(path):
return None
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return None
def save_snapshot(url_key: str, canonical_html: str, sig: str):
path = os.path.join(SNAPSHOT_DIR, f"{url_key}.json")
payload = {"signature": sig, "html": canonical_html, "ts": int(time.time())}
with open(path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
def diff_fragments(old: str, new: str) -> str:
return "\n".join(difflib.unified_diff(old.splitlines(), new.splitlines(), lineterm=""))
def load_progress() -> Dict:
try:
with open(SAVE_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return {"visited": [], "data": []}
except Exception:
return {"visited": [], "data": []}
def save_progress(progress: Dict):
try:
with open(SAVE_FILE, "w", encoding="utf-8") as f:
json.dump(progress, f, ensure_ascii=False, indent=2)
except Exception as e:
logging.error("Не удалось сохранить прогресс: %s", e)
# ---------- мониторинг структуры ----------
def monitor_structure(url: str, html_text: str, css_container: str, url_key: str) -> bool:
soup = BeautifulSoup(html_text, "lxml")
el = soup.select_one(css_container) if css_container else soup.body
if el is None:
logging.warning("Контейнер по селектору '%s' не найден на %s", css_container, url)
send_alert(f"Container selector '{css_container}' not found on {url}")
return False
canonical = canonicalize_html_fragment(str(el))
sig = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
prev = load_snapshot(url_key)
if not prev:
save_snapshot(url_key, canonical, sig)
logging.info("Сохранён исходный снимок для %s", url)
return False
if prev.get("signature") != sig:
old_html = prev.get("html", "")
diff_text = diff_fragments(old_html, canonical)
diff_path = os.path.join(SNAPSHOT_DIR, f"{url_key}_diff_{int(time.time())}.txt")
with open(diff_path, "w", encoding="utf-8") as f:
f.write(diff_text)
save_snapshot(url_key, canonical, sig)
logging.info("Обнаружены изменения структуры %s, diff сохранён в %s", url, diff_path)
send_alert(f"STRUCTURE CHANGE: {url}\nSelector: {css_container}\nDiff saved: {diff_path}")
return True
return False
# ---------- основная логика ----------
def main(urls: List[str], container_selector_for_monitoring: str = "main", wait_selector_for_playwright: str = "body"):
progress = load_progress()
visited = set(progress.get("visited", []))
for url in urls:
if url in visited:
logging.info("Пропускаем уже посещённый: %s", url)
continue
proxy = random.choice(PROXIES) if PROXIES else None
html_text = robust_fetch_html(url, proxy, wait_selector_for_playwright)
if not html_text:
logging.error("Не удалось получить %s ни через requests, ни через Playwright", url)
send_alert(f"Failed to fetch {url} via both requests and Playwright")
visited.add(url)
progress["visited"] = list(visited)
save_progress(progress)
continue
# мониторим структуру
url_key = hashlib.sha1(url.encode("utf-8")).hexdigest()
try:
changed = monitor_structure(url, html_text, container_selector_for_monitoring, url_key)
if changed:
logging.info("Обнаружено изменение структуры на %s", url)
except Exception as e:
logging.exception("Сбой монитора структуры для %s: %s", url, e)
send_alert(f"Structure monitor exception for {url}: {e}")
# пример извлечения: сначала CSS, затем откат на XPath
css_selector = "h1.product-title" # настройте под сайт
xpath_expr = "//h1[contains(@class,'product') or contains(.,'Product')]/text()"
try:
item = extract_item(html_text, css_selector, xpath_expr)
if item:
logging.info("Извлечено с %s : %s", url, item)
progress["data"].append({"url": url, "title": item})
else:
logging.info("Основной метод не сработал для %s; пробуем мета-тег og:title", url)
meta_title = parse_with_xpath(html_text, "//meta[@property='og:title']/@content")
if meta_title:
progress["data"].append({"url": url, "title": meta_title})
else:
send_alert(f"Parsing failed for {url}")
except Exception as e:
logging.exception("Ошибка парсинга для %s: %s", url, e)
send_alert(f"Parsing crash {url}: {e}")
visited.add(url)
progress["visited"] = list(visited)
save_progress(progress)
time.sleep(random.uniform(1.0, 3.0))
logging.info("Скрапинг завершён. Собрано элементов: %d", len(progress.get("data", [])))
telegram_send(f"Scraping finished. Items: {len(progress.get('data', []))}")
# ---------- запуск ----------
if __name__ == "__main__":
seed_urls = [
# замените на реальные URL
"https://example.com/product/1",
"https://example.com/product/2",
]
# контейнер для мониторинга (важный блок для отслеживания структурных изменений)
container_selector = "div#content"
# селектор, появления которого ждёт Playwright, чтобы считать страницу отрендеренной
wait_selector = "div#content"
main(seed_urls, container_selector_for_monitoring=container_selector, wait_selector_for_playwright=wait_selector)
В этом скрипте вы можете задать свой список прокси на ротацию, свои заголовки для user-агента, а также список целевых страниц для парсинга.
Ключевые фишки: скрипт сначала пробует скачать страницу через requests (с retry/backoff), при неудаче автоматически переключается на headless-браузер через Playwright, ждёт появления заданного CSS-селектора и возвращает отрендеренный HTML.
Здесь также есть: логирование, Telegram-уведомления (укажите свои токены), снимки DOM + детект изменений (через функцию diff), сохранение прогресса и базовая ротация прокси/UA. Парсинг изначально осуществляется на основе CSS. Если он не срабатывает, то включается XPath fallback.
Не забудьте установить необходимые библиотеки и зависимости:
pip install requests beautifulsoup4 lxml playwright
Затем проинициализируйте браузер для playwright:
python -m playwright install chromium
Это лишь один из вариантов того, как может выглядеть скрипт парсинга на Python, устойчивый к изменениям в вёрстке.
Заключение
Итак, примерно здесь должно прийти понимание, что недостаточно просто написать самую суть скрипта и начать парсить целевой сайт. Мало найти повторяющийся паттерн в DOM-структуре. Так вы не сможете создать стабильный бизнес-процесс и не добьётесь впечатляющих результатов при больших масштабах сбора данных.
Нужно ещё подумать об отходных путях: логировании, ротации прокси, реагировании на ошибки, автоматизации изменений в URL/DOM-структуре, сохранении прогресса и многом другом.
Чем больше вы предусмотрите, тем стабильнее будет работать ваш скрипт парсинга на Python.
Крупные коммерческие парсеры невозможны без надёжной сетевой инфраструктуры и качественных прокси. И они как раз есть у Froxy. В вашем распоряжении 10+ миллионов IP с точным таргетингом и автоматической ротацией по времени или при каждом новом запросе.