2025, Oct 08 03:18

Надёжный парсинг пагинатора DataTables в Selenium: стратегия Next

Почему клики по индексам пагинатора в Selenium, дают сбои и как стабильно собирать все страницы DataTables через кнопку Next. Пример: дивиденды Vale.

Парсинг постраничных таблиц в Selenium кажется простым — пока не проявятся особенности самого виджета. На странице дивидендов Vale по адресу https://investidor10.com.br/acoes/vale3/ пагинация состоит из числовых кнопок и переключателей Next/Previous. Переход по числовому индексу работает на первых страницах, но попытка нажать на idx="5" неожиданно переносит на idx="8", из‑за чего строки с 6 и 7 страниц остаются пропущенными. К тому же время от времени всплывает NoSuchElementError, хотя целевой элемент виден в DOM.

Как воспроизвести проблему

Следующий фрагмент опирается на прямые клики по числовым ссылкам пагинатора через атрибут data-dt-idx. Скрипт прокручивает страницу, нажимает нужный номер, ждёт появления таблицы и передаёт управление процедуре парсинга.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep
def loop_pagers():
    tabs = browser.find_elements(By.CSS_SELECTOR, "a[data-dt-idx]")
    total = len(tabs)
    for k in range(total):
        hit_pager(str(k + 1))
def hit_pager(idx):
    try:
        locator = (By.CSS_SELECTOR, f'a[data-dt-idx="{idx}"]')
        pager = WebDriverWait(browser, 10).until(EC.presence_of_element_located(locator))
        browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        sleep(1)
        browser.execute_script("arguments[0].scrollIntoView({behavior:'instant', block:'center' });", pager)
        browser.execute_script("arguments[0].click();", pager)
        WebDriverWait(browser, 10).until(EC.presence_of_element_located((By.ID, "table-dividends-history")))
        harvest_grid()  # процедура парсинга
    except Exception as err:
        print(f"Failed to execute function: {err}")

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

Элемент пагинации ведёт себя нестабильно при кликах по числовому idx. На практике клик по idx="5" переносит к idx="8", из‑за чего данные со страниц 6 и 7 пропускаются. Кроме того, код порой выбрасывает NoSuchElementError, даже когда кнопка присутствует на странице, что указывает на динамический ререндеринг и кратковременные проблемы с кликабельностью. Проще говоря, стратегия с числовыми индексами для этого виджета ненадёжна.

Практичный способ собрать все страницы

Более устойчивый подход здесь — собрать все видимые строки на первой странице и затем переходить по одной странице вперёд через кнопку Next, пока она не исчезнет. Схема начинается с перехода на https://investidor10.com.br/acoes/vale3/, ожидания видимости секции дивидендов (id="dividends-section"), прокрутки обёртки таблицы в область видимости и чтения заголовка из блока dataTables_scroll.

Далее код парсит первую страницу и входит в цикл. В каждой итерации он пытается нажать Next, идентифицируя её селектором #table-dividends-history_paginate > a.paginate_button.next. После успешного клика делается короткая пауза для обновления таблицы, затем считываются новые строки. Если кнопку Next найти не удаётся, цикл завершается — значит, все страницы обработаны. Если клик перехвачен, попытка без лишнего шума будет повторена на следующей итерации. В конце все строки собираются в pandas DataFrame с найденными заголовками.

import time
import pandas as pd
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.common.exceptions import NoSuchElementException, ElementClickInterceptedException
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
rows_agg = []
def harvest_rows(tbl):
    table_rows = tbl.find_elements(By.CSS_SELECTOR, "div.dataTables_scrollBody>table>tbody>tr")
    for row in table_rows:
        rows_agg.append([d.text for d in row.find_elements(By.TAG_NAME, 'td')])
chrome_cfg = ChromeOptions()
chrome_cfg.add_argument("--start-maximized")
chrome_cfg.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_cfg.add_experimental_option("useAutomationExtension", False)
browser = Chrome(options=chrome_cfg)
waiter = WebDriverWait(browser, 10)
target_url = "https://investidor10.com.br/acoes/vale3/"
browser.get(target_url)
waiter.until(EC.visibility_of_element_located((By.ID, "dividends-section")))
widget_wrap = browser.find_element(By.ID, "table-dividends-history_wrapper")
browser.execute_script("arguments[0].scrollIntoView(true);", widget_wrap)
grid_box = widget_wrap.find_element(By.CSS_SELECTOR, "div.dataTables_scroll")
headers = grid_box.find_element(By.CSS_SELECTOR, "div.dataTables_scrollHead").text.split('\n')
print(f"Table Header {headers}")
print("Extracting Page 1...")
harvest_rows(grid_box)
page_counter = 2
has_next = True
while has_next:
    try:
        next_btn = widget_wrap.find_element(By.CSS_SELECTOR, '#table-dividends-history_paginate>a[class="paginate_button next"]')
        try:
            next_btn.click()
            time.sleep(1)
            print(f"Extracting Page {page_counter}...")
            harvest_rows(grid_box)
            page_counter += 1
        except ElementClickInterceptedException:
            pass
    except NoSuchElementException:
        print("Reached End Page")
        has_next = False
# собрать и показать таблицу
df = pd.DataFrame(rows_agg, columns=headers)
print(df)

В примерном запуске лог фиксирует извлечение каждой страницы и сообщает о достижении финальной. Затем выводится единый DataFrame со всеми строками из всех страниц под обнаруженными столбцами.

Заголовок таблицы ['TIPO', 'DATA COM', 'PAGAMENTO', 'VALOR']
Извлечение страницы 1...
Извлечение страницы 2...
Извлечение страницы 3...
Извлечение страницы 4...
Извлечение страницы 5...
Извлечение страницы 6...
Извлечение страницы 7...
Извлечение страницы 8...
Достигнута последняя страница

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

Пагинаторы, которые динамически перестраиваются, меняют подписи или ререндерятся, плохо сочетаются с кликами по индексам. Итерация через Next исключает «скачки» по idx и гарантирует, что каждая страница будет посещена ровно один раз. Заодно упрощается сбор данных — можно переиспользовать один и тот же контекст элемента для чтения строк. Помните, что фиксированная пауза в одну секунду после клика — это лишь лучшая попытка и может быть ненадёжной при колебаниях скорости сети или отрисовки.

Выводы

Если пагинатор непредсказуемо реагирует на клики по числовым индексам, переходите к детерминированной стратегии обхода. Загрузите нужную секцию, прокрутите таблицу в область видимости, соберите заголовки и строки с первой страницы и продвигайтесь через Next, пока кнопка не исчезнет. По мере продвижения агрегируйте результаты и сформируйте финальный DataFrame. Особое внимание уделяйте «жёстким» задержкам во время пагинации — при плавающих временах загрузки они могут стать слабым звеном.

Статья основана на вопросе с StackOverflow от user30126350 и ответе Ajeet Verma.