2025, Nov 01 07:17

Стабильный обход ссылок в Selenium: собираем href, избегаем StaleElementReference

Как стабильно пройтись по списку ссылок в Selenium на Python: соберите href заранее, избегайте StaleElementReferenceException и ошибок WebDriverWait. Пример.

Пробежаться по коллекции WebElement в Selenium, перейти по каждой ссылке и забрать крошку данных — кажется пустяком, пока в игру не вступает навигация. Как только браузер уходит со страницы, ранее найденные элементы теряют актуальность, а наивные стратегии ожидания быстро рассыпаются с ошибками TypeError или StaleElementReferenceException. Ниже — наглядный разбор сбоя и устойчивое решение.

Постановка задачи

Вы начинаете с функции, которая открывает страницу со списком, проходит по карточкам, заходит в ссылку внутри каждой карточки, переходит на страницу детали, достает дату и повторяет цикл. Код выглядит примерно так:

def pull_posting_dates(browser, start_url):
    date_bins = []
    waiter = WebDriverWait(browser, 5)
    browser.get(start_url)
    # cards — список веб‑элементов
    cards = browser.find_elements(By.CLASS_NAME, 'object-cards-block.d-flex.cursor-pointer')
    for card in cards:
        try:
            anchor = waiter(card, 5).until(
                EC.presence_of_element_located(
                    (By.CSS_SELECTOR, 'div.main-container-margins.width-100 > a')
                )
            )
        except NoSuchElementException:
            print('no such element')
        # follow the link
        browser.get(anchor.get_attribute('href'))
        stamp = browser.find_element(
            By.XPATH, '//*[@id="Data"]/div/div/div[4]/div[2]/span'
        ).text
        date_bins.append(stamp)

Почему все ломается

Первый затык — TypeError: WebDriverWait is not callable. Передача WebElement в WebDriverWait вроде waiter(card, 5) пытается вызвать объект ожидания как функцию, а это не так. Правильный шаблон: создать WebDriverWait с драйвером и таймаутом, затем вызывать until с ожидаемым условием. Попытка «привязать» ожидание к конкретному элементу таким образом и приводит к ошибке о вызове объекта.

Второй, более принципиальный момент — навигация. Как только вызывается get(), «вид» драйвера на прежнюю страницу исчезает. Selenium хранит ссылки на DOM-узлы в памяти браузера. Переход на новую страницу полностью заменяет DOM, делая накопленные ссылки с прошлого экрана недействительными. Отсюда StaleElementReferenceException после первого перехода и провал повторного использования элементов, собранных до вызова get(). Даже без полной навигации, существенные изменения DOM заставляют искать элементы заново — их позиции в памяти меняются.

Решение: сначала собрать href, затем заходить на каждую страницу

Вариантов несколько, но самый простой — заранее извлечь все атрибуты href, не уходя со страницы. Сохранив ссылки в список Python, можно безопасно итерироваться и вызывать get() для каждой, дожидаясь нужного элемента на странице детали. Подход прямолинейный, надежный и не использует «протухшие» ссылки на элементы. Это не самый быстрый паттерн, зато он прозрачен и работает стабильно.

from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


SOURCE_URL = "https://upn.ru/kupit/kvartiry"


def scrape_post_dates(session, entry_url):
    session.get(entry_url)
    gate = WebDriverWait(session, 10)
    predicate = EC.presence_of_all_elements_located
    container = (By.CLASS_NAME, "object-cards-block.d-flex.cursor-pointer")

    links = []
    for node in gate.until(predicate(container)):
        a_tag = node.find_element(
            By.CSS_SELECTOR, "div.main-container-margins.width-100 > a"
        )
        href_val = a_tag.get_attribute("href")
        if href_val is not None:
            links.append(href_val)

    stamps = []
    for link_url in links:
        session.get(link_url)
        single_pred = EC.presence_of_element_located
        spot = (By.XPATH, "//*[@id='Data']/div/div/div[4]/div[2]/span")
        target = gate.until(single_pred(spot))
        txt = target.text
        stamps.append(txt)
        print(txt)

    return stamps


if __name__ == "__main__":
    opts = ChromeOptions()
    opts.add_argument("--headless=new")
    with Chrome(options=opts) as drv:
        results = scrape_post_dates(drv, SOURCE_URL)

Пример вывода (фрагмент):

Размещено: 16.06.2025 11
Размещено: 06.06.2025 6
Размещено: 02.06.2025 57
Размещено: 19.04.2025 42
Размещено: 03.04.2025 29
Размещено: 25.03.2025 63
...

Почему это важно

Критично понимать, как Selenium отслеживает DOM-узлы. После навигации или заметного обновления DOM старые ссылки теряют силу, потому что структура страницы была заменена. Надежный прием — сначала извлечь примитивные данные вроде URL или ID, затем переходить и заново находить нужное на новой странице. Это делает парсер устойчивым, уменьшает «флапающие» ожидания и локализует отказы.

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

Итоги

Не вызывайте WebDriverWait как функцию и не «прикрепляйте» его к WebElement. Используйте один объект ожидания на контекст драйвера и передавайте условия в until. Не переиспользуйте элементы после навигации; вместо этого соберите href заранее и итерируйтесь по ним, каждый раз находя нужное на целевой странице. С таким паттерном вы избавляетесь от TypeError, обходите «протухшие» ссылки и делаете скрапер предсказуемым. Если позже упремся в производительность, поверх этого подхода можно добавить конкуррентность.

Статья основана на вопросе с сайта StackOverflow от Burtsev и ответе от Ramrab.