DEV Community

Eugene
Eugene

Posted on

Как мы держим мусор подальше от программных гео-страниц: белый список метро и чёрный список районов

На podbor-minuta.ru есть страницы-подборки квартир, сгруппированные по метро и по району. Идея простая: у каждой станции метро и у каждого района своя страница со списком новостроек рядом. Такие страницы собираются заранее из базы, у каждой свой адрес, заголовок и реальный список квартир. Проблема была в том, из чего мы эти группы строили.

Данные о метро и районе приходят от скрейпера, а он берёт их с чужих сайтов как есть. В поле "метро" вместо чистого названия станции лежали строки вроде "Тушинская (13 мин.)". То есть к станции приклеено время пешком. Для человека это мелочь, а для группировки - катастрофа: "Тушинская", "Тушинская (5 мин.)", "Тушинская (13 мин.)" считаются тремя разными значениями. Одна станция превращалась в несколько почти одинаковых страниц.

С районом было хуже. В поле "район" скрейпер сваливал что попало: названия жилых комплексов ("ЖК Сезар Сити"), рекламные куски ("многофункциональный комплекс..."), а иногда целые предложения из описания - с ценами, площадями и стадиями стройки. Настоящих районов там было меньше половины.

Наивная группировка по этим полям дала около 193 тонких страниц, почти дублей. Для молодого домена это прямой риск: поисковики не любят пачку пустых похожих страниц и могут понизить весь сайт. Нам нужны были только страницы с настоящими станциями и настоящими районами, и чтобы варианты одной станции схлопывались в одну.

Первое, что мы поняли: метро и район - это два разных случая, и лечить их надо по-разному.

Для метро у нас есть полный справочник станций Москвы. Значит, можно проверять по белому списку: если после чистки значение есть в справочнике - берём, если нет - выкидываем. А чтобы схлопнуть варианты, адрес страницы строится не из сырого значения, а из нормализованного названия. Сначала убираем хвост со временем пешком, получаем чистую "Тушинскую", и уже из неё делаем адрес. Все варианты с разным временем сходятся в один и тот же адрес.

// "Тушинская (13 мин.)" -> "Тушинская" -> tushinskaya
const station = normalizeMetroValue(raw);
if (!isKnownMetroStation(station) && !KNOWN_TRANSIT_LINES.has(lineKey)) {
  return null; // не настоящая станция - страницы не будет
}
const slug = transliterateSlug(station);
Enter fullscreen mode Exit fullscreen mode

Отдельно мы оставили линии как самостоятельный вид страницы: МЦД, МЦК, БКЛ. Это не станции, но у них есть реальный спрос в поиске ("новостройки у МЦД") и под ними тысячи квартир. Поэтому рядом с белым списком станций держим маленький список линий.

const KNOWN_TRANSIT_LINES = new Set([
  'мцд', 'мцк', 'мцд-1', 'мцд-2', 'мцд-3', 'мцд-4', 'мцд-5', 'бкл',
]);
Enter fullscreen mode Exit fullscreen mode

С районом так не получилось, и вот почему. Полного справочника районов у нас нет. В нашей таблице районов не хватает округов и городов Московской области. А это настоящие, живые группы, которые мы не хотим терять. Если сделать белый список, мы выкинем вместе с мусором и половину нормальных данных.

Поэтому для района мы пошли от обратного: не описываем, что хорошо, а описываем, что заведомо плохо, и это отбраковываем. Плохое здесь хорошо определено. Название жилого комплекса почти всегда начинается с "ЖК", "МФК", "жилой комплекс", "башня", "резиденции". Кусок описания почти всегда содержит цифры с "млн", "тыс", "руб", "кв. м" или "мин", либо слова про стройку - "возводится", "сдан", "очередь". Настоящий район не выглядит так никогда.

const JUNK_DISTRICT_RE =
  /(жилой\s+(?:комплекс|квартал)|(?:^|\s)(?:жк|мфк|башня)(?:\s|$)|(?:^|\s)(?:апарт|резиденц)[а-яё]*|\d[\d.,\s]*(?:млн|тыс|руб|кв\.?\s?м|мин\b)|возвод|строит|сдан|очеред)/i;

export function isJunkDistrictValue(raw: string): boolean {
  const v = raw.trim().toLowerCase();
  if (!v || v.length > 60) return true; // район не бывает длиной в предложение
  return JUNK_DISTRICT_RE.test(v);
}
Enter fullscreen mode Exit fullscreen mode

Пара тонкостей, на которых легко обжечься. В JavaScript класс "\w" не включает кириллицу, поэтому продолжения слов пришлось писать явно через "[а-яё]*". И стемы надо привязывать к границе слова: если просто искать "дом", регулярка поймает "Домодедово" и убьёт настоящий район. Поэтому короткие слова требуют пробела или конца строки вокруг, а префиксы вроде "апарт" и "резиденц" ловят проектные формы напрямую.

Обе проверки сходятся в одной функции. Она получает сырое значение и либо возвращает пару "показываемое имя плюс адрес", либо null, и тогда страницы просто не будет. Это единственная точка, через которую гео попадает в каталог и в карту сайта.

export function resolveSeoGeoLabel(dim, rawDisp) {
  const raw = (rawDisp || '').trim();
  if (!raw) return null;
  let display;
  if (dim === 'metro') {
    const station = normalizeMetroValue(raw);
    if (!isKnownMetroStation(station) && !KNOWN_TRANSIT_LINES.has(key)) return null;
    display = station;
  } else {
    if (isJunkDistrictValue(raw)) return null;
    display = raw;
  }
  const slug = transliterateSlug(display);
  if (!slug || slug.length > MAX_GEO_SLUG_LEN) return null;
  return { display, slug };
}
Enter fullscreen mode Exit fullscreen mode

Сверху идёт ещё один простой запас прочности. Мы ограничили длину адреса в 64 символа. Каждая такая страница при сборке становится папкой на диске, а имя папки в файловой системе не может быть длиннее 255 байт. Одно грязное значение длиной в целое предложение когда-то уронило нам всю сборку. Про эту историю у нас есть отдельная статья, а здесь просто не пускаем длинные значения дальше.

И последний фильтр - количество. Страницу строим только если под ней набирается хотя бы порог квартир. В SQL это одна строчка "HAVING COUNT(*) >= minLots". Так отсекаются группы из одной-двух квартир, которые тоже дают тонкие страницы.

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

Сайт - podbor-minuta.ru, ежедневный мониторинг цен на новостройки Москвы.

Top comments (0)