Files
anti-plagiarism/scripts/parsers/tests/test_pmc.py
jze9 2aac40ed3e feat(parsers): новый источник PubMed Central (PMC) — реальный полный текст
Отвечает на "неужели больше нет библиотек" — добавлен четвёртый источник.
PMC Open Access Subset (NCBI E-utilities) — крупнейший биомедицинский открытый
архив, бесплатный API без обязательного ключа (NCBI_API_KEY — опционально,
поднимает лимит 3→10 запросов/сек). В отличие от остальных источников отдаёт
РЕАЛЬНЫЙ полный текст статьи (bodyJATS XML) прямо в ответе efetch — 17-53К
символов на статью в проверке вживую, не аннотацию и не 700-символьный OCR-
фрагмент. Никаких новых зависимостей — xml.etree.ElementTree (stdlib) + httpx,
по образцу arxiv.py.

- scripts/parsers/pmc.py: esearch (open access[filter]) → efetch (батчи по 20,
  JATS XML) → плоские словари → unified schema. Год-фильтр, свой User-Agent.
- Зарегистрирован в index.run_parser (services/worker-indexer/app/tasks/index.py)
  и в CLI run_parser.py — доступен как source_type="pmc" наравне с остальными.
- 7 юнит-тестов на реальных JATS XML-фрагментах (без сети): извлечение полей,
  фильтр contrib-type=author (не editor), пустой/битый XML, батч из 2 статей.

Рассмотрены и НЕ добавлены: Semantic Scholar (общий rate-limit исчерпан без
API-ключа, ключ — самостоятельная регистрация юзера) и CORE.ac.uk (обязателен
ключ). Google Scholar/ResearchGate — намеренно не трогаем, скрейпинг нарушает ToS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 14:47:15 +05:00

89 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Юнит-тесты парсера PubMed Central (PMC) — чистая логика (без сети).
_parse_article работает с реальными xml.etree.ElementTree узлами (JATS XML из
efetch), поэтому тесты строят минимальные JATS-фрагменты, а не мокают HTTP.
"""
import xml.etree.ElementTree as ET
from pmc import PMCParser
ARTICLE_XML = """
<article>
<front>
<journal-meta>
<journal-title-group><journal-title>Journal of Testing</journal-title></journal-title-group>
</journal-meta>
<article-meta>
<article-id pub-id-type="pmcaid">1234567</article-id>
<article-id pub-id-type="doi">10.1000/test.123</article-id>
<title-group><article-title>A <italic>Study</italic> of Testing</article-title></title-group>
<contrib-group>
<contrib contrib-type="author">
<name><surname>Ivanov</surname><given-names>Ivan</given-names></name>
</contrib>
<contrib contrib-type="editor">
<name><surname>NotAnAuthor</surname><given-names>X</given-names></name>
</contrib>
</contrib-group>
<pub-date pub-type="epub"><year>2024</year></pub-date>
<abstract><p>This is the <bold>abstract</bold> text.</p></abstract>
</article-meta>
</front>
<body>
<p>First paragraph of the body.</p>
<p>Second paragraph with <xref>a ref</xref> inline.</p>
</body>
</article>
"""
def _article() -> ET.Element:
return ET.fromstring(ARTICLE_XML)
def test_parse_article_extracts_all_fields():
raw = PMCParser()._parse_article(_article())
assert raw["id"] == "1234567"
assert raw["doi"] == "10.1000/test.123"
assert raw["title"] == "A Study of Testing" # вложенный <italic> склеен
assert raw["journal"] == "Journal of Testing"
assert raw["year"] == 2024
assert raw["abstract"] == "This is the abstract text."
assert raw["full_text"] == "First paragraph of the body. Second paragraph with a ref inline."
def test_parse_article_only_includes_authors_not_editors():
raw = PMCParser()._parse_article(_article())
assert raw["authors"] == [{"last_name": "Ivanov", "first_name": "Ivan"}]
def test_parse_article_without_article_meta_is_empty():
assert PMCParser()._parse_article(ET.fromstring("<article/>")) == {}
def test_transform_maps_to_unified_schema():
raw = PMCParser()._parse_article(_article())
t = PMCParser().transform(raw)
assert t["source"] == "pmc"
assert t["ext_id"] == "pmc:1234567"
assert t["lang"] == "en"
assert t["url"] == "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1234567/"
assert t["authors"][0]["last_name"] == "Ivanov"
assert t["full_text"].startswith("First paragraph")
def test_transform_empty_without_id():
assert PMCParser().transform({"title": "x"}) == {}
def test_parse_articles_batch():
xml = f"<pmc-articleset>{ARTICLE_XML}{ARTICLE_XML}</pmc-articleset>"
parsed = PMCParser()._parse_articles(xml)
assert len(parsed) == 2
assert all(p["id"] == "1234567" for p in parsed)
def test_parse_articles_malformed_xml_returns_empty():
assert PMCParser()._parse_articles("<not valid xml") == []