Парсинг URL (от англ. parse — «разбирать») — это разбор адреса веб-страницы на отдельные составные части: протокол, домен, порт, путь, параметры запроса и якорь. Из «сырой» строки вида https://example.com/catalog?page=2 мы получаем структурированный объект, к каждому полю которого можно обратиться по отдельности и при необходимости изменить его.
На первый взгляд задача кажется тривиальной — «разбить строку по символам». На практике у URL есть формальная спецификация (RFC 3986) с массой нюансов: кодирование спецсимволов, относительные ссылки, необязательные части адреса, нормализация. Поэтому почти в каждом языке есть штатный инструмент для парсинга URL, и писать собственный разбор регулярными выражениями практически никогда не нужно.
Из чего состоит URL
Разберём максимально «полный» адрес, чтобы увидеть все компоненты:
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 почти всегда относительные, а не абсолютные. В разметке вы встретите:
<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.
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. Он же умеет разрешать относительные ссылки — для этого базовый адрес передаётся вторым аргументом.
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().
$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() сразу даёт разрешение относительной ссылки.
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).
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:
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 — базовая, но важная операция: она превращает строку-адрес в структуру, с которой удобно работать. В обычных приложениях он нужен для извлечения параметров, валидации и маршрутизации, а в краулерах становится критически важным — без приведения относительных ссылок к абсолютному виду и без нормализации обход сайта попросту невозможен. Хорошая новость в том, что почти в каждом языке для этого есть надёжный штатный инструмент, и изобретать велосипед не приходится.