fix(parsers): починить CyberLeninka + подготовить русскую заливку корпуса
Корпус на 99.4% английский, 0 русских источников — при том что сервис для русских студентов. Корень: парсер CyberLeninka был сломан (слал GET на /api/search → HTTP 405) и ни разу не наполнял базу. - cyberleninka.py: GET→POST с JSON-телом (mode=articles); authors теперь из списка (API отдаёт список, не строку); чистка <b>-подсветки и HTML-сущностей ("). Проверено вживую: 5/5 студенческих тем возвращают реальные русские статьи. - Юнит-тесты парсера (scripts/parsers/tests/, 5 шт.) + обвязка; run_tests.sh обобщён на пути → парсеры теперь в тест-гейте CI. Всего тестов: 87. - scripts/seed_ru_sources.py: сидер parse_sources по 30 студенческим дисциплинам (dry-run по умолчанию, --apply для записи). НЕ запускает заливку — готовит задания. - docs/INGESTION.md: runbook (текущее состояние, шаги запуска, проверка, масштаб). Прод-путь index.run_parser уже поддерживает cyberleninka и openalex(lang=ru). Заливку не запускал — это отдельный go (ресурсоёмко: GPU-эмбеддинги, рост БД). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
121
scripts/seed_ru_sources.py
Normal file
121
scripts/seed_ru_sources.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Подготовка русскоязычной заливки корпуса — сидинг parse_sources (CyberLeninka).
|
||||
|
||||
Зачем: корпус на ~99% английский (0 русских источников), хотя сервис — для русских
|
||||
студентов. Этот скрипт создаёт в таблице `parse_sources` набор заданий CyberLeninka
|
||||
по основным студенческим дисциплинам. Он НЕ запускает заливку — только готовит
|
||||
задания; их затем триггерят (кнопка «Запустить» в админке или Celery-задача
|
||||
`index.run_parser(source_id)`).
|
||||
|
||||
Идемпотентно: источник с таким `name` повторно не создаётся.
|
||||
|
||||
Запуск:
|
||||
# dry-run — показать план, ничего не писать:
|
||||
python scripts/seed_ru_sources.py
|
||||
# создать источники в БД (лимит на каждую дисциплину):
|
||||
python scripts/seed_ru_sources.py --apply --limit 500
|
||||
|
||||
Креды PG берутся из .env (POSTGRES_HOST/PORT/DB/USER/PASSWORD) или окружения.
|
||||
ВНИМАНИЕ: локальный .env может указывать на старый PG — для прода используйте
|
||||
POSTGRES_HOST=192.168.1.38 (см. вывод по наполненности базы).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Студенческие дисциплины → поисковый запрос в CyberLeninka (русскоязычный).
|
||||
DISCIPLINES = [
|
||||
"экономический анализ", "менеджмент организации", "бухгалтерский учёт",
|
||||
"финансы и кредит", "маркетинг", "гражданское право", "уголовное право",
|
||||
"конституционное право", "трудовое право", "педагогика", "психология личности",
|
||||
"социология", "философия науки", "история России", "политология",
|
||||
"информационные технологии", "программирование", "базы данных",
|
||||
"математический анализ", "физика", "химия", "биология", "медицина",
|
||||
"экология", "лингвистика", "литературоведение", "государственное управление",
|
||||
"международные отношения", "журналистика", "культурология",
|
||||
]
|
||||
|
||||
|
||||
def _load_env() -> None:
|
||||
"""Подтянуть переменные из .env репозитория, если файл есть."""
|
||||
env = Path(__file__).resolve().parent.parent / ".env"
|
||||
if not env.exists():
|
||||
return
|
||||
for line in env.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, _, val = line.partition("=")
|
||||
os.environ.setdefault(key.strip(), val.strip())
|
||||
|
||||
|
||||
def _pg_dsn() -> dict:
|
||||
return {
|
||||
"host": os.environ.get("POSTGRES_HOST", "localhost"),
|
||||
"port": int(os.environ.get("POSTGRES_PORT", "5432")),
|
||||
"dbname": os.environ.get("POSTGRES_DB", "antiplagiator"),
|
||||
"user": os.environ.get("POSTGRES_USER", "antiplagiator"),
|
||||
"password": os.environ.get("POSTGRES_PASSWORD", ""),
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--apply", action="store_true", help="реально записать в БД (иначе dry-run)")
|
||||
ap.add_argument("--limit", type=int, default=500, help="лимит документов на дисциплину")
|
||||
ap.add_argument("--source-type", default="cyberleninka", help="тип парсера (по умолчанию cyberleninka)")
|
||||
args = ap.parse_args()
|
||||
|
||||
_load_env()
|
||||
rows = [(f"ru:{d}", args.source_type, d) for d in DISCIPLINES]
|
||||
|
||||
print(f"Дисциплин: {len(rows)} | тип: {args.source_type} | лимит/шт: {args.limit}")
|
||||
print(f"Потенциальный объём: ~{len(rows) * args.limit} документов")
|
||||
if not args.apply:
|
||||
print("\n[dry-run] будут созданы источники (name → query):")
|
||||
for name, _, q in rows:
|
||||
print(f" {name:38} → {q!r}")
|
||||
print("\nЗапустите с --apply, чтобы записать в parse_sources.")
|
||||
return
|
||||
|
||||
try:
|
||||
import psycopg2
|
||||
except ImportError:
|
||||
sys.exit("Нужен psycopg2 (есть в образах воркеров). Запускайте на app-хосте/в контейнере.")
|
||||
|
||||
dsn = _pg_dsn()
|
||||
print(f"\nПодключение к PG {dsn['host']}:{dsn['port']}/{dsn['dbname']} ...")
|
||||
conn = psycopg2.connect(**dsn)
|
||||
conn.autocommit = True
|
||||
created, skipped, ids = 0, 0, []
|
||||
with conn.cursor() as cur:
|
||||
for name, stype, query in rows:
|
||||
cur.execute("SELECT id FROM parse_sources WHERE name = %s", (name,))
|
||||
existing = cur.fetchone()
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
cur.execute(
|
||||
'INSERT INTO parse_sources '
|
||||
'(source_type, name, query, lang, "limit", enabled, last_status, docs_added) '
|
||||
"VALUES (%s, %s, %s, 'ru', %s, TRUE, 'idle', 0) RETURNING id",
|
||||
(stype, name, query, args.limit),
|
||||
)
|
||||
ids.append(cur.fetchone()[0])
|
||||
created += 1
|
||||
conn.close()
|
||||
|
||||
print(f"Создано: {created}, пропущено (уже были): {skipped}")
|
||||
if ids:
|
||||
print(f"ID новых источников: {ids}")
|
||||
print("\nЗапуск заливки (после проверки источников):")
|
||||
print(" • Админка → раздел «Источники» → «Запустить» у каждого, ЛИБО")
|
||||
print(" • Celery: for i in ids: index.run_parser.delay(i)")
|
||||
print("Рекомендация: сначала запустить 1-2 для проверки темпа/качества, потом остальные.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user