Работа с URL: ссылки и параметры пагинации

Скрейпер постоянно собирает URL и разбирает их параметры — это отдельный навык.
Модуль urllib.parse умеет разбирать query-string в словарь, собирать URL обратно и превращать относительные ссылки (/page2) в абсолютные. Без этого нельзя обходить каталоги.

Найдя на странице ссылки, ты часто получаешь их в относительном виде: href="/catalog/123". Чтобы перейти по ним, нужен полный адрес. А чтобы листать страницы, нужно уметь менять параметр page в query-string. Всё это решает стандартный urllib.parse — и он работает в браузере.

Относительные ссылки → абсолютные

Функция urljoin соединяет базовый URL страницы с относительной ссылкой, как это делает браузер:

Попробуй сам ▶

from urllib.parse import urljoin

base = 'https://shop.example/catalog/page2'

links = ['/product/1', '../about', 'page3', 'https://ext.io/x']
for link in links:
    print(f'{link:20} -> {urljoin(base, link)}')

Сборка query-string для пагинации

Чтобы листать каталог, скрейпер меняет параметры и собирает URL заново через urlencode:

Попробуй сам ▶

from urllib.parse import urlencode, urlparse, parse_qs, urlunparse

url = 'https://shop.example/catalog?sort=price&page=1'
p = urlparse(url)
params = parse_qs(p.query)

# построим адреса страниц 1..3
for page in range(1, 4):
    params['page'] = [str(page)]
    query = urlencode(params, doseq=True)
    new = urlunparse(p._replace(query=query))
    print(new)

Как работает под капотом

urlparse раскладывает URL на шесть частей (схема, хост, путь, параметры, query, фрагмент). parse_qs превращает page=1&sort=price в словарь со списками значений. После изменения urlencode и urlunparse собирают всё обратно в корректный адрес — с правильным экранированием спецсимволов и кириллицы. Это надёжнее ручной склейки строк через +.

Нормализация URL и борьба с дублями

При обходе сайта один и тот же ресурс встречается под разными адресами: с якорем #section и без, с разным порядком query-параметров, со слешем на конце и без. Если не нормализовать URL, скрейпер будет ходить по одной странице многократно — это и лишняя нагрузка на сайт, и дубли в данных. Нормализация приводит адреса к каноничному виду: убрать фрагмент #, отсортировать параметры, привести хост к нижнему регистру, решить вопрос с завершающим слешем.

Связанная задача — оставаться в пределах одного домена. Перейдя по ссылке, легко случайно «уйти» на внешний сайт и начать скрейпить чужой ресурс без спроса. Поэтому после urljoin сравнивают netloc полученного URL с целевым доменом и отбрасывают чужие ссылки. Множество уже посещённых адресов (set) плюс проверка домена — это, по сути, ручная реализация того, что Scrapy делает автоматически своим планировщиком с дедупликацией. Понимая механику, ты осознанно контролируешь, куда именно ходит твой бот.

Частые ошибки

  • Склеивать URL вручную. base + '/' + link ломается на ../, лишних слешах и абсолютных ссылках. urljoin учитывает все случаи.
  • Не экранировать параметры. Пробелы и кириллица в query ломают запрос; urlencode кодирует их правильно.
  • Забывать про doseq=True. Без него списки значений кодируются криво.

Best practices

  • Любую относительную ссылку прогоняй через urljoin(base, link).
  • Для построения URL с параметрами используй urlencode, а не конкатенацию строк.
  • Нормализуй URL (убирай дубли, якоря #) перед добавлением в очередь обхода.

Когда обход становится большим, очередь URL и множество посещённых адресов лучше держать не только в памяти, но и на диске или в базе — чтобы прерванный краулинг можно было продолжить, а не начинать заново и снова грузить сайт. Это та же идея, что заложена в планировщик Scrapy с его персистентной очередью. Даже если ты пишешь обход вручную на requests, полезно с самого начала думать о нём как о системе «очередь → посещено → результаты», а не как о простом цикле: так код легче масштабировать и делать вежливым.

Ещё одна частая тонкость — относительные ссылки бывают разных видов: начинающиеся со слеша (/catalog, от корня сайта), с двумя точками (../page, на уровень выше), без префикса (page3, относительно текущей папки) и протокол-относительные (//cdn.example/img). Самостоятельно учитывать все эти случаи утомительно и легко ошибиться, поэтому единая функция urljoin и нужна: она реализует ровно ту же логику разрешения адресов, что и браузер, по стандарту. Доверяя ей, ты получаешь корректные абсолютные URL во всех ситуациях и не плодишь тонких багов в обходе сайта.

Итог: работа с URL — рутина скрейпинга. urljoin превращает относительные ссылки в абсолютные, а urlencode/urlunparse аккуратно собирают адреса для пагинации.

Проверьте себя
1. Что делает функция urljoin(base, link)?
AУдаляет ссылку
BПревращает относительную ссылку в абсолютную относительно базового URL
CШифрует URL
DСчитает количество ссылок
2. Почему собирать URL с параметрами лучше через urlencode, а не конкатенацией строк?
Aurlencode быстрее интернета
Burlencode правильно экранирует пробелы и кириллицу, а ручная склейка ломает запрос
CКонкатенация запрещена в Python
DРазницы нет