Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
8.6 KiB
Python
268 lines
8.6 KiB
Python
"""ГОСТ 7.1-2003 — Библиографическая запись. Библиографическое описание.
|
||
|
||
Полная запись для списка литературы.
|
||
|
||
Формат для статьи в журнале:
|
||
Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
|
||
|
||
Формат для книги:
|
||
Автор(ы). Название. — Город : Издательство, Год. — X с.
|
||
|
||
Правила по ГОСТ 7.1-2003:
|
||
- До 3 авторов: перечислить всех
|
||
- 4+ авторов: первые 3 + "и др." (рус.) / "et al." (англ.)
|
||
- Разделитель смысловых частей: " — "
|
||
- Разделитель авторов: ", "
|
||
- Место публикации через "/"
|
||
"""
|
||
|
||
import re
|
||
|
||
|
||
def _format_author(author: dict) -> str:
|
||
"""
|
||
Форматировать одного автора.
|
||
|
||
Args:
|
||
author: dict с полями last_name, first_name, initials (опционально)
|
||
|
||
Returns:
|
||
Строка вида "Иванов И. И." или "Иванов И."
|
||
"""
|
||
last_name = author.get("last_name", "").strip()
|
||
initials = author.get("initials", "").strip()
|
||
first_name = author.get("first_name", "").strip()
|
||
|
||
if not last_name:
|
||
return ""
|
||
|
||
if initials:
|
||
# Нормализовать инициалы: обеспечить точки
|
||
if not initials.endswith("."):
|
||
initials += "."
|
||
return f"{last_name} {initials}"
|
||
elif first_name:
|
||
# Извлечь инициалы из полного имени
|
||
parts = first_name.split()
|
||
inits = "".join(p[0].upper() + "." for p in parts if p)
|
||
return f"{last_name} {inits}"
|
||
else:
|
||
return last_name
|
||
|
||
|
||
def _format_authors(authors: list[dict], lang: str = "ru") -> str:
|
||
"""
|
||
Форматировать список авторов по правилам ГОСТ 7.1-2003.
|
||
|
||
Args:
|
||
authors: Список словарей с авторами
|
||
lang: Язык для "и др." / "et al."
|
||
|
||
Returns:
|
||
Строка с отформатированными авторами
|
||
"""
|
||
if not authors:
|
||
return ""
|
||
|
||
et_al = "и др." if lang == "ru" else "et al."
|
||
formatted = [_format_author(a) for a in authors if a.get("last_name")]
|
||
formatted = [f for f in formatted if f]
|
||
|
||
if not formatted:
|
||
return ""
|
||
|
||
if len(formatted) <= 3:
|
||
return ", ".join(formatted)
|
||
else:
|
||
return ", ".join(formatted[:3]) + f", {et_al}"
|
||
|
||
|
||
class GOST71Formatter:
|
||
"""Форматтер полных библиографических записей по ГОСТ 7.1-2003."""
|
||
|
||
def format_full(self, doc: dict) -> str:
|
||
"""
|
||
Форматировать полную библиографическую запись.
|
||
|
||
Args:
|
||
doc: Словарь с полями документа
|
||
|
||
Returns:
|
||
Отформатированная строка библиографической записи
|
||
"""
|
||
doc_type = self._detect_type(doc)
|
||
|
||
if doc_type == "article":
|
||
return self._format_article(doc)
|
||
elif doc_type == "book":
|
||
return self._format_book(doc)
|
||
elif doc_type == "web":
|
||
return self._format_web(doc)
|
||
else:
|
||
return self._format_generic(doc)
|
||
|
||
def _detect_type(self, doc: dict) -> str:
|
||
"""Определить тип документа."""
|
||
if doc.get("journal"):
|
||
return "article"
|
||
if doc.get("url") and not doc.get("journal"):
|
||
return "web"
|
||
return "generic"
|
||
|
||
def _format_article(self, doc: dict) -> str:
|
||
"""
|
||
Форматировать статью в журнале.
|
||
|
||
Формат: Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
|
||
"""
|
||
lang = doc.get("lang", "ru") or "ru"
|
||
authors = doc.get("authors", [])
|
||
author_str = _format_authors(authors, lang=lang)
|
||
|
||
title = (doc.get("title") or "").strip()
|
||
journal = (doc.get("journal") or "").strip()
|
||
year = doc.get("year", "")
|
||
volume = doc.get("volume", "")
|
||
issue = doc.get("issue", "")
|
||
pages = doc.get("pages", "")
|
||
doi = doc.get("doi", "")
|
||
|
||
parts = []
|
||
|
||
# Авторы и название
|
||
if author_str:
|
||
parts.append(f"{author_str}. {title}")
|
||
else:
|
||
parts.append(title)
|
||
|
||
# Место публикации
|
||
location = f"// {journal}"
|
||
if year:
|
||
location += f". — {year}"
|
||
if volume:
|
||
location += f". — Т. {volume}"
|
||
if issue:
|
||
location += f", № {issue}"
|
||
if pages:
|
||
location += f". — С. {pages}"
|
||
|
||
parts.append(location)
|
||
|
||
result = " ".join(parts)
|
||
|
||
# DOI
|
||
if doi:
|
||
result += f". — DOI: {doi}"
|
||
|
||
return result.strip()
|
||
|
||
def _format_book(self, doc: dict) -> str:
|
||
"""
|
||
Форматировать книгу / монографию.
|
||
|
||
Формат: Автор(ы). Название. — Город : Издательство, Год. — X с.
|
||
"""
|
||
lang = doc.get("lang", "ru") or "ru"
|
||
authors = doc.get("authors", [])
|
||
author_str = _format_authors(authors, lang=lang)
|
||
|
||
title = (doc.get("title") or "").strip()
|
||
year = doc.get("year", "б. г.")
|
||
pages = doc.get("pages", "")
|
||
|
||
parts = []
|
||
|
||
if author_str:
|
||
parts.append(f"{author_str}.")
|
||
|
||
parts.append(f"{title}.")
|
||
parts.append(f"— {year}.")
|
||
|
||
if pages:
|
||
# Предполагаем, что pages содержит количество страниц
|
||
parts.append(f"— {pages} с.")
|
||
|
||
return " ".join(parts).strip()
|
||
|
||
def _format_web(self, doc: dict) -> str:
|
||
"""
|
||
Форматировать электронный ресурс.
|
||
|
||
Формат: Автор(ы). Название [Электронный ресурс]. — URL: ... (дата обращения: ...)
|
||
"""
|
||
lang = doc.get("lang", "ru") or "ru"
|
||
authors = doc.get("authors", [])
|
||
author_str = _format_authors(authors, lang=lang)
|
||
|
||
title = (doc.get("title") or "").strip()
|
||
url = (doc.get("url") or "").strip()
|
||
year = doc.get("year", "")
|
||
|
||
parts = []
|
||
|
||
if author_str:
|
||
parts.append(f"{author_str}.")
|
||
|
||
# Для ГОСТ — обозначение электронного ресурса
|
||
parts.append(f"{title} [Электронный ресурс].")
|
||
|
||
if year:
|
||
parts.append(f"— {year}.")
|
||
|
||
if url:
|
||
parts.append(f"— URL: {url}")
|
||
|
||
return " ".join(parts).strip()
|
||
|
||
def _format_generic(self, doc: dict) -> str:
|
||
"""Базовый форматтер для неопределённых типов."""
|
||
lang = doc.get("lang", "ru") or "ru"
|
||
author_str = _format_authors(doc.get("authors", []), lang=lang)
|
||
title = (doc.get("title") or "").strip()
|
||
year = doc.get("year", "")
|
||
|
||
result = ""
|
||
if author_str:
|
||
result += f"{author_str}. "
|
||
result += title
|
||
if year:
|
||
result += f". — {year}"
|
||
|
||
return result.strip()
|
||
|
||
|
||
def _get_sort_key(doc: dict) -> str:
|
||
"""
|
||
Получить ключ сортировки для документа.
|
||
|
||
Кириллица идёт перед латиницей (для русских традиций):
|
||
сначала русскоязычные источники, затем иностранные.
|
||
|
||
Args:
|
||
doc: Словарь с метаданными
|
||
|
||
Returns:
|
||
Строка для сортировки
|
||
"""
|
||
authors = doc.get("authors", [])
|
||
if authors:
|
||
last_name = authors[0].get("last_name", "")
|
||
else:
|
||
last_name = doc.get("title", "")
|
||
|
||
if not last_name:
|
||
return "яяя" # В конец
|
||
|
||
# Кириллица < латиница (традиция: русские источники первыми)
|
||
first_char = last_name[0].lower()
|
||
is_latin = ord(first_char) < 256 and first_char.isalpha()
|
||
|
||
# Префикс для сортировки: 0 для кириллицы, 1 для латиницы
|
||
prefix = "1" if is_latin else "0"
|
||
return f"{prefix}{last_name.lower()}"
|
||
|
||
|
||
def format_full(doc: dict) -> str:
|
||
"""Удобная функция для быстрого форматирования."""
|
||
return GOST71Formatter().format_full(doc)
|