2026, Jan 09 18:02

Парсинг Google в Selenium: исправляем CSS‑селектор и настраиваем ожидания

Почему Selenium возвращает пустую выдачу Google: исправляем CSS‑селектор, добавляем явные ожидания и user‑agent, проверяем разметку и headless‑режим.

Сбор ссылок из результатов Google, чтобы получить сайты школ и адреса электронной почты, часто кажется простой задачей, пока скрипт вдруг не возвращает пустой список без явной ошибки. Ниже — точный разбор одной из таких ловушек в Selenium и способ её устранить без переделки общей логики парсинга.

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

Нужно выполнить запрос к Google по страницам типа PSHE на доменах .sch.uk и извлечь URL из выдачи. Изначальный скрипт запускает безголовый Chrome, формирует URL поиска и находит ссылки в контейнере результатов. Но на выходе — пусто.

Воспроизводимый код, который не работает

Фрагмент ниже повторяет ключевое поведение исходного подхода, сохраняя логику программы и лишь меняя имена:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
import time

def crawl_google(q, limit=10):
    opts = Options()
    opts.add_argument("--headless")  # Запуск безголового браузера
    opts.add_argument("--disable-blink-features=AutomationControlled")
    opts.add_argument("--no-sandbox")
    opts.add_argument("--disable-dev-shm-usage")

    browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)

    target = f"https://www.google.com/search?q={q}&num={limit}"
    browser.get(target)

    time.sleep(2)

    collected = []
    nodes = browser.find_elements(By.CSS_SELECTOR, 'div.yuRUbf > a')
    for node in nodes:
        href = node.get_attribute('href')
        if href:
            collected.append(href)

    browser.quit()
    return collected

phrase = "PSHE site:.sch.uk"
out = crawl_google(phrase, limit=20)

for idx, link in enumerate(out, 1):
    print(f"{idx}. {link}")

Что на самом деле идёт не так

Локатор использует комбинатор прямого потомка в CSS‑селекторе. Выражение div.yuRUbf > a ищет тег <a>, который является непосредственным ребёнком указанного <div>. На странице выдачи ссылка находится не напрямую внутри, а глубже по дереву. Из‑за отсутствия отношения родитель → прямой потомок выборка ничего не находит, и скрипт молча возвращает пустой список.

Есть и практическая тонкость. Имена классов вроде yuRUbf запутаны и могут меняться, поэтому полагаться на них рискованно. Кроме того, после первого перехода можно упереться в reCaptcha, и тогда ожидаемая разметка просто не загрузится. В таких случаях полезно проверить page_source сразу после навигации или на время отключить безголовой режим — так видно, что именно получает браузер.

Решение: поправить селектор

Переход от комбинатора прямого потомка к комбинатору потомка решает проблему. Селектор div.yuRUbf a находит ссылки, вложенные на любой глубине внутри указанного контейнера.

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
import time

def crawl_google(q, limit=10):
    opts = Options()
    opts.add_argument("--headless")
    opts.add_argument("--disable-blink-features=AutomationControlled")
    opts.add_argument("--no-sandbox")
    opts.add_argument("--disable-dev-shm-usage")

    browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)

    target = f"https://www.google.com/search?q={q}&num={limit}"
    browser.get(target)

    time.sleep(2)

    collected = []
    nodes = browser.find_elements(By.CSS_SELECTOR, 'div.yuRUbf a')
    for node in nodes:
        href = node.get_attribute('href')
        if href:
            collected.append(href)

    browser.quit()
    return collected

phrase = "PSHE site:.sch.uk"
out = crawl_google(phrase, limit=20)

for idx, link in enumerate(out, 1):
    print(f"{idx}. {link}")

Более надёжный вариант той же логики

Стабильность повышают замена произвольных задержек на явные ожидания, отказ от лишней зависимости в пользу Selenium Manager и передача строки user‑agent. Вариант ниже сохраняет то же поведение, но включает эти правки и исправленный селектор:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait


def fetch_serp(q, limit=10):
    chrome_opts = Options()
    chrome_opts.add_argument("--headless")
    chrome_opts.add_argument("--disable-blink-features=AutomationControlled")
    chrome_opts.add_argument("--no-sandbox")
    chrome_opts.add_argument("--disable-dev-shm-usage")
    chrome_opts.add_argument(
        "user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
    )

    browser = webdriver.Chrome(options=chrome_opts)
    page = f"https://www.google.com/search?q={q}&num={limit}"
    browser.get(page)
    browser.maximize_window()
    wait_for = WebDriverWait(browser, 10)

    hrefs = []
    elements = wait_for.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, 'div.yuRUbf a')))
    for el in elements:
        href = el.get_attribute('href')
        if href:
            hrefs.append(href)

    browser.quit()
    return hrefs

term = "PSHE site:.sch.uk"
serp = fetch_serp(term, limit=20)

for idx, u in enumerate(serp, 1):
    print(f"{idx}. {u}")

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

Разница между комбинатором прямого потомка и потомка в записи минимальна, но по эффекту — критична. Одна неверная буква способна обрушить весь шаг извлечения без каких‑либо исключений. Стоит привести локатор в соответствие с реальной структурой DOM — и процесс сразу оживает. Дополнительно: замена фиксированных пауз на ожидания делает прогоны предсказуемее, встроенный Selenium Manager избавляет от лишнего менеджера драйверов, а указание user‑agent помогает там, где цель отвергает стандартную «автоматическую» сигнатуру. При парсинге выдачи не забывайте и о защитных механизмах: разметка может не подгружаться как задумано, поэтому просмотр полученного HTML или временное отключение headless‑режима — практичный способ проверить, что реально видит браузер.

Итоги

Если ваш скрипт для Google возвращает пустой список, в первую очередь проверьте семантику селектора. Когда ссылка не является прямым ребёнком, используйте селектор потомка. Отдавайте предпочтение явным ожиданиям, упростите настройку драйвера встроенным менеджером и передавайте правдоподобный user‑agent. Осторожнее с запутанными именами классов и помните: защитные страницы могут менять то, что получает автоматизация. Часто этих небольших, точечных правок достаточно, чтобы превратить пустую выборку в рабочий парсинг без переделки процесса.