- Intro
- Dynamic vs Hardcoded Pagination
- Prerequisites
- Types of pagination
- Token Pagination
  - Dynamic Pagination with Token based Websites
- Non Token Pagination
  - Dynamic Pagination with Non Token based Websites
- Click Pagination
  - Dynamic Pagination with Clicks
- Scroll or JavaScript Evaluation
  - Dynamic Pagination with Scrolls
- Conclusion

вступление

В этом посте блога будут рассмотрены наиболее распространенные методы нумерации страниц, которые можно применять для выполнения динамической нумерации страниц на любом веб-сайте. Этот пост в блоге продолжается и будет обновляться, если будут обнаружены новые методы.

Динамическая и жестко запрограммированная нумерация страниц

Что такое динамическая нумерация страниц?

Что ж, это просто способ разбиения на страницы всех доступных страниц, когда вы не знаете, сколько их, он просто просматривает их все:

while True:
    requests.get('<website_url>')
    # data extraction code

    # condition to paginate to the next page or to exit pagination

Жестко закодированный подход отличается явным указанием количества N страниц, которые мы хотим разбить на страницы:

# hardcoded way to paginate from 1 to 25th page
for page_num in range(1, 26):
    requests.get('<website_url>')
    # data extraction code

Это простой подход, если нам нужно извлечь данные из N страниц. Но что, если нам нужно извлечь все страницы из нескольких, скажем, категорий на одном сайте, и если каждая категория содержит разное количество страниц?

Дело в том, что при использовании жестко закодированного подхода мы придем к тому, что нам нужно обновить номера страниц, чтобы они соответствовали требованиям для каждой страницы, что не особенно удовлетворяет :-)

Условие выхода из динамической разбивки на страницы

Давайте также остановимся на секунду и посмотрим, в чем еще разница — динамическая while True нумерация страниц и for page_num in range(...) подход.

Вы заметили комментарий в динамической нумерации страниц: «условие перехода на следующую страницу или выхода из пагинации»?

Это означает, что всякий раз, когда мы используем динамическую разбивку на страницы, нам всегда нужно какое-то условие для выхода из бесконечного цикла. Это может быть: исчез элемент, номер предыдущей страницы отличается от текущего, высота элементов одинаковая и т. д.

Предпосылки

Если вы хотите попробовать, давайте сначала создадим отдельную среду.

Если вы используете Linux:

python -m venv env && source env/bin/activate

Если вы работаете в Windows и используете Git Bash:

python -m venv env && source env/Scripts/activate

Затем установите необходимые библиотеки, если хотите попробовать сами:

$ pip install requests bs4 parsel playwright
  • requests: сделать запрос на сайт.
  • bs4: Парсер HTML.
  • parsel: еще один парсер HTML, более быстрый, чем bs4, используемый в нескольких примерах.
  • playwright: автоматизация современного браузера.

Для playwright, если вы работаете в Linux, вам также необходимо установить дополнительные зависимости:

$ sudo apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libatspi2.0-0 libwayland-client0

После этого нам нужно установить хром (или другие браузеры):

$ playwright install chromium

Типы пагинации

Существует четыре наиболее распространенных типа пагинации:

  1. пагинация токенов с использованием уникального токена. Например: SAOxcijdaf#Ad21
  2. нумерация страниц без токенов с использованием цифр. Например: 1,2,3,4,5.
  3. щелкните постраничную навигацию. Например: нажимайте кнопку перехода на следующую страницу, пока кнопка не исчезнет.
  4. прокрутка или разбиение на страницы с оценкой JavaScript. Например: прокручивать страницу до тех пор, пока не останется отзывов. То же самое можно сделать (оценить) с кодом JS.

📌Эти типы пагинации можно комбинировать с теми или иными.

Например, значения нумерации страниц без токена и токена должны обновляться одновременно для перехода на следующую страницу. Вот как работает разбиение на страницы профиля Академии Google без использования автоматизации браузера.

Еще один пример комбинированной пагинации — объединение прокрутки с кликами по мере необходимости (когда появляется определенная кнопка).

Пагинация токенов

Пагинация токенов — это когда веб-сайт генерирует токен, отвечающий за получение данных следующей страницы. Это может выглядеть примерно так: 27wzAGn-__8J.

Этот токен, скорее всего, будет передан как параметр URL, например, на странице профилей Google Scholar это выглядит так:

#                                                     ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
https://scholar.google.com/citations?mauthors=biology&after_author=27wzAGn-__8J

В некоторых случаях этот токен необходимо комбинировать с другими параметрами. Например, страница профиля Google Scholar имеет:

#                                                     ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
https://scholar.google.com/citations?mauthors=biology&after_author=27wzAGn-__8J&astart=10

Динамическое разбиение на страницы с веб-сайтами на основе токенов

Динамическое разбиение на страницы на веб-сайтах на основе токенов происходит путем анализа токена следующей страницы, который может находиться в:

Вот фрагмент кода из моего ответа StackOverflow о разбиении на страницы всех профилей Google Scholar в Python:

from bs4 import BeautifulSoup
import requests, lxml, re

params = {
    "view_op": "search_authors", # profiles tab
    "mauthors": "blizzard",      # search query
    "hl": "en",                  # language of the search
    "astart": 0                  # page number
}

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
}

authors_is_present = True
while authors_is_present:
    html = requests.get("https://scholar.google.com/citations", params=params, headers=headers, timeout=30)
    soup = BeautifulSoup(html.text, "lxml")

    for author in soup.select(".gs_ai_chpr"):
        name = author.select_one(".gs_ai_name a").text
        link = f'https://scholar.google.com{author.select_one(".gs_ai_name a")["href"]}'
        affiliations = author.select_one(".gs_ai_aff").text
        email = author.select_one(".gs_ai_eml").text
        try:
            cited_by = re.search(r"\d+", author.select_one(".gs_ai_cby").text).group() # Cited by 17143 -> 17143
        except: cited_by = None

        print(f"extracting authors at page #{params['astart']}.",
                name,
                link,
                affiliations,
                email,
                cited_by, sep="\n")

    # if next page token exists, we extract next page token form HTML node attribute
    # and increment `astart` parameter +10
    if soup.select_one("button.gs_btnPR")["onclick"]:
        params["after_author"] = re.search(r"after_author\\x3d(.*)\\x26", str(soup.select_one("button.gs_btnPR")["onclick"])).group(1)  # -> XB0HAMS9__8J
        params["astart"] += 10
    else:
        authors_is_present = False

Разбивка на страницы без токенов

Разбиение на страницы без токенов — это просто увеличение номера страницы на число N. Его можно увеличить на 1, 10 (поиск Google), 11 (поиск Bing), 100 (авторские статьи Google Scholar) или другое число в зависимости от того, как работает разбиение на страницы на определенном веб-сайте.

Как упоминалось выше, разбиение на страницы без токенов может быть объединено с токеном для выполнения разбиения на страницы.

Динамическое разбиение на страницы с веб-сайтами без токенов

Определить, использует ли веб-сайт разбивку на страницы без токенов, просто. Следите за параметрами URL, посмотрите, есть ли какие-либо цифры, связанные с параметрами URL, и посмотрите, меняются ли они.

Например, в поиске Google есть параметр start:

# first page (no start parameter, or could be manually set to 0, first page)
https://www.google.com/search?q=sushi

# second page                         ▼▼▼▼▼▼▼▼
https://www.google.com/search?q=sushi&start=10

# third page                          ▼▼▼▼▼▼▼▼
https://www.google.com/search?q=sushi&start=20

Примером кода разбивки на страницы без токенов являются результаты поиска Google. Следующий код очищает все результаты со всех страниц.

from bs4 import BeautifulSoup
import requests, lxml

# https://docs.python-requests.org/en/master/user/quickstart/#passing-parameters-in-urls
params = {
    "q": "sushi",       # search query
    "hl": "en",         # language
    "gl": "us",         # country of the search, US -> USA
    "start": 0,         # number page by default up to 0
}

# https://docs.python-requests.org/en/master/user/quickstart/#custom-headers
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
}

page_num = 0
while True:
    page_num += 1
    print(f"{page_num} page:")

    html = requests.get("https://www.google.com/search", params=params, headers=headers, timeout=30)
    soup = BeautifulSoup(html.text, 'lxml')

    for result in soup.select(".tF2Cxc"):
        title = f'Title: {result.select_one("h3").text}'
        link = f'Link: {result.select_one("a")["href"]}'
        try:
            description = f'Description: {result.select_one(".VwiC3b").text}'
        except: 
            description = None
        print(title, link, description, sep="\n", end="\n\n")

    # if arrow button with attribute 'pnnext' is present -> paginate
    if soup.select_one('.d6cvqb a[id=pnnext]'):
        params["start"] += 10
    else:
        break

Щелкните Разбиение на страницы

Как вы понимаете, он выполняет щелчок и может использоваться только с автоматизацией браузера, такой как playwright или selenium, потому что эти библиотеки предоставляют метод для нажатия на заданный элемент.

Динамическая пагинация с кликами

Все, что нам нужно сделать, это найти кнопку или любой другой элемент, отвечающий за кнопку следующей страницы, с помощью селектора CSS или XPath.

После этого нам нужно выполнить метод click():

# page = playwright.chromium.launch(headless=True).new_page()
# page.goto('<website_url>')
page.query_selector('.Jwxk6d .u4ICaf button').click(force=True)

Прокрутка или оценка JavaScript

Техника прокрутки страниц требует прокрутки для выполнения разбиения на страницы. Прокрутки могут быть либо сверху вниз, либо из стороны в сторону в зависимости от того, как работает веб-сайт.

Динамическая пагинация со скроллингом

Есть три частых метода, которые я использую для выполнения прокрутки с playwright или selenium:

  1. page.keyboard.press(<key>)
  2. page.evaluate(<JS_code>)
  3. page.mouse.wheel(<scrollX>, <scrollY>)

Нажатие кнопки на клавиатуре для прокрутки вниз:

# page = playwright.chromium.launch(headless=True).new_page()
# page.goto('<website_url>')
page.keyboard.press('END') # scrolls to possible end of the page

Оценка кода JavaScript для выполнения прокрутки вниз:

# page = playwright.chromium.launch(headless=True).new_page()
# page.goto('<website_url>')
page.evaluate("""let scrollingElement = (document.scrollingElement || document.body);
              scrollingElement.scrollTop = scrollingElement scrollHeight;""")

📌Мы должны помнить, что всякий раз, когда мы используем прокрутку страницы, нам всегда нужно выполнять проверку условия, которая проверяет высоту определенного элемента до и после прокрутки.

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

last_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop')  # 2200

while True:
  print("scrolling..")
  page.keyboard.press("End")
  time.sleep(3)
  new_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop') # 2800
  if new_height == last_height:
      break
  else:
      last_height = new_height

Вот полный пример из одного из моих сообщений в блоге с пошаговым объяснением очистки всех обзоров приложений Google Play на Python, который использует щелчок, нажатие клавиатуры и оценку для проверки текущей высоты:

import time, json, re
from parsel import Selector
from playwright.sync_api import sync_playwright

def run(playwright):
    page = playwright.chromium.launch(headless=True).new_page()
    page.goto("https://play.google.com/store/apps/details?id=com.collectorz.javamobile.android.books&hl=en_GB&gl=US")

    user_comments = []

    # if "See all reviews" button present
    if page.query_selector('.Jwxk6d .u4ICaf button'):
        print("the button is present.")
        print("clicking on the button.")
        page.query_selector('.Jwxk6d .u4ICaf button').click(force=True)

        print("waiting a few sec to load comments.")
        time.sleep(4)
        last_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop')  # 2200

        while True:
            print("scrolling..")
            page.keyboard.press("End")
            time.sleep(3)
            new_height = page.evaluate('() => document.querySelector(".fysCi").scrollTop')
            if new_height == last_height:
                break
            else:
                last_height = new_height

    selector = Selector(text=page.content())
    page.close()

    print("done scrolling. Exctracting comments...")
    for index, comment in enumerate(selector.css(".RHo1pe"), start=1):
        comment_likes = comment.css(".AJTPZc::text").get()   

        user_comments.append({
            "position": index,
            "user_name": comment.css(".X5PpBb::text").get(),
            "user_avatar": comment.css(".gSGphe img::attr(srcset)").get().replace(" 2x", ""),
            "user_comment": comment.css(".h3YV2d::text").get(),
            "comment_likes": comment_likes.split("people")[0].strip() if comment_likes else None,
            "app_rating": re.search(r"\d+", comment.css(".iXRFPc::attr(aria-label)").get()).group(),
            "comment_date": comment.css(".bp9Aid::text").get(),
            "developer_comment": {
                "dev_title": comment.css(".I6j64d::text").get(),
                "dev_comment": comment.css(".ras4vb div::text").get(),
                "dev_comment_date": comment.css(".I9Jtec::text").get()
            }
        })

    print(json.dumps(user_comments, indent=2, ensure_ascii=False))

with sync_playwright() as playwright:
    run(playwright)

Пример из другого моего сообщения в блоге, в котором показано, как очистить все результаты Naver Video:

from playwright.sync_api import sync_playwright
import json

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://search.naver.com/search.naver?where=video&query=minecraft")
    video_results = []

    not_reached_end = True
    while not_reached_end:
        page.evaluate("""let scrollingElement = (document.scrollingElement || document.body);
                                 scrollingElement.scrollTop = scrollingElement scrollHeight;""")

        if page.locator("#video_max_display").is_visible():
            not_reached_end = False

    for index, video in enumerate(page.query_selector_all(".video_bx"), start=1):
        title = video.query_selector(".text").inner_text()
        link = video.query_selector(".info_title").get_attribute("href")
        thumbnail = video.query_selector(".thumb_area img").get_attribute("src")
        channel = None if video.query_selector(".channel") is None else video.query_selector(".channel").inner_text()
        origin = video.query_selector(".origin").inner_text()
        video_duration = video.query_selector(".time").inner_text()
        views = video.query_selector(".desc_group .desc:nth-child(1)").inner_text()
        date_published = None if video.query_selector(".desc_group .desc:nth-child(2)") is None else \
            video.query_selector(".desc_group .desc:nth-child(2)").inner_text()

        video_results.append({
            "position": index,
            "title": title,
            "link": link,
            "thumbnail": thumbnail,
            "channel": channel,
            "origin": origin,
            "video_duration": video_duration,
            "views": views,
            "date_published": date_published
        })

    print(json.dumps(video_results, indent=2, ensure_ascii=False))
    browser.close()

В примере с пагинацией Naver взгляните на условие if a, которое выходит из бесконечного цикла:

if page.locator("#video_max_display").is_visible():
    not_reached_end = False

Заключение

  1. Следите за параметрами URL. Если что-то меняется при разбивке на страницы, это может быть признаком того, что эти параметры можно использовать для программного разбиения на страницы.
  2. Попробуйте найти токены следующей страницы в исходном коде страницы.
  3. Если из приведенных выше пунктов ничего не найдено, используйте разбивку по страницам с помощью щелчка или прокрутки, или и то, и другое.

Надеюсь, вы нашли это полезным. Дайте мне знать, если что-то все еще сбивает с толку.

Больше контента на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord.

Заинтересованы в масштабировании запуска вашего программного обеспечения? Посмотрите Цирк.

Мы предлагаем бесплатные консультации экспертов и индивидуальные решения, которые помогут вам повысить осведомленность о вашем технологическом продукте или услуге и принять их.