Масштабный парсинг — это не просто извлечение данных из HTML-кода. Это процесс, чем-то напоминающий движение по улицам и домам. Чтобы исследовать всю местность, вам нужно пройтись по каждой улице (категории и разделу целевого сайта) и заглянуть в каждый двор (на отдельные страницы). Но где взять список всех URL-адресов сайта и как потом получить содержимое всех страниц?
В этом материале подробно рассказываем о том, какие существуют подходы для поиска всех ссылок сайта, а также о том, как можно организовать их правильный обход.
Лучшие способы найти все URL-адреса на одном домене
Почему только на одном домене? Дело в том, что на страницах целевого сайта могут содержаться ссылки на внешние ресурсы. И если вы не ограничите получение всех страниц сайта доменом, то парсер может войти в рекурсию: он будет находить всё время новые URL, уже на новых сайтах, ставить их в очередь на извлечение, снова находить на них ссылки, и так до бесконечности, а точнее до тех пор, пока скрипт не обойдёт весь Интернет. И то, при условии, что вы запретите ему посещение найденных адресов дважды.
К слову, существуют готовые инструменты, которые реализуют задачу парсинга сайта для получения списка всех его URL, такие как Screaming Frog.
Но в контексте нашей статьи нас больше интересует логика формирования очереди парсинга. Это фундаментальная задача при построении любых скрейперов. Жалко, что она имеет множество решений, и ни одно из них нельзя назвать самым лучшим.
Итак, как вы можете найти все URL с конкретного сайта:
Способ 1. Обратиться к XML-картам сайта (файлы sitemap.xml)
Парсер обращается напрямую к карте сайта и извлекает из неё форматированные данные. Такие карты движок сайта в норме формирует автоматически — специально для поисковых ботов (пауков). Формат хранимой информации строго определён — это XML-разметка. Казалось бы — It's easy!
Но не всё может быть так гладко:
- Карты сайта может не быть.
- В карте может быть устаревшая информация, особенно если она не формируется автоматически.
- В карте может не быть важных разделов и страниц.
- Сама карта может быть разбита на несколько файлов — под разные разделы и типы материалов.
Чтобы не напороться на санкции и блокировки, нужно дополнительно изучить правила для ботов, изложенные в директивах robots.txt.
Способ 2. Использовать готовые профильные инструменты и программы
Основная идея такая — указать нужный целевой домен в программе или в онлайн-сервисе, профильный инструмент сам обойдёт страницы, а на выходе отдаст только итоговый список всех URL сайта в удобном формате, например, в CSV или XML. Уже с ним сможет работать ваш парсер. К нишевым инструментам можно отнести такие решения, как Netpeak Spider, Screaming Frog, Ahrefs, Semrush и пр.
Выглядит идеально. Но есть нюансы:
- Многие программные SEO-краулеры распространяются платно, как и аналогичные SEO-сервисы.
- Stand-alone программы могут не работать с определёнными типами сайтов. А ещё для них могут потребоваться качественные ротируемые прокси, чтобы ваш IP не забанили после нескольких автоматических запросов. Всё из-за того, что многие современные сайты не спарсить без headless-браузеров.
- Пока программа не закончит сбор URL, вы не сможете взять их в дальнейшую работу. А если на целевом сайте десятки тысяч страниц, то вам придётся ждать ОЧЕНЬ долго.
- Внешние сервисы и программы тяжело интегрируются с самописными скриптами автоматизации.
Способ 3. Обратиться к поиску Google
Вместо Google может выступать любой другой поисковик. Но Google традиционно имеет больший охват, а значит данные в нём с большей вероятностью будут актуальными. Логика простая — вы используете для поиска специальные операторы, например, site:тут-целевой-домен.com, и поисковик возвращает вам список всех страниц, которые значатся в его индексе. Хитро и даже вполне жизнеспособно для определённых ситуаций, например, когда не получается обойти антибот-защиту на нужном сайте.
Но снова есть нюансы:
- В Google вы сможете получить только те страницы, о которых «знает» поисковик. Покрытие никогда не бывает 100%-ным, так как часть страниц и разделов для поисковика могут оставаться недоступными.
- Глубина поиска Google не является бесконечной. А если учесть, что с недавних пор в поисковой выдаче больше нельзя настроить показ 100 результатов, то вы сможете собрать лишь малую часть из существующих URL одного домена.
- Выдача поисковика давно персонализируется, поэтому результаты могут быть условно «хаотичными» — с разных IP и локаций можно получать уникальные списки страниц по одному домену.
- Google тоже защищается от парсеров и может периодически показывать капчу или блокировать отдельные IP-адреса.
Смотрите также: Как парсить SERP Google
Способ 4. Найти все URL на домене самописным скриптом
Все перечисленные выше проблемы и недостатки можно решить, если написать такой скрипт, который будет извлекать URL-адреса и обходить страницы по нужной вам логике: в закрытых и открытых разделах, с учётом директив robots.txt или без, с фильтрацией неподходящих адресов и т.д.
Но и здесь есть свои подводные камни:
- Этот скрипт нужно написать. Без знаний программирования вы даже не сможете адаптировать готовые решения под свои задачи.
- На страницах сайта могут быть ловушки (honeypots), а сам сайт может защищаться веб-файрволлом. Например, WAF от Cloudflare.
- Сами страницы могут формироваться динамически. Поэтому парсинг потребует сложных и ресурсоёмких решений, например, на основе антидетектов или headless-браузеров.
- Важно сохранять прогресс или статус парсинга, чтобы можно было вернуться к точке, в которой вы получили ошибку. И не начинать всё заново.
- Всегда существует вероятность комбинаторного взрыва — это когда внутри URL-адресов используются дополнительные параметры, которые не несут полезной нагрузки. Но из-за них URL получаются уникальными, соответственно, итоговый список может разрастаться до бесконечности, заполняясь техническими дублями.
- В список URL-адресов могут попадать не только сами страницы, но и все дополнительные файлы — CSS-стили, JS-скрипты, картинки, документы и пр.
Как просканировать сайт на наличие всех URL-адресов

Здесь опишем то, как должна выглядеть общая логика парсера, который ищет все URL на одном домене:
Определение начальных точек (точек входа)
В качестве стартового адреса обычно используется главная страница (домен) или ссылка на карту сайта (обычно это что-то вида домен.com/sitemap.xml). Но в определённых ситуациях могут быть актуальны списки опорных категорий или маски (RegExp-выражения). Это позволяет сразу задать структуру обхода и не начинать с одной-единственной точки.
Инициализация очереди обхода
Скрипт должен получить HTML-содержимое стартовой страницы или страниц, чтобы собрать с них следующие URL-адреса. Именно так формируется очередь, на основе которой ищутся все URL домена. Наиболее распространённая логика обхода — FIFO (первый пришёл, первый ушёл, то есть пошёл на парсинг). Но могут быть и другие подходы: последовательный обход категорий, рандомизация и т.п.
Создание структуры для хранения прогресса и посещённых URL
Чтобы сохранить прогресс и статус парсинга отдельных страниц, создаётся структура для хранения данных и признаков посещённых URL. Это может быть хеш-таблица или полноценная база данных с задействованием механизма полей.
Исключение внешних ссылок и нормализация URL
Технически никто не запрещает вам искать URL, ведущие на другие сайты (имеющие сторонние домены), но включать их в обход нельзя — так вы не сможете остановиться никогда. А ещё в базу будут попадать ненужные (мусорные) URL — ссылки на скрипты, картинки, CSS-таблицы, фильтры с параметрами и т.п. Чтобы исключить их из базы, нужна настройка правил фильтрации.
Наиболее логичные правила:
- В очередь на обход или в базу заносятся только URL, начинающиеся с целевого домена. С учётом или без учёта поддоменов — тут уже по желанию и в зависимости от структуры сайта.
- Найденные URL должны приводиться к каноническому виду — удаляются якоря (после символа «#») и параметры (они объединяются знаком «&», а сами параметры передаются после «?»). В некоторых случаях нужно добавлять или удалять завершающие слеши «/», чтобы привести URL к одному виду. Логично также очистить страницы фильтров. Правила фильтрации чаще всего задаются конструкциями ?sort=, ?filter= и т.п.
- Из списка на обход удаляются ненужные паттерны — URL-адреса скриптов, картинок, CSS, документов и т.п.
Первая полноценная итерация обхода
Здесь вам нужно описать логику парсинга отдельно взятой страницы — это ваш основной цикл. В норме: берётся URL из очереди, который не имеет признака «посещён», к этому URL выполняется HTTP-запрос, из тела страницы извлекается весь HTML-код, внутри кода ищутся все URL, они очищаются и нормализуются на основе требований и правил, описанных в 4 шаге, проверяются на наличие в базе. Если ссылки новые, то они попадают в очередь на обход.
Цикл завершается только тогда, когда он не может найти новые страницы
Здесь «новые» — это те страницы, которые не имеют признака «посещён».
Даже если скрипт остановить или прервать, на основе системы статусов обхода процесс всегда можно возобновить с того места, где он остановился.
Резидентные прокси
Идеальные прокси-серверы для доступа к ценным данным со всего мира.
На что ещё можно обратить внимание:
- Нагрузка. При большом объёме страниц важно соблюдать баланс между глубиной и шириной обхода. Следует правильно продумать архитектуру многопоточности и скорости обращений. Частоту запросов можно регулировать за счёт времени задержек. Но их не стоит делать одинаковыми, чтобы не вызывать подозрений у антибот-систем.
- Запреты и ограничения. Подумайте про правила и логику обхода на основе директив robots.txt и политики использования. Парсинг должен быть этичным.
- Обработка ошибок и нестандартных ответов. Стандартный ответ сервера — код 200. Но не все ссылки, найденные на страницах сайта, ведут на существующие объекты. Плюс антибот-система может в любой момент заблокировать ваши запросы или отдать страницы-заглушки. Чтобы обезопасить себя от таких проблем, нужно правильно обрабатывать коды ошибок и мониторить нестандартное содержимое страниц. Наиболее важные коды: 404 (страница не существует), 403 (доступ запрещён), 301/302 — это редиректы на правильные конечные URL и 500 (сервер недоступен).
- Логика повторных попыток. Это блок кода, который позволяет быстро настраивать поведение парсера при обнаружении ошибок — когда сайт или сервер недоступен или когда вам заблокировали доступ. Например, можно задействовать ротацию прокси.
- Логирование и мониторинг. При очень больших объёмах парсинга можно подумать о внедрении метрик и показателей, которые облегчат мониторинг прогресса: сколько URL уже обработано, сколько в очереди, сколько ошибок найдено и каких, текущая скорость парсинга и т.п.
- Особая логика завершения обхода. Например, можно завершать работу парсера не только когда очередь пуста, но и когда достигнуты лимиты по времени или по глубине обхода, по количеству страниц и пр.
Типичные проблемы поиска всех URL на домене:
- Зацикливание. Парсер будет ходить «по кругу».
- Экспоненциальный рост количества URL из-за неправильной очистки параметров (или при полном отсутствии очистки).
- Недоступность контента без JS-рендеринга. Вам буквально неоткуда будет брать список всех URL сайта, так как конечное содержимое не формируется без полноценного браузера.
- Блокировки со стороны сайта. Чаще всего это баны IP-адресов, которые легко обходятся с помощью прокси.
- Неэффективная логика обхода. Наиболее важные страницы могут всё время отодвигаться в конец списка.
- Повышение сложности архитектуры при росте масштаба.
В общем-то, даже «простой» обход сайта может легко превратиться в задачу корпоративного уровня: с построением распределённых очередей, балансировкой нагрузки, дашбордами, графами и т.п.
Пример использования Python для поиска всех URL-адресов на домене
Пример простого скрипта, который обходит заданное количество страниц целевого сайта. Если указать очень большое число, то он обойдёт все URL:
import time
import os
import csv
import requests
from urllib.parse import urljoin, urlparse, urldefrag, parse_qs, urlencode
from bs4 import BeautifulSoup
import xml.etree.ElementTree as ET
# -----------------------------
# Конфигурация
# -----------------------------
START_URLS = ["https://example.com"]
ALLOWED_DOMAIN = "example.com"
STATE_FILE = "crawler_state.csv" # можно поменять на .xml
SAVE_EVERY = 20 # сохранять состояние каждые N URL
REQUEST_DELAY = 0.5
MAX_URLS = 1000
TIMEOUT = 10
BLACKLIST_PARAMS = {"utm_source", "utm_medium", "utm_campaign", "sort", "filter", "page"}
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; SimpleCrawler/1.1)"
}
# -----------------------------
# Нормализация URL
# -----------------------------
def normalize_url(url):
url, _ = urldefrag(url)
parsed = urlparse(url)
if ALLOWED_DOMAIN not in parsed.netloc:
return None
query = parse_qs(parsed.query)
filtered_query = {k: v for k, v in query.items() if k not in BLACKLIST_PARAMS}
query_string = urlencode(filtered_query, doseq=True)
normalized = parsed._replace(query=query_string).geturl()
if normalized.endswith("/"):
normalized = normalized[:-1]
return normalized
# -----------------------------
# Извлечение ссылок
# -----------------------------
def extract_links(html, base_url):
soup = BeautifulSoup(html, "html.parser")
links = set()
for tag in soup.find_all("a", href=True):
full_url = urljoin(base_url, tag.get("href"))
normalized = normalize_url(full_url)
if normalized:
links.add(normalized)
return links
# -----------------------------
# Сохранение состояния (CSV)
# -----------------------------
def save_state_csv(queue, visited):
with open(STATE_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["type", "url"])
for url in visited:
writer.writerow(["visited", url])
for url in queue:
writer.writerow(["queue", url])
def load_state_csv():
queue = []
visited = set()
with open(STATE_FILE, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row["type"] == "visited":
visited.add(row["url"])
elif row["type"] == "queue":
queue.append(row["url"])
return queue, visited
# -----------------------------
# Сохранение состояния (XML)
# -----------------------------
def save_state_xml(queue, visited):
root = ET.Element("crawler")
visited_el = ET.SubElement(root, "visited")
for url in visited:
ET.SubElement(visited_el, "url").text = url
queue_el = ET.SubElement(root, "queue")
for url in queue:
ET.SubElement(queue_el, "url").text = url
tree = ET.ElementTree(root)
tree.write(STATE_FILE, encoding="utf-8", xml_declaration=True)
def load_state_xml():
tree = ET.parse(STATE_FILE)
root = tree.getroot()
visited = set()
queue = []
for url in root.find("visited"):
visited.add(url.text)
for url in root.find("queue"):
queue.append(url.text)
return queue, visited
# -----------------------------
# Универсальная загрузка/сохранение
# -----------------------------
def save_state(queue, visited):
if STATE_FILE.endswith(".xml"):
save_state_xml(queue, visited)
else:
save_state_csv(queue, visited)
def load_state():
if not os.path.exists(STATE_FILE):
return None, None
if STATE_FILE.endswith(".xml"):
return load_state_xml()
else:
return load_state_csv()
# -----------------------------
# Краулер
# -----------------------------
def crawl():
queue, visited = load_state()
if queue is None:
print("[i] State file not found. Starting fresh.")
queue = list(START_URLS)
visited = set()
else:
print(f"[i] Loaded state. Queue: {len(queue)}, Visited: {len(visited)}")
processed_since_save = 0
while queue and len(visited) < MAX_URLS:
url = queue.pop(0)
if url in visited:
continue
print(f"[+] Visiting: {url}")
visited.add(url)
processed_since_save += 1
try:
response = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
if response.status_code != 200:
continue
links = extract_links(response.text, url)
for link in links:
if link not in visited and link not in queue:
queue.append(link)
except requests.RequestException:
continue
# периодическое сохранение
if processed_since_save >= SAVE_EVERY:
print("[i] Saving state...")
save_state(queue, visited)
processed_since_save = 0
time.sleep(REQUEST_DELAY)
print("[i] Final save...")
save_state(queue, visited)
return visited
# -----------------------------
# Запуск
# -----------------------------
if __name__ == "__main__":
urls = crawl()
print(f"\nTotal URLs collected: {len(urls)}")
Скрипт работает без headless-браузера и использует анализатор Beautiful Soup. Если рядом со скриптом при запуске не существует файла с URL-адресами, то он создаёт его и наполняет с нуля. Если файл есть, то в нём подхватываются статусы посещения, чтобы процесс не начинался каждый раз сначала.
При желании вы можете задать свой целевой URL и ограничения, задержки и параметры, которые следует удалять из структуры адреса.
Заключение и рекомендации
Выше мы рассмотрели наиболее вероятные способы того, как проверить все страницы сайта и собрать итоговый список URL-адресов одного домена. Самый простой — использовать готовые решения. Но самый правильный и функциональный — написать свой скрипт парсинга.
Ни профильные инструменты, ни масштабные парсеры не смогут долго работать без качественных прокси. Froxy — это миллионы ротируемых прокси с оплатой за потребляемый трафик. Число параллельных потоков может быть до 1000. С нашей помощью вы сможете закрыть даже самые сложные задачи по сбору данных.

