This commit is contained in:
Тимур Абайдулин
2026-03-10 16:33:39 +03:00
commit 84b8246562
11 changed files with 1898 additions and 0 deletions

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
# Яндекс OAuth-токен (работает для Трекера и Вики)
YT=y0_...
ORG_ID=7405124
# Supabase
SUPABASE_HOST=aws-1-eu-north-1.pooler.supabase.com
SUPABASE_PORT=5432
SUPABASE_USER=postgres.xeakxxnriopsmaxdioke
SUPABASE_PASSWORD=YmyO2rmahTkfdV97
SUPABASE_DB=postgres
# OpenAI
OPENAI_API_KEY=sk-...

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
__pycache__/

141
README.md Normal file
View File

@@ -0,0 +1,141 @@
# wiki_embedding
Семантический поиск по страницам Яндекс Вики через pgvector + OpenAI embeddings.
## Что делает
- Обходит раздел «Управление Аналитики» Яндекс Вики через API и Playwright
- Сохраняет содержимое страниц в Supabase (PostgreSQL)
- Генерирует векторные embeddings через OpenAI `text-embedding-3-small`
- Позволяет делать семантический поиск по ~700 страницам на русском и английском
## Стек
| Компонент | Технология |
|-----------|-----------|
| Хранилище | Supabase (PostgreSQL 17) |
| Векторный поиск | pgvector, cosine similarity (`<=>`) |
| Embeddings | OpenAI `text-embedding-3-small` (1536 dims) |
| Wiki API | Яндекс Вики API v1 (`api.wiki.yandex.net/v1`) |
| Браузерный краулер | Playwright + Chromium headless |
| Язык | Python 3.12 |
## Структура файлов
```
wiki_embedding/
├── wiki_sync.py # Главный скрипт синхронизации (API → Supabase → embeddings)
├── wiki_embeddings.py # Генерация embeddings и семантический поиск
├── yandex_wiki.py # Краулер через Яндекс Вики API v1
├── wiki_tree_crawler.py # Playwright-краулер для страниц с {% tree %}
├── wiki_check_slugs.py # Проверка покрытия ROOT_SLUGS
├── wiki_auth.py # Сохранение браузерной сессии для Playwright
├── supabase.py # SupabaseManager — подключение и операции с БД
├── requirements.txt # Зависимости
└── .env # Credentials (не коммитить)
```
## Настройка
### 1. Установить зависимости
```bash
pip install -r requirements.txt
playwright install chromium
```
### 2. Создать .env
```
# Яндекс OAuth-токен (работает для Трекера и Вики)
YT=y0_...
ORG_ID=7405124
# Supabase
SUPABASE_HOST=aws-1-eu-north-1.pooler.supabase.com
SUPABASE_PORT=5432
SUPABASE_USER=postgres.xeakxxnriopsmaxdioke
SUPABASE_PASSWORD=...
SUPABASE_DB=postgres
# OpenAI
OPENAI_API_KEY=sk-...
```
### 3. Сохранить браузерную сессию (один раз)
```bash
python wiki_auth.py
```
Откроется браузер — залогинься в wiki.yandex.ru, нажми Enter.
## Запуск
### Полная синхронизация
```bash
python wiki_sync.py
```
Обходит все 642 slug-а из `ROOT_SLUGS`, обновляет `wiki_pages`, генерирует embeddings.
### Обнаружить новые страницы через Playwright
```bash
python wiki_tree_crawler.py
```
Открывает страницы с `{% tree %}` в headless-браузере, находит дочерние slug-и которых нет в базе.
### Проверить покрытие
```bash
python wiki_check_slugs.py
```
Показывает какие `{% tree %}` страницы не покрыты ROOT_SLUGS.
### Семантический поиск (из кода)
```python
import sys
sys.path.insert(0, '/Users/at/code/wiki_embedding')
from supabase import SupabaseManager
from wiki_embeddings import search
db = SupabaseManager()
db.connect()
results = search(db, 'твой запрос', limit=5)
for r in results:
print(r['similarity'], r['title'])
print(r['content_text'][:500])
db.close()
```
Similarity > 0.5 — хорошее совпадение.
## Cron (ежедневная синхронизация)
```
0 3 * * * cd /Users/at/code/wiki_embedding && /usr/bin/python3 wiki_sync.py >> logs/wiki_sync.log 2>&1
```
## Как устроен поиск
1. При индексировании каждая страница → вектор через OpenAI (title + content)
2. При поиске запрос → вектор тем же способом
3. pgvector находит страницы с минимальным косинусным расстоянием (`<=>`)
4. Возвращается `1 - distance` как similarity (0..1)
Точный поиск без индекса (IVFFlat не используется — при < 10k векторов даёт плохие результаты).
## Схема БД
```sql
-- Содержимое страниц
wiki_pages (id, slug, title, page_type, modified_at, content_hash, value JSONB)
-- Векторные embeddings
wiki_embeddings (id, slug, title, content_text, content_hash, embedding vector(1536))
```

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
openai==2.17.0
playwright==1.58.0
psycopg2-binary==2.9.11
python-dotenv==1.2.1
requests==2.32.5
pendulum==3.1.0

443
supabase.py Normal file
View File

@@ -0,0 +1,443 @@
"""
Модуль для работы с Supabase (PostgreSQL)
"""
import os
from pathlib import Path
import psycopg2
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / '.env')
class SupabaseManager:
"""Менеджер для работы с Supabase"""
def __init__(self):
"""Инициализация подключения к Supabase"""
self.host = os.getenv('SUPABASE_HOST')
self.port = os.getenv('SUPABASE_PORT')
self.user = os.getenv('SUPABASE_USER')
self.password = os.getenv('SUPABASE_PASSWORD')
self.database = os.getenv('SUPABASE_DB')
self.conn = None
self.cursor = None
def connect(self):
"""Подключение к базе данных"""
try:
self.conn = psycopg2.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
sslmode='require',
)
self.cursor = self.conn.cursor()
# Проверка версии
self.cursor.execute('SELECT version();')
db_version = self.cursor.fetchone()
print(f"Подключение к Supabase успешно!\nВерсия PostgreSQL: {db_version[0]}")
return True
except Exception as e:
print(f"✗ Ошибка подключения к Supabase: {e}")
return False
def close(self):
"""Закрытие соединения"""
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
print("✓ Соединение с базой данных закрыто")
# ============================================================================
# ПРОВЕРКА ТАБЛИЦ
# ============================================================================
def table_exists(self, table_name):
"""Проверка существования таблицы"""
self.cursor.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = %s
);
""", (table_name,))
return self.cursor.fetchone()[0]
def table_has_data(self, table_name):
"""Проверка, что таблица не пустая"""
self.cursor.execute(f"SELECT EXISTS (SELECT 1 FROM {table_name} LIMIT 1);")
return self.cursor.fetchone()[0]
# ============================================================================
# СОЗДАНИЕ ТАБЛИЦ
# ============================================================================
def create_employee_table(self):
"""Создание таблицы employee"""
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS employee (
id SERIAL PRIMARY KEY,
pg_load_dttm TIMESTAMPTZ NOT NULL,
yatracker_employee_id TEXT NOT NULL,
value JSONB NOT NULL
);
""")
self.conn.commit()
print("Таблица 'employee' создана")
def create_tasks_table(self):
"""Создание таблицы tasks"""
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
pg_load_dttm TIMESTAMPTZ NOT NULL,
queue TEXT,
task_key TEXT NOT NULL,
task_title TEXT,
status_name TEXT,
created_at_dttm TIMESTAMPTZ,
updated_at_dttm TIMESTAMPTZ,
status_start_dttm TIMESTAMPTZ,
value JSONB NOT NULL
);
""")
self.conn.commit()
print("Таблица 'tasks' создана")
def create_employee_info_table(self):
"""Создание таблицы employee_info"""
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS employee_info (
id SERIAL PRIMARY KEY,
pg_load_dttm TIMESTAMPTZ NOT NULL,
column_d TEXT,
column_n TEXT
);
""")
self.conn.commit()
print("Таблица 'employee_info' создана")
# ============================================================================
# ЗАГРУЗКА ДАННЫХ
# ============================================================================
def load_employee_data(self, employee_df):
"""Загрузка данных сотрудников в базу"""
if len(employee_df) > 0:
print("\nЗагружаем данные в таблицу 'employee'...")
for _, row in employee_df.iterrows():
self.cursor.execute("""
INSERT INTO employee (pg_load_dttm, yatracker_employee_id, value)
VALUES (%s, %s, %s::jsonb)
""", (row['pg_load_dttm'], row['yatracker_employee_id'], row['value']))
self.conn.commit()
print(f"✓ Загружено {len(employee_df)} записей в таблицу 'employee'")
else:
print("\nНет данных для загрузки в таблицу 'employee'")
def load_tasks_data(self, tasks_df):
"""Загрузка данных задач в базу"""
if len(tasks_df) > 0:
print("\nЗагружаем данные в таблицу 'tasks'...")
for _, row in tasks_df.iterrows():
self.cursor.execute("""
INSERT INTO tasks (pg_load_dttm, queue, task_key, task_title, status_name,
created_at_dttm, updated_at_dttm, status_start_dttm, value)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb)
""", (
row['pg_load_dttm'],
row['queue'],
row['task_key'],
row['task_title'],
row['status_name'],
row['created_at_dttm'],
row['updated_at_dttm'],
row['status_start_dttm'],
row['value']
))
self.conn.commit()
print(f"✓ Загружено {len(tasks_df)} записей в таблицу 'tasks'")
else:
print("\nНет данных для загрузки в таблицу 'tasks'")
def load_employee_info_data(self, employee_info_df):
"""Загрузка данных employee_info в базу"""
if len(employee_info_df) > 0:
print("\nЗагружаем данные в таблицу 'employee_info'...")
for _, row in employee_info_df.iterrows():
self.cursor.execute("""
INSERT INTO employee_info (pg_load_dttm, column_d, column_n)
VALUES (%s, %s, %s)
""", (
row['pg_load_dttm'],
row['column_d'],
row['column_n']
))
self.conn.commit()
print(f"✓ Загружено {len(employee_info_df)} записей в таблицу 'employee_info'")
else:
print("\nНет данных для загрузки в таблицу 'employee_info'")
# ============================================================================
# ОСНОВНАЯ ЛОГИКА ПРОВЕРКИ И ЗАГРУЗКИ
# ============================================================================
def check_and_prepare_tables(self):
"""
Проверка существования таблиц и создание при необходимости
Returns:
dict: Словарь с флагами наличия данных для каждой таблицы
"""
tables_status = {}
# Проверяем таблицу employee
employee_exists = self.table_exists('employee')
employee_has_data = False
if employee_exists:
employee_has_data = self.table_has_data('employee')
if employee_has_data:
print("Таблица 'employee' существует и содержит данные. Пропускаем загрузку.")
else:
print("Таблица 'employee' существует, но пустая.")
else:
self.create_employee_table()
tables_status['employee'] = employee_has_data
# Проверяем таблицу tasks
tasks_exists = self.table_exists('tasks')
tasks_has_data = False
if tasks_exists:
tasks_has_data = self.table_has_data('tasks')
if tasks_has_data:
print("Таблица 'tasks' существует и содержит данные. Пропускаем загрузку.")
else:
print("Таблица 'tasks' существует, но пустая.")
else:
self.create_tasks_table()
tables_status['tasks'] = tasks_has_data
# Проверяем таблицу employee_info
employee_info_exists = self.table_exists('employee_info')
employee_info_has_data = False
if employee_info_exists:
employee_info_has_data = self.table_has_data('employee_info')
if employee_info_has_data:
print("Таблица 'employee_info' существует и содержит данные. Пропускаем загрузку.")
else:
print("Таблица 'employee_info' существует, но пустая.")
else:
self.create_employee_info_table()
tables_status['employee_info'] = employee_info_has_data
return tables_status
def load_all_data(self, tables_status, employee=None, tasks=None, employee_info=None):
"""
Загрузка всех данных в базу
Args:
tables_status (dict): Статус таблиц из check_and_prepare_tables
employee (pd.DataFrame): Данные сотрудников
tasks (pd.DataFrame): Данные задач
employee_info (pd.DataFrame): Информация о сотрудниках
"""
if employee is not None and not tables_status['employee']:
self.load_employee_data(employee)
elif tables_status['employee']:
print("\nТаблица 'employee' уже содержит данные, пропускаем загрузку")
if tasks is not None and not tables_status['tasks']:
self.load_tasks_data(tasks)
elif tables_status['tasks']:
print("\nТаблица 'tasks' уже содержит данные, пропускаем загрузку")
if employee_info is not None and not tables_status['employee_info']:
self.load_employee_info_data(employee_info)
elif tables_status['employee_info']:
print("\nТаблица 'employee_info' уже содержит данные, пропускаем загрузку")
# ============================================================================
# ПОЛУЧЕНИЕ ДАННЫХ ДЛЯ АНАЛИЗА
# ============================================================================
def get_tasks_for_analysis(self, task_keys=None):
"""
Получение задач для анализа с описанием и исполнителем
Args:
task_keys (list): Список ключей задач для фильтрации (опционально)
Returns:
list: Список словарей с данными задач
"""
if task_keys:
# Формируем строку с ключами для SQL IN
keys_str = ", ".join([f"'{key}'" for key in task_keys])
query = f"""
SELECT *
FROM
(SELECT
t.queue,
t.task_key,
t.task_title,
t.value -> 'description' as description,
t.value -> 'assignee' ->> 'display' as assignee,
row_number() over (partition by task_key order by updated_at_dttm desc) as rn
FROM tasks t
WHERE task_key IN ({keys_str})) T1
WHERE rn = 1;
"""
else:
query = """
SELECT
t.queue,
t.task_key,
t.task_title,
t.value -> 'description' as description,
t.value -> 'assignee' ->> 'display' as assignee
FROM tasks t
ORDER BY t.updated_at_dttm DESC
LIMIT 100;
"""
self.cursor.execute(query)
columns = [desc[0] for desc in self.cursor.description]
rows = self.cursor.fetchall()
tasks = []
for row in rows:
task_dict = dict(zip(columns, row))
tasks.append(task_dict)
return tasks
# ============================================================================
# WIKI PAGES
# ============================================================================
def create_wiki_pages_table(self):
"""Создание таблицы wiki_pages"""
self.cursor.execute("""
CREATE TABLE IF NOT EXISTS wiki_pages (
id SERIAL PRIMARY KEY,
pg_load_dttm TIMESTAMPTZ NOT NULL,
slug TEXT NOT NULL,
wiki_page_id INTEGER,
title TEXT,
page_type TEXT,
modified_at TIMESTAMPTZ,
content_hash TEXT NOT NULL,
value JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_wiki_pages_slug
ON wiki_pages (slug);
CREATE INDEX IF NOT EXISTS idx_wiki_pages_load
ON wiki_pages (pg_load_dttm DESC);
""")
self.conn.commit()
print("Таблица 'wiki_pages' создана")
def get_latest_hashes(self) -> dict[str, str]:
"""Получить последний content_hash для каждого slug."""
self.cursor.execute("""
SELECT DISTINCT ON (slug) slug, content_hash
FROM wiki_pages
ORDER BY slug, pg_load_dttm DESC;
""")
return {row[0]: row[1] for row in self.cursor.fetchall()}
def upsert_wiki_pages(self, pages: list[dict]) -> dict:
"""
Вставить новые и изменённые страницы.
Логика: сравниваем content_hash с последним сохранённым.
Если хеш совпадает — пропускаем. Если новый или изменился — вставляем.
Returns:
dict: {'inserted': int, 'unchanged': int}
"""
import json
if not pages:
return {'inserted': 0, 'unchanged': 0}
if not self.table_exists('wiki_pages'):
self.create_wiki_pages_table()
latest_hashes = self.get_latest_hashes()
inserted = 0
unchanged = 0
for page in pages:
slug = page['slug']
new_hash = page['content_hash']
stored_hash = latest_hashes.get(slug)
if stored_hash == new_hash:
unchanged += 1
continue
value = {
'slug': slug,
'wiki_page_id': page.get('wiki_page_id'),
'title': page.get('title'),
'page_type': page.get('page_type'),
'modified_at': str(page.get('modified_at') or ''),
'content': page.get('content', ''),
}
self.cursor.execute("""
INSERT INTO wiki_pages
(pg_load_dttm, slug, wiki_page_id, title, page_type, modified_at, content_hash, value)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s::jsonb)
""", (
page['pg_load_dttm'],
slug,
page.get('wiki_page_id'),
page.get('title'),
page.get('page_type'),
page.get('modified_at'),
new_hash,
json.dumps(value, ensure_ascii=False),
))
inserted += 1
self.conn.commit()
return {'inserted': inserted, 'unchanged': unchanged}
def execute_sql_file(self, sql_file_path):
"""
Выполнение SQL запроса из файла
Args:
sql_file_path (str): Путь к файлу с SQL запросом
Returns:
list: Список словарей с результатами
"""
with open(sql_file_path, 'r', encoding='utf-8') as f:
query = f.read()
self.cursor.execute(query)
columns = [desc[0] for desc in self.cursor.description]
rows = self.cursor.fetchall()
results = []
for row in rows:
result_dict = dict(zip(columns, row))
results.append(result_dict)
return results

36
wiki_auth.py Normal file
View File

@@ -0,0 +1,36 @@
"""
Сохранить сессию wiki.yandex.ru для последующего использования Playwright.
Запуск (один раз):
python wiki_auth.py
Откроется браузер — залогинься в wiki.yandex.ru, затем нажми Enter в терминале.
Cookies сохранятся в wiki_auth.json.
"""
from playwright.sync_api import sync_playwright
from pathlib import Path
AUTH_FILE = Path(__file__).parent / 'wiki_auth.json'
def main():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
print('Открываю wiki.yandex.ru...')
page.goto('https://wiki.yandex.ru')
print()
print('Залогинься в браузере, затем вернись сюда и нажми Enter.')
input('Нажми Enter когда будешь залогинен: ')
context.storage_state(path=str(AUTH_FILE))
browser.close()
print(f'✓ Сессия сохранена в {AUTH_FILE}')
if __name__ == '__main__':
main()

78
wiki_check_slugs.py Normal file
View File

@@ -0,0 +1,78 @@
"""
Проверка покрытия ROOT_SLUGS: находит страницы в wiki_pages с {% tree %},
которые не добавлены в ROOT_SLUGS — у них могут быть необнаруженные дочерние страницы.
Запуск:
python wiki_check_slugs.py
"""
import sys
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / '.env')
sys.path.insert(0, str(Path(__file__).parent))
from supabase import SupabaseManager
# ROOT_SLUGS из wiki_sync.py — поддерживай синхронно
from wiki_sync import ROOT_SLUGS
def main():
db = SupabaseManager()
if not db.connect():
sys.exit(1)
try:
# Все slug-и в базе
db.cursor.execute("SELECT slug FROM wiki_pages ORDER BY slug")
all_slugs = {row[0] for row in db.cursor.fetchall()}
# Страницы с {% tree %} — у них могут быть необнаруженные дети
db.cursor.execute("""
SELECT slug, value->>'title' AS title
FROM wiki_pages
WHERE value->>'content' LIKE '%{%% tree%%}%'
ORDER BY slug
""")
tree_pages = db.cursor.fetchall()
finally:
db.close()
root_slugs_set = set(ROOT_SLUGS)
# Страницы с {% tree %}, которых нет в ROOT_SLUGS
missing_from_roots = [(s, t) for s, t in tree_pages if s not in root_slugs_set]
# ROOT_SLUGS которых нет в базе (удалены или никогда не синхронизировались)
missing_from_db = root_slugs_set - all_slugs
print(f'Всего страниц в wiki_pages: {len(all_slugs)}')
print(f'Страниц с {{% tree %}}: {len(tree_pages)}')
print(f'ROOT_SLUGS: {len(ROOT_SLUGS)}')
print()
if missing_from_roots:
print('⚠️ Страницы с {% tree %}, НЕ добавленные в ROOT_SLUGS:')
print(' (у них могут быть дочерние страницы, которые краулер не найдёт)')
print()
for slug, title in missing_from_roots:
print(f' + {slug}')
print(f' «{title}»')
print(f' https://wiki.yandex.ru/{slug}')
print()
else:
print('Все страницы с {% tree %} уже есть в ROOT_SLUGS')
if missing_from_db:
print('⚠️ ROOT_SLUGS которых НЕТ в базе (не были синхронизированы или удалены):')
for slug in sorted(missing_from_db):
print(f' - {slug}')
print()
else:
print('Все ROOT_SLUGS присутствуют в базе')
if __name__ == '__main__':
main()

171
wiki_embeddings.py Normal file
View File

@@ -0,0 +1,171 @@
"""
Семантический поиск по Яндекс Вики через OpenAI embeddings + pgvector.
"""
import re
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv(Path(__file__).parent / '.env')
EMBED_MODEL = 'text-embedding-3-small' # 1536 dims, быстро и дёшево
_client = None
def _openai() -> OpenAI:
global _client
if _client is None:
_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
return _client
def clean_wiki_markup(text: str) -> str:
"""Убрать wiki-разметку, оставить чистый текст для embedding."""
if not text:
return ''
# {% ... %} блоки
text = re.sub(r'\{%[^%]*%\}', '', text)
# [[ссылки]] → оставить текст после |
text = re.sub(r'\[\[([^\]|]*)\|([^\]]*)\]\]', r'\2', text)
text = re.sub(r'\[\[([^\]]*)\]\]', r'\1', text)
# ((url текст)) → текст
text = re.sub(r'\(\(https?://\S+\s+([^)]+)\)\)', r'\1', text)
text = re.sub(r'\(\(https?://\S+\)\)', '', text)
# Markdown-разметка
text = re.sub(r'\*{1,3}([^*]+)\*{1,3}', r'\1', text)
text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text)
# Таблицы и спец-символы wiki
text = re.sub(r'[#|]{2,}', '\n', text)
text = re.sub(r'^\s*[#>]+\s*', '', text, flags=re.MULTILINE)
# Лишние пробелы
text = re.sub(r'\n{3,}', '\n\n', text)
return text.strip()
def embed_text(text: str) -> list[float]:
"""Получить embedding для текста через OpenAI API."""
import openai as _openai_module
# Начинаем с 15000 символов, при ошибке обрезаем вдвое
limit = 15000
while limit >= 1000:
try:
resp = _openai().embeddings.create(model=EMBED_MODEL, input=text[:limit])
return resp.data[0].embedding
except _openai_module.BadRequestError as e:
if 'maximum context length' in str(e):
limit = limit // 2
continue
raise
raise ValueError(f'Не удалось уложиться в лимит токенов даже при 1000 символах')
def embed_page(page: dict) -> list[float]:
"""Сгенерировать embedding для страницы (title + content)."""
title = page.get('title', '')
content = clean_wiki_markup(page.get('content', '') or '')
combined = f'{title}\n\n{content}'
return embed_text(combined)
def upsert_embeddings(db, pages: list[dict]) -> dict:
"""
Сгенерировать и сохранить embeddings для новых/изменённых страниц.
Пропускает страницы, у которых content_hash не изменился.
Returns:
dict: {'embedded': int, 'skipped': int}
"""
import json
import pendulum
if not pages:
return {'embedded': 0, 'skipped': 0}
# Получить текущие хеши из wiki_embeddings
db.cursor.execute('SELECT slug, content_hash FROM wiki_embeddings')
stored = {row[0]: row[1] for row in db.cursor.fetchall()}
embedded = 0
skipped = 0
for page in pages:
slug = page['slug']
new_hash = page['content_hash']
content = page.get('content', '') or ''
if not content.strip():
skipped += 1
continue
if stored.get(slug) == new_hash:
skipped += 1
continue
print(f' ↑ embedding: {slug}')
content_text = clean_wiki_markup(content)
vector = embed_page({'title': page.get('title', ''), 'content': content})
db.cursor.execute("""
INSERT INTO wiki_embeddings
(pg_load_dttm, slug, title, content_text, content_hash, embedding)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (slug) DO UPDATE SET
pg_load_dttm = EXCLUDED.pg_load_dttm,
title = EXCLUDED.title,
content_text = EXCLUDED.content_text,
content_hash = EXCLUDED.content_hash,
embedding = EXCLUDED.embedding
""", (
pendulum.now('Europe/Moscow'),
slug,
page.get('title', ''),
content_text,
new_hash,
json.dumps(vector),
))
db.conn.commit()
embedded += 1
return {'embedded': embedded, 'skipped': skipped}
def search(db, query: str, limit: int = 5) -> list[dict]:
"""
Семантический поиск по wiki_embeddings.
Args:
db: подключённый SupabaseManager
query: текстовый запрос на любом языке
limit: кол-во результатов
Returns:
Список {'slug', 'title', 'similarity', 'content_text'}
"""
import json
query_vec = embed_text(query)
db.cursor.execute("""
SELECT
slug,
title,
content_text,
1 - (embedding <=> %s::vector) AS similarity
FROM wiki_embeddings
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (json.dumps(query_vec), json.dumps(query_vec), limit))
rows = db.cursor.fetchall()
return [
{
'slug': row[0],
'title': row[1],
'content_text': row[2],
'similarity': round(float(row[3]), 4),
}
for row in rows
]

737
wiki_sync.py Normal file
View File

@@ -0,0 +1,737 @@
"""
Ежедневная синхронизация Яндекс Вики → Supabase.
Запуск:
python wiki_sync.py
Cron (каждый день в 03:00):
0 3 * * * cd /Users/at/python/tracker-checker && /usr/bin/python3 wiki_sync.py >> logs/wiki_sync.log 2>&1
"""
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
import pendulum
from yandex_wiki import crawl
from supabase import SupabaseManager
from wiki_embeddings import upsert_embeddings
load_dotenv(Path(__file__).parent / '.env')
# ── Настройки ────────────────────────────────────────────────────────────────
API_KEY = os.getenv('API_KEY') or os.getenv('YT')
ORG_ID = os.getenv('ORG_ID') or os.getenv('ORG')
# Корневые разделы для обхода — добавляй нужные slug-и
ROOT_SLUGS = [
# Корневой раздел
'upravlenie-analitiki',
# Процессы УА (дети рендерятся через {% tree %}, добавляй slug вручную когда откроешь в браузере)
'upravlenie-analitiki/processy-ua',
# Продукты УА и все найденные подразделы
'upravlenie-analitiki/produkty-ua',
'upravlenie-analitiki/bdd',
'upravlenie-analitiki/analiticheskaja-podderzhka',
'upravlenie-analitiki/onboarding',
'upravlenie-analitiki/antifraud',
'upravlenie-analitiki/research-analytics',
'upravlenie-analitiki/dlja-sotrudnikov-ua',
'upravlenie-analitiki/operacionnaja-analitika',
'upravlenie-analitiki/biznes-analitika',
'upravlenie-analitiki/produktovaja-analitika-kpa',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti',
'upravlenie-analitiki/logirovanie-mp',
'upravlenie-analitiki/produktovaja-analitika-kpo',
# Добавленные вручную по URL
'upravlenie-analitiki/platforma-po-av-testam',
'upravlenie-analitiki/kak-podat-zajavku-analitikam',
'upravlenie-analitiki/put-novichka',
'upravlenie-analitiki/dutykotiki',
'upravlenie-analitiki/processy-ua/dq-process',
'upravlenie-analitiki/processy-ua/emk-process',
'upravlenie-analitiki/processy-ua/process-revju-issledovatelskix-zadach',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie',
# Аналитическая поддержка (дети через {% tree %})
'upravlenie-analitiki/analiticheskaja-podderzhka/ab-testy',
'upravlenie-analitiki/analiticheskaja-podderzhka/komanda',
'upravlenie-analitiki/analiticheskaja-podderzhka/dashbordy',
'upravlenie-analitiki/analiticheskaja-podderzhka/issledovanija-i-logi',
'upravlenie-analitiki/analiticheskaja-podderzhka/ad-hoc',
# Добавлено автоматически через wiki_tree_crawler.py
'upravlenie-analitiki/antifraud/bonusy-nachislennye-sotrudnikami-vruchnuju',
'upravlenie-analitiki/antifraud/bonusy-za-jekologicheskie-akcii',
'upravlenie-analitiki/antifraud/frod-na-kso-krazhi',
'upravlenie-analitiki/antifraud/frod-ot-ujazvimosti-mp-v-funkcionalnosti-skanirujj',
'upravlenie-analitiki/antifraud/frod-s-fejjkovymi-zakazami',
'upravlenie-analitiki/antifraud/frod-v-chaevyx',
'upravlenie-analitiki/antifraud/frod-v-limitax',
'upravlenie-analitiki/antifraud/frod-v-limitax/frod-v-limitax.-ruchnye-ogranichenija-menedzherov-',
'upravlenie-analitiki/antifraud/frod-v-otmenjonnyx-zakazax',
'upravlenie-analitiki/antifraud/frod-v-otmenjonnyx-zakazax/zakazy-cherez-storonnie-marketplejjsy',
'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti',
'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti/primenenie-kupona-s-limitom-ot-summy-n-rublejj-k-m',
'upravlenie-analitiki/antifraud/frod-v-programmax-lojalnosti/xaljavshhiki',
'upravlenie-analitiki/antifraud/frod-v-referalnojj-programme',
'upravlenie-analitiki/antifraud/frod-v-rejjtingax-i-otzyvax.-klientskijj-put-posta',
'upravlenie-analitiki/antifraud/frod-v-vozvratax',
'upravlenie-analitiki/antifraud/frod-v-vozvratax/dop.materialy',
'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod',
'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/jekonomika-antifroda-v-chasti-limitov',
'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/kak-schitat-soxranjonnye-antifrodom-sredstva',
'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/primenenie-kuponov-na-pervyjj-zakaz-bolee-1-raza',
'upravlenie-analitiki/antifraud/jekonomika-proekta-antifrod/soxranennye-dengi-v-chasti-vozvratov-2.0',
'upravlenie-analitiki/antifraud/klasterizacija-i-klassifikacija-klientov-v-vozvrat',
'upravlenie-analitiki/antifraud/lzhenovichki',
'upravlenie-analitiki/antifraud/lzhenovichki/analitika-vremeni-zhizni-lzhe-novichkov',
'upravlenie-analitiki/antifraud/massovost-vozvratov-pervyx-zakazov-po-partnerskojj',
'upravlenie-analitiki/antifraud/raschet-marzhinalnosti-po-froderam',
'upravlenie-analitiki/antifraud/rasskazhi-o-frode',
'upravlenie-analitiki/antifraud/skanirovanie-tovarov-s-dvojjnym-shtrixkodom',
'upravlenie-analitiki/antifraud/summarnye-dannye-za-24-jj-god-po-obemu-vozvratovbo',
'upravlenie-analitiki/bdd/blok-analiticheskaja-kultura-i-standarty',
'upravlenie-analitiki/bdd/blok-dannye',
'upravlenie-analitiki/bdd/blok-infrastruktura',
'upravlenie-analitiki/bdd/manifest-bdd',
'upravlenie-analitiki/bdd/porazgonjat',
'upravlenie-analitiki/bdd/porazgonjat/perechen-slozhnyx-proektov',
'upravlenie-analitiki/bi-analitiki',
'upravlenie-analitiki/bi-analitiki/arxiv',
'upravlenie-analitiki/bi-analitiki/arxivacija-otchetov',
'upravlenie-analitiki/bi-analitiki/kritichnost-dashbordov---standart',
'upravlenie-analitiki/bi-analitiki/obeshhanija-komandy-bi',
'upravlenie-analitiki/bi-analitiki/onbording-novichkov',
'upravlenie-analitiki/bi-analitiki/opisanie-otchetov',
'upravlenie-analitiki/bi-analitiki/plany-razvitija',
'upravlenie-analitiki/bi-analitiki/poleznye-resursy',
'upravlenie-analitiki/bi-analitiki/self-servisy-ua-vv-self-service',
'upravlenie-analitiki/bi-analitiki/standarty-komandy-bi',
'upravlenie-analitiki/bi-analitiki/vnutrennjaja-dokumentacija',
'upravlenie-analitiki/bi-analitiki/xjendover-dashbordov-v-bi-komandu-ot-analitikov-ua',
'upravlenie-analitiki/biznes-analitika/zadachi-1',
'upravlenie-analitiki/dlja-sotrudnikov-ua/benefity-dlja-sotrudnikov',
'upravlenie-analitiki/dlja-sotrudnikov-ua/dlja-tex-kto-rabotaet-vne-rf',
'upravlenie-analitiki/dlja-sotrudnikov-ua/instrukcija-po-voinskomu-uchetu',
'upravlenie-analitiki/dlja-sotrudnikov-ua/poleznye-ssylki-dlja-obuchenija',
'upravlenie-analitiki/dlja-sotrudnikov-ua/uxod-v-otpusk-i-na-bolnichnyjj',
'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo',
'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/obuchenie-po-ponjatnomu-tekstu',
'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/rabota-s-jatrekerom',
'upravlenie-analitiki/dutykotiki/rabota-dezhurnogo/shablon-dlja-stati-po-vygruzke',
'upravlenie-analitiki/dutykotiki/segmentacii',
'upravlenie-analitiki/dutykotiki/segmentacii/segmentacija-clc',
'upravlenie-analitiki/dutykotiki/vygruzki',
'upravlenie-analitiki/dutykotiki/vygruzki/11',
'upravlenie-analitiki/dutykotiki/vygruzki/1c-napitki',
'upravlenie-analitiki/dutykotiki/vygruzki/2f70ef29029b',
'upravlenie-analitiki/dutykotiki/vygruzki/3-gruppa-roznicy-pokazateli-lp-i-abonement',
'upravlenie-analitiki/dutykotiki/vygruzki/414dcd3c2b4c',
'upravlenie-analitiki/dutykotiki/vygruzki/4814eb73fc5e',
'upravlenie-analitiki/dutykotiki/vygruzki/6a6a82721362',
'upravlenie-analitiki/dutykotiki/vygruzki/6ff40875fcba',
'upravlenie-analitiki/dutykotiki/vygruzki/a1e2856653b5',
'upravlenie-analitiki/dutykotiki/vygruzki/adresa-kontragentov-postavshhikov',
'upravlenie-analitiki/dutykotiki/vygruzki/aktualnyjj-spisok-pozicijj-texnologa',
'upravlenie-analitiki/dutykotiki/vygruzki/analitika-novyx-torgovyx-tochek',
'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-magazinu-7514',
'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-produktam-iz-rastitelnojj-linejjki',
'upravlenie-analitiki/dutykotiki/vygruzki/analitika-po-zakazam-na-zavtra',
'upravlenie-analitiki/dutykotiki/vygruzki/analitika-proniknovenija-kategorijj-v-chek',
'upravlenie-analitiki/dutykotiki/vygruzki/analiz-aktivnyx-darkstorov-po-otgruzkam-kontragent',
'upravlenie-analitiki/dutykotiki/vygruzki/analiz-aktivnyx-darkstorov-po-otgruzkam-kontragent-19-11',
'upravlenie-analitiki/dutykotiki/vygruzki/analiz-kommunikacijj-dlja-olesi-kashicynojj-b2b',
'upravlenie-analitiki/dutykotiki/vygruzki/analiz-ottoka',
'upravlenie-analitiki/dutykotiki/vygruzki/analiz-pokupatelejj-assortimenta-podgruppy-morozhe',
'upravlenie-analitiki/dutykotiki/vygruzki/apteki-kol-vo-gen.zakazov-vyruchka-s-vozvratami-i-',
'upravlenie-analitiki/dutykotiki/vygruzki/asr-po-kartam',
'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-1s-napitki-alkogol',
'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-kategorii-supermarket-s-prinadlezhnost',
'upravlenie-analitiki/dutykotiki/vygruzki/assortiment-tovarov',
'upravlenie-analitiki/dutykotiki/vygruzki/avgmedianchecks',
'upravlenie-analitiki/dutykotiki/vygruzki/b2b---klienty-s-dvumja-i-bolee-zakazami',
'upravlenie-analitiki/dutykotiki/vygruzki/barista-i-dr.dolzhnosti-kotorye-rabotali-s-01.01.2',
'upravlenie-analitiki/dutykotiki/vygruzki/bonus-bags',
'upravlenie-analitiki/dutykotiki/vygruzki/bonusnye-karty-sotrudnikov',
'upravlenie-analitiki/dutykotiki/vygruzki/bonusnye-karty-zapolnivshie-formu-registracii-dlja',
'upravlenie-analitiki/dutykotiki/vygruzki/bonusy-za-utilizaciju-s-telefonami-i-kartami-pokup',
'upravlenie-analitiki/dutykotiki/vygruzki/ceny-tovarov-dobrojj-polki',
'upravlenie-analitiki/dutykotiki/vygruzki/chastota-pokupok-tovarov-v-kategorii-suxofrukty',
'upravlenie-analitiki/dutykotiki/vygruzki/cheki-s-nds-i-bez-nds-po-partnerskojj-programme-s-',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-dlja-rolika-o-vv---otkrytye-tt-novye-goroda',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-kogorty-novichkov-po-partnjorskojj-programm',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-o-pokupkax-v-magazinax-po-kartamtelefonam',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-nomenklature',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-pokupateljam-dobrojj-polki',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-po-prodazham-alkogolja-offlajjn',
'upravlenie-analitiki/dutykotiki/vygruzki/dannye-za-god',
'upravlenie-analitiki/dutykotiki/vygruzki/data-degustacii-i-prodazhi-po-ee-itogam',
'upravlenie-analitiki/dutykotiki/vygruzki/daty-podkljuchenija-tt-k-polochnomu-ili-67-grupp-r',
'upravlenie-analitiki/dutykotiki/vygruzki/dinamika-kart-dlja-kategorii-mjasnye-delikatesy.ko',
'upravlenie-analitiki/dutykotiki/vygruzki/dinamika-prodazh-desertnojj-i-xlebnojj-kategorii',
'upravlenie-analitiki/dutykotiki/vygruzki/dlja-informirovanija-roznicy',
'upravlenie-analitiki/dutykotiki/vygruzki/dolja-tovarov-indilavki-v-korzine-ambassadora',
'upravlenie-analitiki/dutykotiki/vygruzki/dolja-zakazov-s-suxojj-polkojj-zamorozkojj-sgorjac',
'upravlenie-analitiki/dutykotiki/vygruzki/dostavka-4-chasa-i-dachnyjj-jekspress---nomera-tel',
'upravlenie-analitiki/dutykotiki/vygruzki/dovozy',
'upravlenie-analitiki/dutykotiki/vygruzki/e-maily-tt-gde-est-kassiry',
'upravlenie-analitiki/dutykotiki/vygruzki/file-grudki-indejjki',
'upravlenie-analitiki/dutykotiki/vygruzki/frov-vygruzka-vyruchki-po-dnjam',
'upravlenie-analitiki/dutykotiki/vygruzki/good-polka',
'upravlenie-analitiki/dutykotiki/vygruzki/httpsbdo.kaiten.ruspace63173card35882806',
'upravlenie-analitiki/dutykotiki/vygruzki/informacija-o-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/jekstremalnye-obrashhenija',
'upravlenie-analitiki/dutykotiki/vygruzki/kart-po-testeram',
'upravlenie-analitiki/dutykotiki/vygruzki/karty---novichki',
'upravlenie-analitiki/dutykotiki/vygruzki/karty-b2b-s-limitom',
'upravlenie-analitiki/dutykotiki/vygruzki/karty-i-zakazy-po-dobrym-pokupkam',
'upravlenie-analitiki/dutykotiki/vygruzki/karty-po-dobrojj-polke-i-jeko',
'upravlenie-analitiki/dutykotiki/vygruzki/karty-polzovatelejj-u-kogo-byla-vkljuchena-nastroj',
'upravlenie-analitiki/dutykotiki/vygruzki/karty-uchastnikov-akcii-knopsy',
'upravlenie-analitiki/dutykotiki/vygruzki/kategorii-osnovnye-metriki',
'upravlenie-analitiki/dutykotiki/vygruzki/kogorta-partnjorov-partnjorskojj-programmy-razdele',
'upravlenie-analitiki/dutykotiki/vygruzki/kol-vo-onlajjnoflajjn-polzovatelejj-po-asr',
'upravlenie-analitiki/dutykotiki/vygruzki/kol-vo-unikalnyx-kart-pokupatelejj-kosmetiki',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-kulinarija-vv',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-s-kso-po-rjadu-magazinov',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-chekov-v-razreze-kass',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-obrashhenijj-iz-arxiva',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-obrashhenijj-po-tegam-smena-adresa-i-o',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-pokupok-torty-pirozhnye-s-13-po-15-fev',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-prodazh-36161-i-35085',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-tt-i-novye-goroda-prisutstvija-po-kodu',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-tt-po-okrugam',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-uchastnikov-blagotvoritelnojj-akcii',
'upravlenie-analitiki/dutykotiki/vygruzki/kolichestvo-unikalnyx-kart',
'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-dlja-sborshhika.-frov-vesovye',
'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-i-zhaloby-k-zakazam-c-14-po-20-avgusta',
'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-k-zakazu',
'upravlenie-analitiki/dutykotiki/vygruzki/kommentarii-o-proizvoditeljax',
'upravlenie-analitiki/dutykotiki/vygruzki/korzina-pokupatelejj-po-adresu-dostavki',
'upravlenie-analitiki/dutykotiki/vygruzki/limity-arendy-v-1s-finansy---operacii---registr-sv',
'upravlenie-analitiki/dutykotiki/vygruzki/metod-avtorizacii-na-kso-kassa-samoobsluzhivanija',
'upravlenie-analitiki/dutykotiki/vygruzki/molochnye-kategorii-po-vozrastam',
'upravlenie-analitiki/dutykotiki/vygruzki/morozhenoe',
'upravlenie-analitiki/dutykotiki/vygruzki/nachislennye-bonusy--cheki',
'upravlenie-analitiki/dutykotiki/vygruzki/nochnaja-dostavka-zakaz-i-dostavka-osushhestvljali',
'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatura-chestnyjj-znak-za-2023-g',
'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatura-veganstvo--prodakt-menedzher',
'upravlenie-analitiki/dutykotiki/vygruzki/nomenklatury-novinki',
'upravlenie-analitiki/dutykotiki/vygruzki/nomera-bonusnyx-kart',
'upravlenie-analitiki/dutykotiki/vygruzki/nomera-bonusnyx-kart-projavljajushhix-aktivnost-v-',
'upravlenie-analitiki/dutykotiki/vygruzki/nomera-i-daty-dogovorov-b2b',
'upravlenie-analitiki/dutykotiki/vygruzki/nomera-kart-po-vozrastam-18-24-25-34',
'upravlenie-analitiki/dutykotiki/vygruzki/nomera-oshibok-oplat-v-obrashhenijax-pokupatelejj',
'upravlenie-analitiki/dutykotiki/vygruzki/nomerov-bonusnyx-kart-po-tovaru-tvorog-detskijj-ja',
'upravlenie-analitiki/dutykotiki/vygruzki/nomerov-kuratorov-i-pomoshhnikov-dostavki',
'upravlenie-analitiki/dutykotiki/vygruzki/novichki',
'upravlenie-analitiki/dutykotiki/vygruzki/novichki-dlja-darkstorov-gruppy-3',
'upravlenie-analitiki/dutykotiki/vygruzki/novichki-oflajjn-i-novichki-seti',
'upravlenie-analitiki/dutykotiki/vygruzki/novichki-v-onlajjne--top-10-v-pervom-cheke-novichk',
'upravlenie-analitiki/dutykotiki/vygruzki/novinki-v-razbivke-po-mesjacam-kol-vo-tt-i-kol-vo-',
'upravlenie-analitiki/dutykotiki/vygruzki/obogashhenie-vygruzki-dannymi-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-33-tipa-ot-neavtorizovannyx-polzovate',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-degustacii-v-magazine',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-moloku',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-nomeram-kart',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-syru',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-po-upakovke-i-jetiketkam',
'upravlenie-analitiki/dutykotiki/vygruzki/obrashhenija-pokupatelejj-po-kategorijam-v-razreze',
'upravlenie-analitiki/dutykotiki/vygruzki/obratnaja-svjaz-po-zootovaram',
'upravlenie-analitiki/dutykotiki/vygruzki/opredelenie-sotrudnikane-sotrudnika-po-nomeru-kart',
'upravlenie-analitiki/dutykotiki/vygruzki/otchet-po-medikamentam',
'upravlenie-analitiki/dutykotiki/vygruzki/otchjot-po-otdelu-bez-upakovki-za-janvar-ijul-2024',
'upravlenie-analitiki/dutykotiki/vygruzki/otdel-bez-upakovki-srednijj-chek',
'upravlenie-analitiki/dutykotiki/vygruzki/otgruzki-s-tt-klientov-b2b',
'upravlenie-analitiki/dutykotiki/vygruzki/otsrochka-platezha-po-ka',
'upravlenie-analitiki/dutykotiki/vygruzki/ottokklienty-8048',
'upravlenie-analitiki/dutykotiki/vygruzki/otzyvy-pokupatelejj-na-cenu',
'upravlenie-analitiki/dutykotiki/vygruzki/parsing-tt-po-geozonam',
'upravlenie-analitiki/dutykotiki/vygruzki/partnjory-partnjorskojj-programmy-region',
'upravlenie-analitiki/dutykotiki/vygruzki/perekljuchenie-pozicijj',
'upravlenie-analitiki/dutykotiki/vygruzki/peresechenie-auditorijj-po-lukumu',
'upravlenie-analitiki/dutykotiki/vygruzki/peresechenie-po-pokupkam-vafel-sladosti',
'upravlenie-analitiki/dutykotiki/vygruzki/pokazateli-po-proektu-indilavka',
'upravlenie-analitiki/dutykotiki/vygruzki/pokupateli-mango',
'upravlenie-analitiki/dutykotiki/vygruzki/pokupateli-restoranov',
'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-u-partnjorov',
'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-v-indilavke-posle-festivalja',
'upravlenie-analitiki/dutykotiki/vygruzki/pokupki-vo-vv-u-kart-kotorye-pokupali-na-obed.ru',
'upravlenie-analitiki/dutykotiki/vygruzki/polzovateli-dlja-achivki',
'upravlenie-analitiki/dutykotiki/vygruzki/polzovateli-mobilnogo-prilozhenija-kotorye-ne-sove',
'upravlenie-analitiki/dutykotiki/vygruzki/poschitat-kolichestvo-stolovyx-priborov-v-zakazax-',
'upravlenie-analitiki/dutykotiki/vygruzki/poslednie-cheki-batarejjki--bonusy-za-utilizaciju',
'upravlenie-analitiki/dutykotiki/vygruzki/postavshhiki-i-stoimost-pechatnojj-upakovki',
'upravlenie-analitiki/dutykotiki/vygruzki/postavshhiki-piva',
'upravlenie-analitiki/dutykotiki/vygruzki/povtornye-pokupki-indilavka',
'upravlenie-analitiki/dutykotiki/vygruzki/povtornye-zakazy-iz-partnjorki-s-primenenie-promok',
'upravlenie-analitiki/dutykotiki/vygruzki/pp-vv-analiz-reklamy-esh-uchis-tvori',
'upravlenie-analitiki/dutykotiki/vygruzki/prichiny-vozvratov-po-segmentam',
'upravlenie-analitiki/dutykotiki/vygruzki/privlechjonnye-novichki-v-indilavku',
'upravlenie-analitiki/dutykotiki/vygruzki/procentnoe-sootnoshenie-shtatnyx-sotrudnikov-i-aut',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-bytovojj-ximii-na-rozliv',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-deserta',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-kulinarii-i-kulinarii-rp-na-2-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-kulinarii-vv-po-nedeljam',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-lavka-vkusa---otbornoe',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-onlajjn-v-moskve',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-po-jajjcu-v-period-pasxi',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-spisanija-zc-po-tov.pozicijam-kotorye-vyv',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-termostakana-za-poslednie-polgoda',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-aptek',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-dobraja-polka-so-skidkami',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-so-stavkojj-nds-10',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tovarov-supermarketa-na-vajjldberriz-wild',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-trex-tovarov-za-poslednijj-god',
'upravlenie-analitiki/dutykotiki/vygruzki/prodazhi-tt-bez-uchjota-v-cheklajjnax-tovarov-iz-k',
'upravlenie-analitiki/dutykotiki/vygruzki/promokod-krasota--info-po-istochnikam-po-promokoda',
'upravlenie-analitiki/dutykotiki/vygruzki/proschet-vozvratnosti-i-chastoty-pokupok-unikalnyx',
'upravlenie-analitiki/dutykotiki/vygruzki/proverit-kartochki-pokupatelejj-na-nalichie-zakazo',
'upravlenie-analitiki/dutykotiki/vygruzki/proverka-sebestoimosti-komplektov',
'upravlenie-analitiki/dutykotiki/vygruzki/raschet-sr-kolva-zakazov',
'upravlenie-analitiki/dutykotiki/vygruzki/rasshirennaja-vygruzka-po-zakazam-b2b-klientov',
'upravlenie-analitiki/dutykotiki/vygruzki/razmernost-upakovok-skju',
'upravlenie-analitiki/dutykotiki/vygruzki/segmentacija-onlajjnoflajjn-po-kartam',
'upravlenie-analitiki/dutykotiki/vygruzki/shtrixkody-na-nomenklatury',
'upravlenie-analitiki/dutykotiki/vygruzki/skidka-kontragentov.-sbor-bazy',
'upravlenie-analitiki/dutykotiki/vygruzki/skoroportjashhijjsja-assortiment-i-ego-postavshhik',
'upravlenie-analitiki/dutykotiki/vygruzki/sneki-vygruzka-kart-dlja-gruppy-tovarov-dlja-vozra',
'upravlenie-analitiki/dutykotiki/vygruzki/sootnesti-nomera-telefonov-i-nomera-kart-vv',
'upravlenie-analitiki/dutykotiki/vygruzki/sootnoshenie-chisla-klientov-v-razreze-asr',
'upravlenie-analitiki/dutykotiki/vygruzki/sostav-chekov-v-kotoryx-est-tovary-iz-kategorii-ku',
'upravlenie-analitiki/dutykotiki/vygruzki/sostavy-i-postavshhiki-dlja-napitkov-i-alkogolja',
'upravlenie-analitiki/dutykotiki/vygruzki/sostavy-produkcii',
'upravlenie-analitiki/dutykotiki/vygruzki/sotrudniki-kotorye-ustroilis-do-01.01.2013-i-rabot',
'upravlenie-analitiki/dutykotiki/vygruzki/spisanija-po-degustacii-texnolog-proverka-kachestv',
'upravlenie-analitiki/dutykotiki/vygruzki/spisanija-s-tt-i-onlajjn-prodazhi-po-murmansku',
'upravlenie-analitiki/dutykotiki/vygruzki/spisaniyapostavki',
'upravlenie-analitiki/dutykotiki/vygruzki/spisok-aktualnojj-nomenklatury',
'upravlenie-analitiki/dutykotiki/vygruzki/spisok-nomenklatury-po-opredelennomu-prodakt-mened',
'upravlenie-analitiki/dutykotiki/vygruzki/spisok-tovarov-dlja-mobilnogo-prilozhenija-eatfit',
'upravlenie-analitiki/dutykotiki/vygruzki/spisok-tt-s-priznakom-kafe',
'upravlenie-analitiki/dutykotiki/vygruzki/spisok-zakazov-iz-torgovojj-tochki-7087ds-bbr-239',
'upravlenie-analitiki/dutykotiki/vygruzki/sposob-prigotovlenija-v-lichnom-kabinete-postavshh',
'upravlenie-analitiki/dutykotiki/vygruzki/sravnenie-prodazh-dvux-vidov-makaron',
'upravlenie-analitiki/dutykotiki/vygruzki/srednee-i-mediannoe-kolichestvo-chekov-v-moskve-i-',
'upravlenie-analitiki/dutykotiki/vygruzki/srednee-kol-vo-tovara-v-cheke',
'upravlenie-analitiki/dutykotiki/vygruzki/srednee-kolichestvo-paketov-v-zakazax-partnera-sbe',
'upravlenie-analitiki/dutykotiki/vygruzki/srednjaja-stoimost-pokupki-i-chastota-pokupki-xurm',
'upravlenie-analitiki/dutykotiki/vygruzki/srednjaja-summa-realizovannyx-tovarov--summa-ostat',
'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-kolichestvu-zakazov-kategorii-gorjac',
'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-prodazham',
'upravlenie-analitiki/dutykotiki/vygruzki/statistika-po-slitym-kartam',
'upravlenie-analitiki/dutykotiki/vygruzki/statistika-sbora-paketov-i-kryshek',
'upravlenie-analitiki/dutykotiki/vygruzki/statusy-postavshhikov',
'upravlenie-analitiki/dutykotiki/vygruzki/summa-bonusov-po-nomeram-kart',
'upravlenie-analitiki/dutykotiki/vygruzki/temperatura-xranenija-tovarov-kategorii-alkogol',
'upravlenie-analitiki/dutykotiki/vygruzki/top-100-po-kolichestvu-onlajjn-zakazov.-tt-ne-v-zo',
'upravlenie-analitiki/dutykotiki/vygruzki/top-5-oxlazhdenki-vkusmil',
'upravlenie-analitiki/dutykotiki/vygruzki/top-50-detskix-tovarov',
'upravlenie-analitiki/dutykotiki/vygruzki/top-darkov-dlja-achivki-blagotvoritelnosti',
'upravlenie-analitiki/dutykotiki/vygruzki/top-pozicijj-v-prodazhax-k-ng-2023',
'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-po-vozrastam',
'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-s-maksimalnymi-vozvratami',
'upravlenie-analitiki/dutykotiki/vygruzki/top-tovarov-v-pervom-cheke-karty-klienta',
'upravlenie-analitiki/dutykotiki/vygruzki/top-tovary-55',
'upravlenie-analitiki/dutykotiki/vygruzki/top-tovary-geo',
'upravlenie-analitiki/dutykotiki/vygruzki/tt-kafe',
'upravlenie-analitiki/dutykotiki/vygruzki/tt-po-moskve-i-sankt-peterburgu',
'upravlenie-analitiki/dutykotiki/vygruzki/tt-s-dostavkojj',
'upravlenie-analitiki/dutykotiki/vygruzki/unikalnye-pokupateli-tovara-dobryjj-prjanik',
'upravlenie-analitiki/dutykotiki/vygruzki/unikalnyu-karty-po-tipu-zhaloby-33-4-s-razbivkojj',
'upravlenie-analitiki/dutykotiki/vygruzki/vajjtstory-gde-est-sborka-zakazov-jelektronnaja-po',
'upravlenie-analitiki/dutykotiki/vygruzki/ves-tovara-do-i-posle-sborki',
'upravlenie-analitiki/dutykotiki/vygruzki/vitrinyproekty-po-vsem-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/vozvraty-pokupki-i-obrashhenija-pokupatelejj-po-se',
'upravlenie-analitiki/dutykotiki/vygruzki/vozvraty-s-jekspress-dostavkojj-po-servisam',
'upravlenie-analitiki/dutykotiki/vygruzki/vtorsyre-zakaz-uslugi-s-dostavkojj',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-18-24-letnix',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-analitiki',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-auditorii',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-cen-za-period-v-dinamike',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-po-onlajjn-zakazam-v-adresax-s-jek',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-po-palletam',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dannyx-predoplaty',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-daty-rozhdenija-i-pola-klientov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dlja-sokrashhenija-zatrat-vremeni-linejjn',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-dolejj-kazhdogo-puti-v-korzine-pokupatelj',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-email-texnologov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-informacii-po-nalichiju-statusa-xrupkoe',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-jeko--i-nejeko--pokupatelejj',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kart-po-spisku-tt-kto-ne-zakazyval-dostav',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kolichestva-offlajjn-pokupatelejj-po-regi',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kolichestva-pokupatelejj-na-lp',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-kuratorov-kurerov-i-kassirov-komplektovsh',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomenklatury',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-i-kart-stavili-negativn',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-ottok',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-nomerov-telefonov-po-kartam',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-otzyvov-i-rejjtinga-sp-vvgo',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-po-programme-lojalnosti-novi',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-obrashhenijj-po-samovyvozu',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-planogramm-kafe',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-po-kartam-kategorija-non-fud',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-pokupatelejj-indilavki-za-2024-god',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-postavshhikov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-poterjannye-pokupateli-po-syru-rossijjsko',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-prinadlezhnosti-k-stmnestm-tovara-s-privj',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-raspredelenija-so-skladov-sumka-kofe-na-2',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-realizacijj-restorannyx-zakazov-jur.licam',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-skju-iz-1s-mjasnye-delikatesy.-kolbasy',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-srednjaja-vyruchka-i-srednee-kol-vo-cheko',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-strok-k-zakazam',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-telefonov-arendodatelejj-mikromarket-stat',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-texnologov-po-proizvoditeljupostavshhiku',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-top-50-tovarov-iz-top-20-kategorijj',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-top-tovarov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-tovarov-darkstorov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-tovarov-s-opredelennym-sostavom',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-v-formate-parket-chekov-novichkov',
'upravlenie-analitiki/dutykotiki/vygruzki/vygruzka-zakazov-slotovyx-jekspress-na-blizhajjshi',
'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-i-kolichestvo-v-kategorii-mjaso-i-zamoroz',
'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-i-kolichestvo-vegan-tovarov',
'upravlenie-analitiki/dutykotiki/vygruzki/vyruchka-po-dnjam-i-ostatki-po-chasam',
'upravlenie-analitiki/dutykotiki/vygruzki/xarakteristika-nomenklatury-i-id-xarakteristiki',
'upravlenie-analitiki/dutykotiki/vygruzki/zadacha-arbuznyx-kommentariev',
'upravlenie-analitiki/dutykotiki/vygruzki/zakaz-po-telefonu',
'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-dlja-nochlezhki',
'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-oformlennye-cherez-servis-samovyvoz-s-tovar',
'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-oformlennyjj-vne-grafika-raboty-servisa-dar',
'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-partnjorskojj-programmy-sovershjonnye-s-tov',
'upravlenie-analitiki/dutykotiki/vygruzki/zakazy-po-lsk-fjeshn-ooo',
'upravlenie-analitiki/dutykotiki/vygruzki/zapros-bonusnyx-kart-po-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/zapros-kolichestva-chekov-v-torgovyx-tochkax',
'upravlenie-analitiki/dutykotiki/vygruzki/zaprosy-mvd-i-drugix-gos.organov-po-pokupateljam',
'upravlenie-analitiki/dutykotiki/vygruzki/zhaloby-na-neprijatnyjj-zapax-v-tt',
'upravlenie-analitiki/dutykotiki/vygruzki/zheltyjj-cennik-po-partneru-jae',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/ab-testirovanie',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/bagi',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/blagotvoritelnost-vo-vkusvill',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/formulirovka-zaprosa-na-vygruzku',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/issledovanija',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/poleznye-sovety-dlja-power-bi',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/postanovka-zadachi-analitiku',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/prioritezacija-zadach',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/segmentacija-lifestyle',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/texbot',
'upravlenie-analitiki/dutykotiki/zanimatelno-ob-analitike/vremja-vypolnenija-zadach',
'upravlenie-analitiki/kak-podat-zajavku-analitikam/kak-ispolzovat-shablony-dlja-zadach',
'upravlenie-analitiki/logirovanie-mp/aa55c98a3fdf',
'upravlenie-analitiki/logirovanie-mp/biznes-trebovanija-k-sisteme-logirovanija',
'upravlenie-analitiki/logirovanie-mp/boli-logov',
'upravlenie-analitiki/logirovanie-mp/boli-svjazannye-s-logami',
'upravlenie-analitiki/logirovanie-mp/cartlogs',
'upravlenie-analitiki/logirovanie-mp/cartlogs/korzina',
'upravlenie-analitiki/logirovanie-mp/cartlogs/oplaty-5-4',
'upravlenie-analitiki/logirovanie-mp/celi-proekta-logirovanie-2.0',
'upravlenie-analitiki/logirovanie-mp/korzina',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/itogicustdev',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/metrics',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/opisanie-novogo-processa-legirovanija',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/scenarii-vzaimodejjstvija-mezhdu-uchastnikami-proc',
'upravlenie-analitiki/logirovanie-mp/proektlogirovaniya/zony-otvetstvennosti-i-kompetencii-uchastnikov-pro',
'upravlenie-analitiki/logirovanie-mp/razdel-1',
'upravlenie-analitiki/logirovanie-mp/razdel-1/istorija-versijj',
'upravlenie-analitiki/logirovanie-mp/semanticheskaja-model',
'upravlenie-analitiki/logirovanie-mp/vitriny-logov',
'upravlenie-analitiki/logirovanie-mp/vitriny-logov/primer-specificheskaja-vitrina-s-uzkospecializirov',
'upravlenie-analitiki/logirovanie-mp/vitriny-logov/razdel-2',
'upravlenie-analitiki/logirovanie-mp/vnedrenie-izmenenijj-v-tekushhijj-process-sozdanij',
'upravlenie-analitiki/logirovanie-mp/voprosy-po-chernovomu-arxitekturnomu-resheniju',
'upravlenie-analitiki/onboarding/dostupy',
'upravlenie-analitiki/onboarding/dostupy/6849a4eb2845',
'upravlenie-analitiki/onboarding/formy-jatrekera',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/1s',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/analiticheskie-instrumenty',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/full-power-bi-desktop',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instruction-ytracker',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instrukcija-po-nastrojjke-vs-code',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/instrukcija-po-podkljucheniju-clickhouse-v-dbeave',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/nastrojjka-gitlab',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/perenos-s-kajjtena-katja',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/podkljuchenie-cherez-jupyterhub',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/poisk-informacii-po-bd',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/pro-moduli-access',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/sozdanie-ssh-kljucha',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/sql-docs',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/ssrs-i-power-bi-otchety',
'upravlenie-analitiki/onboarding/instrukcii-po-rabote-s-instrumentami/superset',
'upravlenie-analitiki/onboarding/kak-my-vedjom-analiticheskie-zadachi',
'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki',
'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki/komanda-i-vzaimodejjstvie',
'upravlenie-analitiki/onboarding/manifest-produktovojj-analitiki/process-raboty-nad-zadachami',
'upravlenie-analitiki/onboarding/oformlenie-postojannogo-propuska-v-ofis-vv',
'upravlenie-analitiki/onboarding/opisanie-obshhego-analiticheskogo-shablona-opisani',
'upravlenie-analitiki/onboarding/stati-pro-dannye-vv',
'upravlenie-analitiki/onboarding/stati-pro-dannye-vv/produkty-lojalnosti-kratkijj-gajjd',
'upravlenie-analitiki/onboarding/statja-onbordinga-dlja-novichka',
'upravlenie-analitiki/onboarding/upravlenie-analitiki',
'upravlenie-analitiki/operacionnaja-analitika/analitika-b2b',
'upravlenie-analitiki/operacionnaja-analitika/analitika-darkkitchen',
'upravlenie-analitiki/operacionnaja-analitika/analitika-mikroservisa-roznicy',
'upravlenie-analitiki/operacionnaja-analitika/analitika-partnerov-apteki-restorany-cvety-etc',
'upravlenie-analitiki/operacionnaja-analitika/analitika-poslednejj-mili',
'upravlenie-analitiki/operacionnaja-analitika/analitika-sborki',
'upravlenie-analitiki/operacionnaja-analitika/raspredelenie-urz-ostatki',
'upravlenie-analitiki/operacionnaja-analitika/revju-2025',
'upravlenie-analitiki/platforma-po-av-testam/arxiv',
'upravlenie-analitiki/platforma-po-av-testam/draft-monitoring-sostojanija-platformy',
'upravlenie-analitiki/platforma-po-av-testam/oshibki',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/onbording',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/reglamenty-testovojj-dokumentacii',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/regress',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/restapi',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/test-plan-pokrytija-kriticheskogo-funkcionala-ab-t',
'upravlenie-analitiki/platforma-po-av-testam/testirovanie/web-funkcional---kalendar',
'upravlenie-analitiki/platforma-po-av-testam/validacija-ab-platformy',
'upravlenie-analitiki/processy-ua/emk-process/primer-zapolnennogo-shablona-dlja-revju-pa',
'upravlenie-analitiki/processy-ua/emk-process/process-revju-pa',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/0d4e3c3b36ec',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/klasterizacija-torgovyx-tochek.-podxod-razvitija',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/kojefficienty-sezonnosti-i-razvedyvatelnyjj-analiz',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/metriki-jeffektivnosti',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/okupaemost-tt',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/otcheta-dlja-podbora-pervichnojj-matricy-tovarov-d',
'upravlenie-analitiki/produktovaja-analitika-kpa/8a1c0312ec94/razlichija-v-sezonnosti-i-v-chekax-budnivyxodnye-i',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/43eb2cbbfe12',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/5-na-fr',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/8325b4e9d0f1',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/ac709a764736',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/dopniki-po-unikalnosti-i-jekskljuzivnosti',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/dorabotka-dashborda-peregovornaja-komanija',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/e58a3ba2ba0b',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/jeffektivnost-termochexlov-rc-dark',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/peredacha-skriptov-olja',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/rejjting-postavshhika',
'upravlenie-analitiki/produktovaja-analitika-kpa/e1cb991f8d0e/shablon-dlja-issledovanijj-ua',
'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa',
'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/chasto-ispolzuemye-tablicy-i-predstavlenija',
'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/metriki',
'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/metriki-gp',
'upravlenie-analitiki/produktovaja-analitika-kpa/metriki-i-skripty-kpa/poleznye-skripty',
'upravlenie-analitiki/produktovaja-analitika-kpa/raznye-ceny-i-cenoobrazovanie',
'upravlenie-analitiki/produktovaja-analitika-kpa/statistika-frov',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/0b2881f238dd',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/15fe987417a3',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/6fb599d9485e',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/d567d5309fec',
'upravlenie-analitiki/produktovaja-analitika-kpa/zadachi/vygruzki',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/chaevye',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/cheki',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/chestnyjj-znak',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/eshpay',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/kassy',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/krepkijj-alkogol',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/ks',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/lojalnost-i-onlajjn-kassa',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/oborudovanie',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/onlajjn-oplaty',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/oplata-na-meste',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/podarochnye-karty',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/pravila-postanovki-zadach-analitikam',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/privjazki-ka',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/spravochnik-platezhnyjj-put',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/spravochnik-platezhnyjj-put---logi-kurera',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/vozvraty',
'upravlenie-analitiki/produktovaja-analitika-kpo/analitika-platezhnogo-puti/vygruzki-dlja-issledovateljj',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/dq-project',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/issledovanie-otkuda-polzovateli-nachinajut-sobirat',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/istochniki-dlja-rascheta-metrik-otchetov-su-i-obes',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/opisanija-verxneurovnevyx-otchetov',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/pererabotka-verxneurovnevojj-otchetnosti',
'upravlenie-analitiki/produktovaja-analitika-kpo/general-team/plany-2024n1',
'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo',
'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo/plany-i-itogi',
'upravlenie-analitiki/produktovaja-analitika-kpo/lidery-pa-kpo/sink-liderov-pa-kpo',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/adzhendy-vstrech',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/novyjj-process-logirovanija',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/onbording',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/prioritizacija-zadach-po-rice',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/process-provedenija-av-testov',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/processy-pa-kpo',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/proekty',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/python-pakety',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/rabota-s-logami',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/reestr-dashbordov',
'upravlenie-analitiki/produktovaja-analitika-kpo/obshhaja-dokumentacija/reestr-ispolzuemyx-na-238kx-tablic-vitrin-vjux',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/ab-testy',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/dashbordy',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-avtorizacii-i-profilja/issledovanija',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/komandaccg',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/plany-i-celi',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/processy-korzina',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/produktovaja-analitika-geoservisov',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-ccg/produktovaja-analitika-korziny',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/jekran-podderzhki',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/katalog',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/neaktualnoe',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/obshhee-kataloga',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/produktovaja-analitika-kartochki-tovara',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-kataloga/rejjtingi-i-otzyvy',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/dokumentirovanie-zadach',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/kommunikacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/lojalnost',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/plany-2024',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/proekty',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/shablony',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/spisok-del-serezhi',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-lojalnosti/vitriny-danny',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp/magaziny',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-offlajjnery-v-mp/processy',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/chaevye',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/onlajjn-oplaty',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/oplaty-na-meste',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-platezhnyx-putejj/vozvraty',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/ab-poisk',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/dokumentacija-biblioteki-dlja-ab-testov',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/kak-rabotaet-poisk',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/logi-poiska-xyunja',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/metriki-poisk',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/onbording-v-komandu-poiska',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/produkty-komandy-analitiki-poiska',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/proekt-novaja-konkurencija',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/researches-poisk',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/search-dashes',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-poiska/vitriny-dannyx-poisk',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/8ff2daf94a28',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/ab-rekomendacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/algoritmicheskie-podborki',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/dashbordy-rekomendacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/issledovanija-rekomendacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/logi-rekomendacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/metriki-rekomendacii',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/onbording-v-komandu-analitiki-rekomendacijj',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/problemy',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/rekomendacii-v-mp',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/tovary-v-rekomendacijax',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-rekomendacijj/vygruzki',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/ab-tests',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/appsflyer',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/bjeklog-gipotez',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/dokumentacija-sajjta',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/jandeks.metrika',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta-slova',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/logirovanie-sajjta-slova-f6de9f',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/metriki-sajjta',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/otchety-komandy-sajjta',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/test-vkusvill-plan',
'upravlenie-analitiki/produktovaja-analitika-kpo/produktovaja-analitika-sajjta/zadachi',
'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo',
'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/domen-driven-design',
'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/logirovanie',
'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/novaja-konkurencija-metriki',
'upravlenie-analitiki/produktovaja-analitika-kpo/proekty-pa-kpo/shablon',
'upravlenie-analitiki/produktovaja-analitika-kpo/verxneurovnevaja-analitika',
'upravlenie-analitiki/produktovaja-analitika-kpo/verxneurovnevaja-analitika/zadachi',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/4689cbbc6dc5',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/dokumentacija-po-zadacham',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/globalnye-issledovanija',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/komanda',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/mexaniki-lojalnosti',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/obshhaja-informacija',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/otchety',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/segmentacii',
'upravlenie-analitiki/produktovaja-analitika-lojalnosti/slovar-metrik',
'upravlenie-analitiki/put-novichka/kak-podat-zajavku-na-dostupy',
'upravlenie-analitiki/put-novichka/perevod-sotrudnika',
'upravlenie-analitiki/put-novichka/pomoshh-v-provedenii-onboarding',
'upravlenie-analitiki/put-novichka/vzaimodejjstvie-upravlenija-analitiki-ua-i-upravle',
'upravlenie-analitiki/research-analytics/analiz-jeffektivnosti-otveta-na-otzyv',
'upravlenie-analitiki/research-analytics/assistent-antifrod-dose-na-klienta',
'upravlenie-analitiki/research-analytics/avtootvety-na-gorjachejj-linii',
'upravlenie-analitiki/research-analytics/b2b-containers',
'upravlenie-analitiki/research-analytics/busting-tovarov-sgorjacha',
'upravlenie-analitiki/research-analytics/favorite-in-tap-bar',
'upravlenie-analitiki/research-analytics/finansovaja-model-fudomatov-obedy',
'upravlenie-analitiki/research-analytics/gen-ai',
'upravlenie-analitiki/research-analytics/gen-ai/copilot-vygruzki',
'upravlenie-analitiki/research-analytics/gen-ai/genai-bt',
'upravlenie-analitiki/research-analytics/gen-ai/rag',
'upravlenie-analitiki/research-analytics/modelirovanie-blokirovki-postavshhikov',
'upravlenie-analitiki/research-analytics/nps',
'upravlenie-analitiki/research-analytics/ocenka-jeffektivnosti-soobshhenija-ob-izmenenijax-',
'upravlenie-analitiki/research-analytics/oec-metrika',
'upravlenie-analitiki/research-analytics/poleznye-bazovye-materialy',
'upravlenie-analitiki/research-analytics/prognozirovanie-pl-obedy',
'upravlenie-analitiki/research-analytics/snjatie-tovara-s-prodazhi',
'upravlenie-analitiki/research-analytics/zajavka-na-otrabotku-kachestva',
]
MAX_DEPTH = 4 # Максимальная глубина обхода ссылок
DELAY = 0.3 # Пауза между запросами (сек), чтобы не перегружать API
# ─────────────────────────────────────────────────────────────────────────────
def main():
started_at = pendulum.now(tz='Europe/Moscow')
print(f'[{started_at.to_datetime_string()}] Запуск wiki_sync')
if not API_KEY or not ORG_ID:
print('Не заданы API_KEY / ORG_ID в .env')
sys.exit(1)
# 1. Обход Wiki
pages = crawl(
root_slugs=ROOT_SLUGS,
api_token=API_KEY,
org_id=ORG_ID,
max_depth=MAX_DEPTH,
delay=DELAY,
)
if not pages:
print('Страниц не найдено. Завершение.')
return
# 2. Сохранение в Supabase
db = SupabaseManager()
if not db.connect():
sys.exit(1)
try:
result = db.upsert_wiki_pages(pages)
print(
f'\n✓ Страницы сохранены:\n'
f' Новых / изменённых: {result["inserted"]}\n'
f' Без изменений: {result["unchanged"]}\n'
f' Всего страниц: {len(pages)}'
)
# 3. Генерация embeddings для новых/изменённых страниц
print('\nГенерируем embeddings...')
emb_result = upsert_embeddings(db, pages)
print(
f'✓ Embeddings:\n'
f' Сгенерировано: {emb_result["embedded"]}\n'
f' Без изменений: {emb_result["skipped"]}'
)
finally:
db.close()
finished_at = pendulum.now(tz='Europe/Moscow')
duration = (finished_at - started_at).in_seconds()
print(f'[{finished_at.to_datetime_string()}] Готово за {duration}с')
if __name__ == '__main__':
main()

146
wiki_tree_crawler.py Normal file
View File

@@ -0,0 +1,146 @@
"""
Обход {% tree %} страниц Яндекс Вики через Playwright.
Находит дочерние страницы, которые не видны через API (рендерятся динамически).
Возвращает список slug-ов которых нет в wiki_pages — их нужно добавить в ROOT_SLUGS.
Запуск:
python wiki_tree_crawler.py
"""
import re
import time
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent / '.env')
from playwright.sync_api import sync_playwright
AUTH_FILE = Path(__file__).parent / 'wiki_auth.json'
WIKI_BASE = 'https://wiki.yandex.ru'
DELAY = 0.5 # пауза между запросами
def slug_from_url(url: str) -> str | None:
"""Извлечь slug из URL wiki.yandex.ru/slug/..."""
match = re.match(r'https?://wiki\.yandex\.ru/([^?#]+?)/?$', url)
if not match:
return None
slug = match.group(1).strip('/')
# Отфильтровать служебные страницы
if any(slug.startswith(p) for p in ['.files', 'homepage/', 'cdp-']):
return None
return slug
def get_child_slugs(page, parent_slug: str) -> list[str]:
"""Открыть страницу и собрать все ссылки на дочерние страницы."""
url = f'{WIKI_BASE}/{parent_slug}'
page.goto(url, wait_until='networkidle', timeout=30000)
time.sleep(DELAY)
# Ищем ссылки в левом навигационном дереве и в содержимом страницы
links = page.eval_on_selector_all(
'a[href]',
'els => els.map(el => el.href)'
)
child_slugs = []
for link in links:
slug = slug_from_url(link)
if slug and slug.startswith(parent_slug + '/') and slug != parent_slug:
child_slugs.append(slug)
return list(set(child_slugs))
def find_tree_pages_in_db() -> list[tuple[str, str]]:
"""Получить из Supabase все страницы с {% tree %} в контенте."""
import sys
sys.path.insert(0, str(Path(__file__).parent))
from supabase import SupabaseManager
db = SupabaseManager()
db.connect()
db.cursor.execute("""
SELECT slug, value->>'title' AS title
FROM wiki_pages
WHERE value->>'content' LIKE '%{%% tree%%}%'
ORDER BY slug
""")
rows = db.cursor.fetchall()
db.close()
return rows
def get_all_known_slugs() -> set[str]:
"""Все slug-и которые уже есть в wiki_pages."""
import sys
sys.path.insert(0, str(Path(__file__).parent))
from supabase import SupabaseManager
db = SupabaseManager()
db.connect()
db.cursor.execute("SELECT slug FROM wiki_pages")
slugs = {row[0] for row in db.cursor.fetchall()}
db.close()
return slugs
def main():
if not AUTH_FILE.exists():
print(f'✗ Файл авторизации не найден: {AUTH_FILE}')
print(' Сначала запусти: python wiki_auth.py')
return
ROOT_PREFIX = 'upravlenie-analitiki'
print('Загружаем список страниц с {% tree %} из Supabase...')
all_tree_pages = find_tree_pages_in_db()
tree_pages = [
(s, t) for s, t in all_tree_pages
if s == ROOT_PREFIX or s.startswith(ROOT_PREFIX + '/')
]
known_slugs = get_all_known_slugs()
print(f'Страниц с {{% tree %}} (всего): {len(all_tree_pages)}, в разделе УА: {len(tree_pages)}')
print()
new_slugs = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context(storage_state=str(AUTH_FILE))
page = context.new_page()
for slug, title in tree_pages:
print(f'{slug} «{title}»')
try:
children = get_child_slugs(page, slug)
new = [s for s in children if s not in known_slugs]
print(f' Найдено дочерних: {len(children)}, новых: {len(new)}')
for s in sorted(children):
marker = ' ← NEW' if s in new else ''
print(f' {s}{marker}')
new_slugs.extend(new)
except Exception as e:
print(f' ✗ Ошибка: {e}')
print()
browser.close()
new_slugs = list(set(new_slugs))
if new_slugs:
print('=' * 60)
print(f'Найдено {len(new_slugs)} новых slug-ов для ROOT_SLUGS:')
print()
for s in sorted(new_slugs):
print(f" '{s}',")
print()
print('Добавь их в ROOT_SLUGS в wiki_sync.py и запусти wiki_sync.py')
else:
print('✓ Новых страниц не найдено')
if __name__ == '__main__':
main()

125
yandex_wiki.py Normal file
View File

@@ -0,0 +1,125 @@
"""
Модуль для обхода страниц Yandex Wiki API
"""
import re
import time
import hashlib
import requests
import pendulum
WIKI_API = 'https://api.wiki.yandex.net/v1'
def _make_headers(api_token: str, org_id: str) -> dict:
return {
'Authorization': f'OAuth {api_token}',
'X-Org-Id': org_id,
}
def get_page(slug: str, api_token: str, org_id: str) -> dict | None:
"""Получить страницу по slug. Возвращает None если не найдена."""
try:
resp = requests.get(
f'{WIKI_API}/pages',
headers=_make_headers(api_token, org_id),
params={'slug': slug, 'fields': 'content,attributes'},
timeout=15,
)
if resp.status_code == 200:
return resp.json()
return None
except requests.RequestException as e:
print(f' ✗ Ошибка при запросе slug={slug}: {e}')
return None
def extract_slugs_from_content(content: str) -> list[str]:
"""Извлечь slug-ссылки из wiki-разметки страницы."""
slugs = []
# [[slug]] и [[slug|текст]]
slugs += re.findall(r'\[\[([^\]|#]+)', content)
# ((https://wiki.yandex.ru/slug текст)) или ((https://wiki.yandex.ru/slug))
url_matches = re.findall(r'https://wiki\.yandex\.ru/([^\s)#]+)', content)
slugs += url_matches
# Нормализуем: убираем пробелы и слэши по краям
result = []
for s in slugs:
s = s.strip().strip('/')
if s and not s.startswith('http') and len(s) < 200:
result.append(s)
return result
def content_hash(content: str) -> str:
"""MD5-хеш содержимого страницы для определения изменений."""
return hashlib.md5(content.encode('utf-8')).hexdigest()
def crawl(
root_slugs: list[str],
api_token: str,
org_id: str,
max_depth: int = 4,
delay: float = 0.3,
) -> list[dict]:
"""
Обойти дерево страниц начиная с root_slugs.
Args:
root_slugs: Список стартовых slug-ов (например ['upravlenie-analitiki'])
api_token: OAuth-токен
org_id: ID организации
max_depth: Максимальная глубина обхода
delay: Пауза между запросами в секундах
Returns:
Список словарей с данными страниц
"""
dt_load = pendulum.now(tz='Europe/Moscow')
visited: set[str] = set()
pages: list[dict] = []
def _crawl(slug: str, depth: int):
if depth > max_depth or slug in visited:
return
visited.add(slug)
print(f' {" " * depth}{slug}')
page = get_page(slug, api_token, org_id)
time.sleep(delay)
if not page:
return
raw_content = page.get('content', '')
attrs = page.get('attributes', {})
modified_at = attrs.get('modified_at') or attrs.get('updated_at')
pages.append({
'pg_load_dttm': dt_load,
'slug': page.get('slug', slug),
'wiki_page_id': page.get('id'),
'title': page.get('title', ''),
'page_type': page.get('page_type', ''),
'modified_at': modified_at,
'content': raw_content,
'content_hash': content_hash(raw_content),
})
# Рекурсивно обходим ссылки
for child_slug in extract_slugs_from_content(raw_content):
_crawl(child_slug, depth + 1)
print(f'Начинаем обход wiki, корневые разделы: {root_slugs}')
for root in root_slugs:
_crawl(root, 0)
print(f'✓ Обход завершён. Найдено страниц: {len(pages)}')
return pages