fix(parsers): починить CyberLeninka + подготовить русскую заливку корпуса

Корпус на 99.4% английский, 0 русских источников — при том что сервис для русских
студентов. Корень: парсер CyberLeninka был сломан (слал GET на /api/search → HTTP
405) и ни разу не наполнял базу.

- cyberleninka.py: GET→POST с JSON-телом (mode=articles); authors теперь из списка
  (API отдаёт список, не строку); чистка <b>-подсветки и HTML-сущностей (&quot;).
  Проверено вживую: 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:
jze9
2026-08-12 21:02:16 +05:00
parent e6c44f30dd
commit 9d005486df
8 changed files with 292 additions and 21 deletions

View File

@@ -0,0 +1,53 @@
"""Юнит-тесты парсера CyberLeninka — чистая логика transform (без сети).
Стерегут исправления реальных багов: JSON-API отдаёт authors списком, а name/
annotation — с HTML-подсветкой (<b>) и сущностями (&quot;).
"""
from cyberleninka import CyberLeninkaParser, _authors_from_list, _clean
# Форма ответа POST /api/search КиберЛенинки
SAMPLE = {
"name": "СОДЕРЖАНИЕ <b>ЭКОНОМИЧЕСКОГО</b> <b>АНАЛИЗА</b> &quot;X&quot;",
"annotation": "В статье <b>рассматривается</b> вопрос",
"authors": ["Вишникина А. Д.", "Клопова А. А."],
"journal": "Экономика и социум",
"year": 2024,
"link": "/article/n/soderzhanie",
}
def test_clean_strips_tags_and_entities():
assert _clean('<b>Тест</b> &quot;X&quot;') == 'Тест "X"'
assert _clean(None) == ""
assert _clean(" чисто ") == "чисто"
def test_authors_from_list():
a = _authors_from_list(["Вишникина А. Д.", "Иванов И."])
assert a[0] == {"last_name": "Вишникина", "initials": "А. Д."}
assert a[1] == {"last_name": "Иванов", "initials": "И."}
assert _authors_from_list([]) == []
def test_transform_cleans_and_maps_fields():
t = CyberLeninkaParser().transform(SAMPLE)
assert "<b>" not in t["title"] and "&quot;" not in t["title"]
assert t["title"].startswith("СОДЕРЖАНИЕ ЭКОНОМИЧЕСКОГО АНАЛИЗА")
assert t["lang"] == "ru"
assert t["year"] == 2024
assert t["journal"] == "Экономика и социум"
assert t["url"] == "https://cyberleninka.ru/article/n/soderzhanie"
assert t["source"] == "cyberleninka"
assert t["authors"][0]["last_name"] == "Вишникина"
assert "<b>" not in t["abstract"]
def test_transform_handles_string_authors():
raw = dict(SAMPLE, authors="Иванов И.И., Петров П.П.")
t = CyberLeninkaParser().transform(raw)
assert len(t["authors"]) == 2
def test_transform_empty_without_id_or_link():
assert CyberLeninkaParser().transform({"name": "x"}) == {}