Масштабный парсинг — это не просто извлечение данных из HTML-кода. Это процесс, чем-то напоминающий движение по улицам и домам. Чтобы исследовать всю местность, вам нужно пройтись по каждой улице (категории и разделу целевого сайта) и заглянуть в каждый двор (на отдельные страницы). Но где взять список всех URL-адресов сайта и как потом получить содержимое всех страниц?
В этом материале подробно рассказываем о том, какие существуют подходы для поиска всех ссылок сайта, а также о том, как можно организовать их правильный обход.
Почему только на одном домене? Дело в том, что на страницах целевого сайта могут содержаться ссылки на внешние ресурсы. И если вы не ограничите получение всех страниц сайта доменом, то парсер может войти в рекурсию: он будет находить всё время новые URL, уже на новых сайтах, ставить их в очередь на извлечение, снова находить на них ссылки, и так до бесконечности, а точнее до тех пор, пока скрипт не обойдёт весь Интернет. И то, при условии, что вы запретите ему посещение найденных адресов дважды.
К слову, существуют готовые инструменты, которые реализуют задачу парсинга сайта для получения списка всех его URL, такие как Screaming Frog.
Но в контексте нашей статьи нас больше интересует логика формирования очереди парсинга. Это фундаментальная задача при построении любых скрейперов. Жалко, что она имеет множество решений, и ни одно из них нельзя назвать самым лучшим.
Итак, как вы можете найти все URL с конкретного сайта:
Парсер обращается напрямую к карте сайта и извлекает из неё форматированные данные. Такие карты движок сайта в норме формирует автоматически — специально для поисковых ботов (пауков). Формат хранимой информации строго определён — это XML-разметка. Казалось бы — It's easy!
Но не всё может быть так гладко:
Чтобы не напороться на санкции и блокировки, нужно дополнительно изучить правила для ботов, изложенные в директивах robots.txt.
Основная идея такая — указать нужный целевой домен в программе или в онлайн-сервисе, профильный инструмент сам обойдёт страницы, а на выходе отдаст только итоговый список всех URL сайта в удобном формате, например, в CSV или XML. Уже с ним сможет работать ваш парсер. К нишевым инструментам можно отнести такие решения, как Netpeak Spider, Screaming Frog, Ahrefs, Semrush и пр.
Выглядит идеально. Но есть нюансы:
Вместо Google может выступать любой другой поисковик. Но Google традиционно имеет больший охват, а значит данные в нём с большей вероятностью будут актуальными. Логика простая — вы используете для поиска специальные операторы, например, site:тут-целевой-домен.com, и поисковик возвращает вам список всех страниц, которые значатся в его индексе. Хитро и даже вполне жизнеспособно для определённых ситуаций, например, когда не получается обойти антибот-защиту на нужном сайте.
Но снова есть нюансы:
Смотрите также: Как парсить SERP Google
Все перечисленные выше проблемы и недостатки можно решить, если написать такой скрипт, который будет извлекать URL-адреса и обходить страницы по нужной вам логике: в закрытых и открытых разделах, с учётом директив robots.txt или без, с фильтрацией неподходящих адресов и т.д.
Но и здесь есть свои подводные камни:
Здесь опишем то, как должна выглядеть общая логика парсера, который ищет все URL на одном домене:
В качестве стартового адреса обычно используется главная страница (домен) или ссылка на карту сайта (обычно это что-то вида домен.com/sitemap.xml). Но в определённых ситуациях могут быть актуальны списки опорных категорий или маски (RegExp-выражения). Это позволяет сразу задать структуру обхода и не начинать с одной-единственной точки.
Скрипт должен получить HTML-содержимое стартовой страницы или страниц, чтобы собрать с них следующие URL-адреса. Именно так формируется очередь, на основе которой ищутся все URL домена. Наиболее распространённая логика обхода — FIFO (первый пришёл, первый ушёл, то есть пошёл на парсинг). Но могут быть и другие подходы: последовательный обход категорий, рандомизация и т.п.
Чтобы сохранить прогресс и статус парсинга отдельных страниц, создаётся структура для хранения данных и признаков посещённых URL. Это может быть хеш-таблица или полноценная база данных с задействованием механизма полей.
Технически никто не запрещает вам искать URL, ведущие на другие сайты (имеющие сторонние домены), но включать их в обход нельзя — так вы не сможете остановиться никогда. А ещё в базу будут попадать ненужные (мусорные) URL — ссылки на скрипты, картинки, CSS-таблицы, фильтры с параметрами и т.п. Чтобы исключить их из базы, нужна настройка правил фильтрации.
Наиболее логичные правила:
Здесь вам нужно описать логику парсинга отдельно взятой страницы — это ваш основной цикл. В норме: берётся URL из очереди, который не имеет признака «посещён», к этому URL выполняется HTTP-запрос, из тела страницы извлекается весь HTML-код, внутри кода ищутся все URL, они очищаются и нормализуются на основе требований и правил, описанных в 4 шаге, проверяются на наличие в базе. Если ссылки новые, то они попадают в очередь на обход.
Здесь «новые» — это те страницы, которые не имеют признака «посещён».
Даже если скрипт остановить или прервать, на основе системы статусов обхода процесс всегда можно возобновить с того места, где он остановился.
Идеальные прокси-серверы для доступа к ценным данным со всего мира.
На что ещё можно обратить внимание:
Типичные проблемы поиска всех 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. С нашей помощью вы сможете закрыть даже самые сложные задачи по сбору данных.