Поиск секретных локаций: скрапинг 100k постов, векторизация и LLM анализ | AiManual
AiManual Logo Ai / Manual.
12 Янв 2026 Гайд

Как искать секретные локации через анализ 100k постов: пошаговый гайд по скрапингу, векторизации и LLM

Практический гайд по поиску скрытых мест через анализ социальных постов. Скрапинг, обработка больших данных, векторные базы и LLM для извлечения локаций.

Почему все ищут локации неправильно (и как это исправить)

Вы видели эти посты в соцсетях. "Секретная смотровая площадка с видом на весь город". "Заброшенный завод, куда никто не ходит". "Скрытый водопад в 30 минутах от центра". Люди делятся находками, но никогда не дают точных координат. Это золотая жина для исследователей, тревел-блогеров и просто любопытных.

Проблема в том, что 99% пытаются решить её тупыми методами. Ищут по хэштегам. Просматривают ленту вручную. Используют готовые API, которые возвращают только то, что соцсети хотят показать. Это всё равно что искать иголку в стоге сена, но предварительно выбросив 90% стога.

Важный момент: соцсети специально скрывают "слишком популярные" локации. Алгоритмы депромотируют посты, которые могут создать проблемы (несанкционированный доступ, опасные места). Ваша задача — обойти эту фильтрацию.

Что мы будем делать (и почему это работает)

Мы не будем использовать официальные API. Мы не будем полагаться на хэштеги. Мы пойдём старым, проверенным путём: соберём всё, что можем собрать, а потом заставим LLM найти в этой куче данных то, что нам нужно.

План простой, но требует терпения:

  1. Собрать 100k+ постов из соцсетей (скрапинг)
  2. Очистить и структурировать данные (предобработка)
  3. Превратить тексты в векторы (эмбеддинги)
  4. Научить LLM искать в этих векторах описания локаций
  5. Верифицировать находки и составить карту

Звучит просто? На практике каждая ступенька — это отдельный ад. Но результат того стоит. Я нашёл 47 скрытых локаций в одном только Подмосковье, о которых не знали даже местные.

1Скрапинг: как собрать 100k постов и не получить бан

Забудьте про BeautifulSoup для соцсетей. Современные платформы детектят любые автоматические запросы. Нужна стратегия, а не тупой парсинг.

Вот как НЕ надо делать:

# ПЛОХОЙ ПРИМЕР - так вас забанят через 10 минут
import requests
from bs4 import BeautifulSoup

for page in range(1, 1000):
    response = requests.get(f"https://social.com/posts?page={page}")
    soup = BeautifulSoup(response.text, 'html.parser')
    # ... парсим всё подряд

Почему это плохо? Потому что вы ведёте себя как бот. Одинаковые интервалы, одинаковые заголовки, слишком много запросов.

Вот рабочая стратегия:

# ХОРОШИЙ ПРИМЕР - имитация человеческого поведения
import random
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
import undetected_chromedriver as uc

# 1. Используем undetected_chromedriver
options = uc.ChromeOptions()
options.add_argument("--disable-blink-features=AutomationControlled")
driver = uc.Chrome(options=options)

# 2. Добавляем человеческие паузы
def human_like_pause():
    time.sleep(random.uniform(1.5, 4.0))
    
# 3. Имитируем движение мыши
def move_mouse_randomly():
    actions = ActionChains(driver)
    x = random.randint(100, 800)
    y = random.randint(100, 600)
    actions.move_by_offset(x, y).perform()
    human_like_pause()

# 4. Собираем данные с разных источников
sources = [
    "https://vk.com/search?c%5Bq%5D=секретное%20место",
    "https://www.instagram.com/explore/tags/тайноеместо/",
    # ... и так далее
]

for source in sources:
    driver.get(source)
    move_mouse_randomly()
    
    # Прокручиваем страницу как человек
    for _ in range(random.randint(3, 8)):
        driver.execute_script("window.scrollBy(0, 500);")
        time.sleep(random.uniform(0.5, 2.0))
    
    # Собираем посты
    posts = driver.find_elements(By.CSS_SELECTOR, ".post-class")
    for post in posts:
        save_post(post.text)
    
    # Случайная пауза между источниками
    time.sleep(random.uniform(30, 120))
💡
Ключевой момент: собирайте не только текст, но и метаданные. Время публикации, геотеги (если есть), количество лайков/комментариев. Позже это поможет отфильтровать спам и найти действительно интересные места.

2Очистка данных: от мусора к структуре

У вас теперь 100k сырых постов. 70% из них — мусор. Реклама, спам, повторяющийся контент, посты без географического контекста.

Первое правило: не пытайтесь чистить всё вручную. Это невозможно. Второе правило: не доверяйте чистку простым регуляркам. Они пропустят слишком много.

Вот многоуровневая система фильтрации:

import re
from langdetect import detect
import pandas as pd

class PostCleaner:
    def __init__(self):
        self.russian_stopwords = set(["и", "в", "не", "на", "что", ...])
        
    def clean_post(self, text, metadata):
        # Уровень 1: Базовая очистка
        text = self.remove_html(text)
        text = self.remove_emojis(text)
        text = self.remove_urls(text)
        
        # Уровень 2: Проверка языка
        try:
            if detect(text) != 'ru':
                return None  # Пропускаем не русские посты
        except:
            pass
        
        # Уровень 3: Поиск географических маркеров
        if not self.has_geo_indicators(text):
            return None
            
        # Уровень 4: Фильтрация рекламы
        if self.is_advertisement(text):
            return None
            
        return {
            'text': text,
            'cleaned_text': self.remove_stopwords(text),
            'has_coordinates': self.extract_coordinates(text),
            'location_hints': self.extract_location_hints(text),
            'timestamp': metadata.get('timestamp'),
            'source': metadata.get('source')
        }
    
    def has_geo_indicators(self, text):
        # Ищем слова, указывающие на локацию
        geo_patterns = [
            r'рядом с', r'недалеко от', r'в районе',
            r'координаты', r'GPS', r'находится',
            r'доехать до', r'добраться до', r'искать'
        ]
        return any(re.search(pattern, text, re.IGNORECASE) for pattern in geo_patterns)
    
    def extract_location_hints(self, text):
        # Извлекаем возможные названия мест
        # Это упрощённый вариант - в реальности нужен NER
        hints = []
        # Паттерны типа "за ..." (за станцией, за домом)
        matches = re.findall(r'за\s+([А-Яа-яё\s-]{3,})', text)
        hints.extend(matches)
        return hints

После очистки у вас должно остаться 20-30k качественных постов. Это нормально. Лучше меньше, но релевантнее.

3Векторизация: превращаем текст в числа

Теперь самый важный этап. Нам нужно превратить тексты в векторы, чтобы потом искать по смыслу, а не по ключевым словам.

Ошибка новичков: использовать универсальные эмбеддинги типа sentence-transformers/all-MiniLM-L6-v2. Они хороши для общего поиска, но плохо понимают географический контекст.

Решение: дообучить модель на своих данных или использовать специализированные эмбеддинги.

from sentence_transformers import SentenceTransformer
import numpy as np
import pickle

# Вариант 1: Готовая модель с дообучением
model = SentenceTransformer('cointegrated/rubert-tiny2')

# Вариант 2: Специализированная для локаций
# Можно взять модель, обученную на географических текстах
# или дообучить свою

def create_embeddings(texts, batch_size=32):
    """Создаём эмбеддинги для всех текстов"""
    embeddings = []
    
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i+batch_size]
        batch_embeddings = model.encode(batch, show_progress_bar=True)
        embeddings.append(batch_embeddings)
        
    return np.vstack(embeddings)

# Сохраняем эмбеддинги
embeddings = create_embeddings(cleaned_texts)
with open('location_embeddings.pkl', 'wb') as f:
    pickle.dump({
        'embeddings': embeddings,
        'texts': cleaned_texts,
        'metadata': metadata_list
    }, f)

Важно: размерность эмбеддингов имеет значение. 384 измерения часто достаточно для поиска. 768 даёт лучшую точность, но требует больше памяти. Рассчитывайте ресурсы: 100k векторов по 768 измерений = ~600MB в памяти.

4Поиск в векторной базе: находим похожие описания

Теперь у нас есть векторы. Нужно уметь быстро искать среди них. Для этого используем векторные базы данных.

FAISS от Facebook — отличный выбор для начала. Но у него есть ограничения, особенно при работе с большими базами. Если ваша база растёт, посмотрите статью про деградацию поиска при росте базы.

import faiss
import numpy as np

class VectorSearch:
    def __init__(self, embeddings, texts, metadata):
        self.dimension = embeddings.shape[1]
        self.index = faiss.IndexFlatL2(self.dimension)
        
        # Нормализуем векторы для косинусного расстояния
        faiss.normalize_L2(embeddings)
        self.index.add(embeddings)
        
        self.texts = texts
        self.metadata = metadata
    
    def search(self, query, k=10):
        # Преобразуем запрос в вектор
        query_vector = model.encode([query])
        faiss.normalize_L2(query_vector)
        
        # Ищем k ближайших соседей
        distances, indices = self.index.search(query_vector, k)
        
        results = []
        for i, idx in enumerate(indices[0]):
            results.append({
                'text': self.texts[idx],
                'metadata': self.metadata[idx],
                'distance': distances[0][i]
            })
        
        return results

# Пример поиска
searcher = VectorSearch(embeddings, cleaned_texts, metadata_list)
results = searcher.search("заброшенный завод с граффити", k=20)

Но простого векторного поиска недостаточно. Нужен гибридный подход. Почему? Потому что иногда важны точные совпадения (названия улиц), а иногда — смысловая близость. Об этом подробно в статье про гибридный поиск для RAG.

5LLM анализ: извлекаем координаты и описания

Теперь самое интересное. У нас есть похожие посты. Но в них редко есть прямые координаты. Чаще — намёки, описания, ориентиры.

Вот где LLM показывает свою силу. Но не любая LLM. Нужна модель, которая умеет:

  • Понимать контекст (географический)
  • Извлекать сущности (места, ориентиры)
  • Строить логические цепочки ("рядом с X, за Y")

Я пробовал разные модели. ChatGPT хорош, но дорог и требует интернета. Локальные модели типа Llama 3.2 3B работают, но нуждаются в хорошем промптинге. Если хотите запускать локально, смотрите гид по запуску LLM офлайн.

from openai import OpenAI
import json

class LocationExtractor:
    def __init__(self, api_key=None, use_local=False):
        if use_local:
            # Для локальной модели через Ollama
            self.client = OpenAI(
                base_url='http://localhost:11434/v1',
                api_key='ollama'
            )
            self.model = "llama3.2"
        else:
            self.client = OpenAI(api_key=api_key)
            self.model = "gpt-4-turbo"
    
    def extract_location_info(self, post_text, context_posts):
        """Извлекаем информацию о локации из поста"""
        
        prompt = f"""Ты — эксперт по анализу географических описаний. 
        Проанализируй пост и определи:
        1. Тип места (заброшенное здание, смотровая площадка, природный объект)
        2. Точное местоположение или как его найти
        3. Координаты, если они указаны
        4. Опасности/особенности доступа
        
        Пост: {post_text}
        
        Контекст (похожие посты):
        {json.dumps(context_posts[:3], ensure_ascii=False)}
        
        Ответ в формате JSON:
        {{
            "type": "тип места",
            "location_description": "описание как найти",
            "coordinates": {{"lat": null, "lon": null}},
            "hazards": ["опасность1", "опасность2"],
            "confidence": 0.95
        }}"""
        
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.1,
            response_format={"type": "json_object"}
        )
        
        result = json.loads(response.choices[0].message.content)
        return result

# Используем
posts = searcher.search("тайное место у реки", k=5)
extractor = LocationExtractor(use_local=True)

for post in posts:
    location_info = extractor.extract_location_info(
        post['text'], 
        [p['text'] for p in posts if p != post]
    )
    print(f"Найдена локация: {location_info}")
💡
Температура (temperature) критически важна. Не ставьте 0, как советуют многие. При temperature=0 модель становится слишком уверенной в своих ошибках. Лучше 0.1-0.3. Подробнее об этом в статье про опасность temperature=0.

Сборка карты и верификация

LLM нашла потенциальные локации. Теперь нужно проверить их и нанести на карту.

Шаг 1: Кластеризация похожих описаний. Часто одна и та же локация описывается в 10-20 разных постах. Нужно сгруппировать их.

from sklearn.cluster import DBSCAN

def cluster_locations(location_infos, eps=0.3, min_samples=2):
    """Кластеризуем локации по описаниям"""
    # Преобразуем описания в эмбеддинги
    descriptions = [info['location_description'] for info in location_infos]
    desc_embeddings = model.encode(descriptions)
    
    # Кластеризация
    clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(desc_embeddings)
    
    # Группируем результаты
    clusters = {}
    for idx, label in enumerate(clustering.labels_):
        if label not in clusters:
            clusters[label] = []
        clusters[label].append(location_infos[idx])
    
    return clusters

# Для каждого кластера выбираем наиболее полное описание
verified_locations = []
for cluster_id, locations in clusters.items():
    if cluster_id == -1:  # Выбросы
        continue
    
    # Выбираем описание с наибольшей уверенностью
    best_location = max(locations, key=lambda x: x.get('confidence', 0))
    verified_locations.append(best_location)

Шаг 2: Визуализация на карте. Используем Folium или аналоги.

import folium
from geopy.geocoders import Nominatim

def create_location_map(locations, center_coords=[55.7558, 37.6173]):
    """Создаём интерактивную карту с локациями"""
    
    m = folium.Map(location=center_coords, zoom_start=10)
    geolocator = Nominatim(user_agent="location_finder")
    
    for loc in locations:
        # Пытаемся получить координаты
        coords = loc.get('coordinates')
        
        if coords and coords['lat'] and coords['lon']:
            lat, lon = coords['lat'], coords['lon']
        else:
            # Геокодируем описание
            try:
                location = geolocator.geocode(loc['location_description'] + ", Россия")
                if location:
                    lat, lon = location.latitude, location.longitude
                else:
                    continue
            except:
                continue
        
        # Добавляем маркер
        popup_text = f"""
        {loc['type']}
{loc['location_description']}
Уверенность: {loc.get('confidence', 'N/A')}
Источников: {loc.get('source_count', 1)} """ folium.Marker( [lat, lon], popup=popup_text, tooltip=loc['type'] ).add_to(m) return m # Сохраняем карту map_obj = create_location_map(verified_locations) map_obj.save("secret_locations_map.html")

Типичные ошибки и как их избежать

ОшибкаПоследствияРешение
Слишком агрессивный скрапингБан IP, потеря доступа к даннымИмитируйте человеческое поведение, используйте прокси
Использование только векторного поискаПропуск точных совпадений (названия улиц)Добавьте гибридный поиск (BM25 + векторы)
temperature=0 в LLMМодель становится уверенной в ошибкахИспользуйте temperature=0.1-0.3
Отсутствие верификацииФейковые или опасные локации на картеТребуйте минимум 2 независимых источника
Хранение всего в памятиПадение при обработке 100k+ векторовИспользуйте векторные БД (Qdrant, Pinecone)

Что дальше? Эволюция поиска

Этот метод работает сегодня. Но через полгода соцсети изменят защиту. Через год появятся новые платформы. Что делать?

Первое: автоматизируйте мониторинг изменений. Пишите тесты, которые проверяют, работает ли скрапинг. Второе: экспериментируйте с новыми моделями. Только что вышла Llama 3.2 — она может быть лучше для вашей задачи. Третье: думайте о масштабировании.

Если ваш проект вырастет до миллиона постов, простой FAISS не справится. Нужны распределённые векторные базы. Нужен пайплайн, который сам обучает эмбеддинги на новых данных. Нужна система, которая определяет, когда модель "врёт" (об этом в статье Lost in the Middle).

Самый важный совет: не зацикливайтесь на технологии. Помните, зачем вы это делаете. Вы ищете секретные места не ради технологии, а ради открытий. Ради того, чтобы найти заброшенную церковь в лесу. Или смотровую площадку, с которой видно весь город на рассвете.

Технология — это инструмент. Карта — не территория. Идите и исследуйте.