Парсинг URL: что это, зачем нужен и как реализуется на разных языках

Парсинг URL: из чего состоит адрес, как разбирать и собирать ссылки, работать с query-параметрами и кодированием на разных языках.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 17 апреля 2025

Парсинг URL (от англ. parse — «разбирать») — это разбор адреса веб-страницы на отдельные составные части: протокол, домен, порт, путь, параметры запроса и якорь. Из «сырой» строки вида https://example.com/catalog?page=2 мы получаем структурированный объект, к каждому полю которого можно обратиться по отдельности и при необходимости изменить его.

На первый взгляд задача кажется тривиальной — «разбить строку по символам». На практике у URL есть формальная спецификация (RFC 3986) с массой нюансов: кодирование спецсимволов, относительные ссылки, необязательные части адреса, нормализация. Поэтому почти в каждом языке есть штатный инструмент для парсинга URL, и писать собственный разбор регулярными выражениями практически никогда не нужно.

Из чего состоит URL

Разберём максимально «полный» адрес, чтобы увидеть все компоненты:

code
https://user:pass@example.com:8080/catalog/items?page=2&sort=price#reviews
└─┬─┘   └──┬───┘ └────┬────┘└─┬─┘└─────┬──────┘ └──────┬──────┘ └──┬──┘
scheme  userinfo    host    port      path             query     fragment
Компонент Значение в примере Назначение
scheme (схема) https Протокол доступа: http, https, ftp, mailto и т. д.
userinfo user:pass Учётные данные (используется редко, в основном для FTP/basic-auth)
host (хост) example.com Доменное имя или IP-адрес
port (порт) 8080 Порт; если не указан, берётся стандартный для схемы (80 для http, 443 для https)
path (путь) /catalog/items Путь к ресурсу на сервере
query (запрос) page=2&sort=price Параметры запроса в формате «ключ=значение»
fragment (якорь) reviews Ссылка на конкретный раздел внутри страницы; на сервер не отправляется

Именно эти части и извлекает парсер. Дальше с ними можно работать: достать значение конкретного GET-параметра, заменить домен, отбросить якорь, собрать новый URL из изменённых компонентов.

Зачем нужен парсинг URL

Парсинг URL встречается практически в любом приложении, которое работает с веб-адресами. Самые частые сценарии:

  • Извлечение параметров запроса. Нужно понять, на какой странице каталога находится пользователь (?page=5), какой выбран фильтр или UTM-метка. Парсер превращает строку запроса в удобный словарь.
  • Валидация и санитизация ссылок. Прежде чем сохранить пользовательский URL в базу или подставить его в редирект, важно убедиться, что схема разрешена (например, не javascript:), а домен входит в белый список.
  • Маршрутизация. Веб-фреймворки разбирают путь и параметры, чтобы определить, какой обработчик вызвать.
  • Аналитика и логирование. Чтобы сгруппировать обращения по доменам или путям, адреса сначала раскладывают на части.
  • Веб-скрапинг и краулинг. Здесь парсинг URL особенно важен — о нём подробнее ниже.

Парсинг URL при построении краулеров

Краулер (поисковый робот) обходит сайт, переходя по ссылкам со страницы на страницу. Чтобы понять, чем краулинг отличается от скрапинга и собственно парсинга, рекомендуем сначала прочитать наш материал «Скрапинг, парсинг и краулинг — в чём отличия?» . Если коротко: краулер отвечает за обход и обнаружение страниц, скрапер — за извлечение данных, а парсинг — за разбор полученного контента, в том числе и адресов.

Главная «ловушка» при обходе сайта в том, что ссылки в HTML почти всегда относительные, а не абсолютные. В разметке вы встретите:

html
<a href="/about">О компании</a>
<a href="../catalog/phones">Телефоны</a>
<a href="page2.html">Следующая</a>
<a href="#reviews">Отзывы</a>

Робот не может перейти по ссылке /about или page2.html напрямую — это не полноценные адреса. Чтобы продолжить обход, относительную ссылку нужно привести к абсолютному виду, опираясь на адрес текущей страницы (базовый URL). Именно за это отвечает парсинг URL:

Базовый URL страницы Относительная ссылка Результат (абсолютный URL)
https://example.com/catalog/items/ /about https://example.com/about
https://example.com/catalog/items/ ../phones https://example.com/catalog/phones
https://example.com/catalog/items/ page2.html https://example.com/catalog/items/page2.html
https://example.com/catalog/items/ https://cdn.example.com/x https://cdn.example.com/x (уже абсолютная)

Эту операцию называют разрешением ссылки (URL resolution), и почти во всех языках для неё есть готовая функция: urljoin в Python, конструктор new URL(href, base) в JavaScript, URI.resolve() в Java, ResolveReference() в Go.

Нормализация и устранение дублей

Помимо приведения к абсолютному виду, краулеру важно нормализовать адреса, чтобы не обходить одну и ту же страницу несколько раз. Типичные приёмы:

  • отбросить якорь #fragment — он указывает на участок внутри страницы, а не на отдельный ресурс (/page#top и /page — это одна и та же страница);
  • привести схему и домен к нижнему регистру;
  • убрать стандартный порт (:443 для https);
  • упорядочить или отфильтровать параметры запроса (например, выкинуть UTM-метки).

После нормализации абсолютные URL складывают в множество уже посещённых адресов — так робот не зацикливается и не делает лишних запросов.

Примеры реализации парсеров URL

Ниже — как разобрать URL на части и как разрешить относительную ссылку штатными средствами популярных языков.

Python

В стандартной библиотеке за всё отвечает модуль urllib.parse.

python
from urllib.parse import urlparse, urljoin, urldefrag, parse_qs

url = "https://example.com:8080/catalog/items?page=2&sort=price#reviews"
parsed = urlparse(url)

print(parsed.scheme)    # https
print(parsed.hostname)  # example.com
print(parsed.port)      # 8080
print(parsed.path)      # /catalog/items
print(parsed.query)     # page=2&sort=price
print(parsed.fragment)  # reviews

# Параметры запроса -> словарь
print(parse_qs(parsed.query))  # {'page': ['2'], 'sort': ['price']}

# Разрешение относительной ссылки (для краулера)
base = "https://example.com/catalog/items/"
print(urljoin(base, "../about"))    # https://example.com/catalog/about
print(urljoin(base, "/contacts"))   # https://example.com/contacts
print(urljoin(base, "page2.html"))  # https://example.com/catalog/items/page2.html

# Отбросить якорь при нормализации
print(urldefrag("https://example.com/page#reviews")[0])  # https://example.com/page

JavaScript (браузер и Node.js)

Встроенный класс URL работает одинаково и в браузере, и в Node.js. Он же умеет разрешать относительные ссылки — для этого базовый адрес передаётся вторым аргументом.

javascript
const url = new URL("https://example.com:8080/catalog/items?page=2&sort=price#reviews");

console.log(url.protocol);  // "https:"
console.log(url.hostname);  // "example.com"
console.log(url.port);      // "8080"
console.log(url.pathname);  // "/catalog/items"
console.log(url.search);    // "?page=2&sort=price"
console.log(url.hash);      // "#reviews"

// Параметры запроса
console.log(url.searchParams.get("page"));  // "2"

// Разрешение относительной ссылки
const base = "https://example.com/catalog/items/";
console.log(new URL("../about", base).href);   // "https://example.com/catalog/about"
console.log(new URL("/contacts", base).href);  // "https://example.com/contacts"

PHP

Базовый разбор выполняет parse_url(), а строку запроса в массив превращает parse_str().

php
$url = "https://example.com:8080/catalog/items?page=2&sort=price#reviews";
$parts = parse_url($url);

echo $parts['scheme'];    // https
echo $parts['host'];      // example.com
echo $parts['port'];      // 8080
echo $parts['path'];      // /catalog/items
echo $parts['fragment'];  // reviews

parse_str($parts['query'], $query);
print_r($query);          // ['page' => '2', 'sort' => 'price']

В PHP нет встроенного аналога urljoin, поэтому для разрешения относительных ссылок в краулерах обычно подключают библиотеку — например, league/uri или хелперы из Guzzle (GuzzleHttp\Psr7\UriResolver).

Java

В стандартной библиотеке для работы с адресами есть класс java.net.URI. Метод resolve() сразу даёт разрешение относительной ссылки.

java
import java.net.URI;

URI uri = URI.create("https://example.com:8080/catalog/items?page=2&sort=price#reviews");

System.out.println(uri.getScheme());    // https
System.out.println(uri.getHost());      // example.com
System.out.println(uri.getPort());      // 8080
System.out.println(uri.getPath());      // /catalog/items
System.out.println(uri.getQuery());     // page=2&sort=price
System.out.println(uri.getFragment());  // reviews

// Разрешение относительной ссылки
URI base = URI.create("https://example.com/catalog/items/");
System.out.println(base.resolve("../about"));   // https://example.com/catalog/about
System.out.println(base.resolve("/contacts"));  // https://example.com/contacts

Go

Пакет net/url из стандартной библиотеки покрывает и разбор, и разрешение ссылок (ResolveReference).

go
package main

import (
    "fmt"
    "net/url"
)

func main() {
    u, _ := url.Parse("https://example.com:8080/catalog/items?page=2&sort=price#reviews")

    fmt.Println(u.Scheme)               // https
    fmt.Println(u.Hostname())           // example.com
    fmt.Println(u.Port())               // 8080
    fmt.Println(u.Path)                 // /catalog/items
    fmt.Println(u.RawQuery)             // page=2&sort=price
    fmt.Println(u.Query().Get("page"))  // 2

    // Разрешение относительной ссылки
    base, _ := url.Parse("https://example.com/catalog/items/")
    ref, _ := url.Parse("../about")
    fmt.Println(base.ResolveReference(ref))  // https://example.com/catalog/about
}

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

Соберём всё воедино — типичный для краулера шаг: спарсить HTML, достать все ссылки и привести их к абсолютному виду. Пример на Python с requests и BeautifulSoup:

python
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urldefrag

def extract_links(page_url: str) -> set[str]:
    response = requests.get(page_url, timeout=10)
    soup = BeautifulSoup(response.text, "html.parser")

    links = set()
    for a in soup.find_all("a", href=True):
        # 1. приводим относительную ссылку к абсолютной
        absolute = urljoin(page_url, a["href"])
        # 2. отбрасываем якорь, чтобы не плодить дубли
        absolute, _ = urldefrag(absolute)
        # 3. оставляем только http(s)-ссылки
        if absolute.startswith(("http://", "https://")):
            links.add(absolute)

    return links

for link in extract_links("https://example.com/catalog/"):
    print(link)

Здесь парсинг URL выполняет две ключевые задачи: urljoin превращает относительные href в полноценные адреса, по которым робот сможет перейти дальше, а urldefrag помогает не считать /page и /page#top разными страницами.

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

  • Парсинг URL регулярными выражениями. Из-за обилия пограничных случаев в спецификации самописный разбор почти всегда оказывается неполным. Используйте штатные библиотеки.
  • Забыть про относительные ссылки. Без приведения к абсолютному виду краулер «спотыкается» на первой же внутренней ссылке.
  • Не нормализовать адреса. Без отбрасывания якоря, упорядочивания параметров и приведения регистра робот обходит одни и те же страницы по кругу.
  • Доверять схеме без проверки. Перед редиректом или вставкой ссылки убеждайтесь, что схема безопасна (http/https), иначе открывается дорога для XSS через javascript:.

Заключение

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