Кластеризация URL помогает краулерам отличать структурные страницы от идентификаторов. Узнайте, как построить устойчивый алгоритм без ML.
Вы когда-нибудь задумывались, как поисковый робот понимает, что /catalog/iphone-15 — это страница товара, а /product/12345 — просто идентификатор? На самом деле, это сложная задача, особенно на крупных сайтах. В этой статье я расскажу, как мы решили проблему кластеризации URL для эффективного обхода сайтов, используя простые эвристики и адаптивное обучение. Вы узнаете, как отличить структурные URL от идентификаторов и почему чистые правила всегда проигрывают.
Когда я впервые столкнулся с задачей, мне казалось, что можно просто посмотреть на URL и понять его тип. Но реальность оказалась сложнее. Паттерн-матчинг разбивался об URL с нестандартными слагами, а Trie-структуры не помогали на больших e-commerce сайтах, где URL генерируются хаотично. Детерминированные правила (например, «если есть число — это ID») работали только для хэшей и чисел, но не для контентных слагов, которые могут быть короткими, без дефисов и содержать буквы с цифрами.
Первая попытка — обучить модель на основе наивного Байеса. Прототип на тестовых URL показал неплохие результаты, но на реальных данных модель требовала огромного датасета и пайплайна. Я понял, что моих навыков в ML недостаточно, и решил попробовать другие подходы.
Я обучил модель на размеченных URL, и она научилась определять «позитивные» пути. Но на продакшене данных было слишком много, и обобщение требовало больше данных и лучшего пайплайна. Осознав, что я не настолько силён в ML, я отказался от этого подхода.
Следующая идея — добавить локальный словарь для каждого сайта. Глобальная модель работала для общих слов, но не понимала, что application/marine может быть крупным хабом. Локальный словарь должен был заполнять пробелы. Но проблема: то, что редко встречалось в общей выборке, было частым на конкретном сайте, и наоборот. Подбор весов для глобального и локального скоринга превратился в охоту на призраков.
Тогда я решил полностью сосредоточиться на локальном словаре. Каждый сайт уникален, и мне нужно было только найти крупные страницы с редкими паттернами URL. Этот подход показал лучшие результаты: он надёжно разделял структурные и идентификаторные URL на основе только локального словаря. Но минус — терялась предварительная оценка, и краулеру требовалось время, чтобы понять структуру сайта. Сначала URL группировались в разные кластеры, вместо того чтобы объединяться в один.
Я перебрал несколько идей: фейковый обход для обучения, ограничение в 100 страниц для сбора словаря — всё это было либо дорого, либо ненадёжно. В итоге я понял: нужно строить устойчивость. Как человек, мы начинаем с догадки, накапливаем данные и уточняем кластеры. Так и здесь: мы добавили чекпоинты каждые 50 загрузок, пересчитывали паттерны URL с обновлённым словарём и ввели защитные механизмы для оценки «редкости» и «структурности». Редкие ключевые слова на сайте могут быть не только 1,2,3, а структурные — не только 40,50. В начале мы не знаем этих порогов, но по мере обхода они уточняются, и кластеры становятся всё более различимыми.
Ниже — упрощённая реализация идеи на Python. Мы собираем статистику по сегментам URL, вычисляем их частоту и на основе этого относим сегмент к структурному или идентификаторному.
from collections import Counter
import re
# Функция разбивает URL на сегменты (пути)
def get_segments(url):
path = url.split('?')[0] # убираем query-параметры
return path.split('/')[1:] # пропускаем первый пустой элемент
# Класс для адаптивной кластеризации
class AdaptiveURLCluster:
def __init__(self, threshold=0.1):
self.segment_counts = Counter() # счётчики сегментов
self.threshold = threshold # порог для определения структурности
self.total_urls = 0
def update(self, urls):
'''Обновляем статистику на основе новых URL'''
for url in urls:
segments = get_segments(url)
for seg in segments:
self.segment_counts[seg] += 1
self.total_urls += 1
# Пересчитываем порог на основе текущих данных
self._recalibrate_threshold()
def _recalibrate_threshold(self):
'''Пересчитываем порог: редкие сегменты — идентификаторы'''
if self.total_urls == 0:
return
# Считаем среднюю частоту сегмента
avg_freq = sum(self.segment_counts.values()) / len(self.segment_counts)
# Порог — доля от средней частоты
self.threshold = avg_freq * 0.1
def classify_segment(self, segment):
'''Определяем тип сегмента: структурный или идентификатор'''
freq = self.segment_counts.get(segment, 0)
if freq == 0:
return 'unknown'
if freq / self.total_urls >= self.threshold:
return 'structural'
else:
return 'identifier'
# Пример использования
cluster = AdaptiveURLCluster()
# Первая партия URL (имитация обхода)
urls_batch1 = [
'/catalog/iphone-15',
'/catalog/samsung-galaxy',
'/product/12345',
'/product/67890',
'/blog/10-tips-for-coding'
]
cluster.update(urls_batch1)
# Проверяем классификацию
for url in urls_batch1:
segments = get_segments(url)
types = [cluster.classify_segment(seg) for seg in segments]
print(url, types)
Этот код демонстрирует базовый принцип: мы накапливаем статистику и адаптивно меняем порог. На практике нужно больше нюансов: учитывать длину сегмента, наличие дефисов, цифр и т.д. Но идея остаётся.
Главный урок — не пытайтесь угадать структуру сайта заранее. Вместо этого создайте систему, которая самообучается на лету. Начните с простого счётчика сегментов, добавьте периодический пересчёт порогов и защитные механизмы. Это поможет вам эффективно обходить даже самые хаотичные сайты. Попробуйте реализовать такой адаптивный кластеризатор в своём краулере — и вы увидите, как сократится время обхода и количество бесполезных запросов.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →