2025, Nov 27 18:02

Почему парсер внезапно возвращает None и как стабилизировать парсинг при серверных лимитах

Почему при парсинге каталога ответы «ломаются»: серверные ограничения и антибот‑фильтры. Практика: паузы, ротация User-Agent, ретраи, Selenium и прокси.

При парсинге каталога страниц с книгами всё может работать исправно на десятках URL, а затем внезапно ломаться: парсер возвращает None, HTML выглядит повреждённым, а точка отказа сдвигается от запуска к запуску. Это классический признак серверных ограничений — лимитов по частоте, CAPTCHA или антибот‑фильтров, а не проблема BeautifulSoup или самого парсера.

Минимальный пример, воспроизводящий поведение

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

import requests
from bs4 import BeautifulSoup
def grab_title(page_url):
    req_headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) "
            "Gecko/20100101 Firefox/108.0"
        )
    }
    resp = requests.get(page_url, headers=req_headers)
    dom = BeautifulSoup(resp.content, "html.parser")
    h1node = dom.find("h1", class_="book__title")
    book_name = h1node.text.strip() if h1node else "Title not found"
    return book_name
# Где-то в вызывающем коде
idx = 0
for row in catalog_links:  # например, ["https://example.com/booknumber12/", ...]
    idx += 1
    href = row[0]
    print(href + "  " + str(idx))
    outcome = grab_title(href)
    if outcome is None:
        print(f"Values niestety none dla: {href} numer {idx}")
        break

Что на самом деле происходит и почему

Причина сбоев не в HTML‑парсере. После множества запросов тело ответа начинает приходить в искажённом виде — это соответствует моменту, когда включаются серверные защиты. Характерный признак: сырой контент ответа уже «сломанный» до парсинга — смесь заглавных и строчных «абракадабр» или неожиданная разметка вместо ожидаемой структуры; в таком виде BeautifulSoup не находит узел заголовка. Сначала проверяйте полезную нагрузку ответа — это подтверждает источник проблемы и переключает внимание с парсера на сам ответ.

Практические способы снизить риски и стабилизировать парсинг

Самый действенный способ реже ловить ограничения — замедлиться и варьировать «отпечаток» запроса. На практике это означает паузы между запросами, ротацию значений User-Agent и проверку ответа перед парсингом. Если сайт выдает JavaScript‑челленджи, поможет загрузка страниц в безголовом браузере. Если дросселирование сохраняется, можно подключить прокси и ротировать IP. Простой механизм повторных попыток тоже спасает от кратковременных сбоев. Помните: фиксированная задержка — не серебряная пуля и поодиночке ненадёжна, а один лишь безголовый браузер не преодолеет жёсткие серверные ограничения.

Ниже — правки, которые добавляют задержки, ротацию User-Agent на каждый запрос и защитные проверки до передачи HTML в BeautifulSoup.

import time
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
def fetch_book_title(target_url):
    ua = UserAgent()
    dyn_headers = {"User-Agent": ua.random}
    try:
        resp = requests.get(target_url, headers=dyn_headers, timeout=10)
        resp.raise_for_status()
        # Быстрая проверка на неожиданный контент
        if "book__title" not in resp.text:
            print(f"Unexpected content from {target_url}")
            return None
        dom = BeautifulSoup(resp.content, "html.parser")
        h1node = dom.find("h1", class_="book__title")
        return h1node.text.strip() if h1node else "Title not found"
    except Exception as exc:
        print(f"Error fetching {target_url}: {exc}")
        return None
# Вежливо итерируемся по каталогу
position = 0
for entry in link_list:
    position += 1
    page_href = entry[0]
    print(page_href + "  " + str(position))
    page_title = fetch_book_title(page_href)
    if page_title is None:
        print(f"Values niestety none dla: {page_href} numer {position}")
        break
    time.sleep(1.5)  # Подстройте под «терпимость» сайта

Ротация User-Agent использует fake_useragent. Установите пакет заранее:

pip install fake-useragent

Если сайт проверяет небраузерных клиентов через JavaScript, безголовый браузер может загрузить страницу как обычный браузер, а затем передать полученный HTML в BeautifulSoup для извлечения данных. Подход тяжелее и медленнее, но помогает там, где содержимое появляется только после клиентского выполнения скриптов.

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
chrome_opts = Options()
chrome_opts.add_argument("--headless")
driver = webdriver.Chrome(options=chrome_opts)
def render_and_extract(page_url):
    driver.get(page_url)
    html_doc = driver.page_source
    dom = BeautifulSoup(html_doc, "html.parser")
    h1node = dom.find("h1", class_="book__title")
    return h1node.text.strip() if h1node else "Title not found"

Когда сервер блокирует по IP, маршрутизация через прокси распределяет трафик и снижает нагрузку на один адрес. Логика парсинга при этом не меняется — меняется только сетевой путь.

proxy_cfg = {
    "http": "http://user:pass@proxy_ip:port",
    "https": "http://user:pass@proxy_ip:port",
}
ua = UserAgent()
rot_headers = {"User-Agent": ua.random}
resp = requests.get("https://example.com", headers=rot_headers, proxies=proxy_cfg, timeout=10)

Почему важно понимать этот класс сбоев

Когда скрейпер начинает сбоить прерывисто — после множества успешных запросов — хочется сменить HTML‑парсер или подправить селекторы. В описанной ситуации это не поможет, потому что проблема возникает до парсинга: сервер меняет то, что вы получаете. Понимание серверных контролей позволяет сосредоточиться на темпе запросов, рандомизации «идентичности» и валидации ответов, а не гоняться за мнимыми багами парсера.

Выводы

Сначала подтвердите проблему, изучив сырой ответ. Если полезная нагрузка уже повреждена, замедляйте запросы и рандомизируйте заголовки, чтобы выглядеть менее «ботоподобно». Добавьте простую проверку содержания и обрабатывайте None на ранней стадии, вместо того чтобы отправлять в парсер испорченный HTML. Рассматривайте безголовый браузер только там, где контент требует выполнения JavaScript, и помните: фиксированные паузы не гарантируют успех. Если сайт всё ещё сопротивляется, прокси и аккуратные ретраи помогут стабилизировать запуск, уважая ограничения целевого ресурса.