Создание агрегаторов научных статей

Всем привет! В этом посте я хочу рассказать про некоторые возможности автоматизации задач, с которыми сталкивается программист-исследователь.

7ea28e1e080810098bb2a958a3b6a965.jpeg

В моей работе часто возникает потребность изучать научные статьи. Это помогает быть в курсе событий, а также исследовать новые методы и области.

Бывает очень полезно фиксировать прочитанную информацию, чтобы не приходилось постоянно возвращаться к одному и тому же материалу, чтобы что-то найти. Обычно я веду заметки, но переносить туда ссылки на статьи, скачивать их, создавать таблицы бывает муторно. Огромную работу проделали создатели гитхаб репозиториев-агрегаторов статей по определённым темам. Например, статьи про клоны кода или статьи про фаззинг. Эти репозитории заставили меня задуматься над автоматизацией создания таких списков статей. В результате я определила последовательность действий, которую я выполняю при исследовании новой темы:

  • Шаг 1. Поиск статей в известных источниках (например, Semantic Scholar или arxiv)

  • Шаг 2. Чтение аннотаций к статьям, по которым часто можно понять, подходят ли они по тематике.

  • Шаг 3. Выделение важных объектов из статей: к ним можно отнести ссылки на гитхаб, графики, картинки (чтобы можно было, например, сослаться на результаты статьи в своей презентации, не испортив качество объекта при вставке) , а также год написания и журнал для понимания уровня статьи и актуальности.

  • Шаг 4. Подробное изучение выбранных статей.

Сбор информации для первых трёх шагов можно автоматизировать. Остановимся подробнее на них.

Поиск статей

Semantic Scholar и arxiv имеют специальное API для поиска статей по запросу. С помощью него можно узнать базовую информацию про статьи и скачать их при наличии открытого доступа.

Например, исходя из документации для arxiv, для поиска статей по запросу потребуется написать подобный код:

import arxiv

# Construct the default API client.
client = arxiv.Client()

# Search for the 10 most recent articles matching the keyword "quantum."
search = arxiv.Search(
  query = "quantum",
  max_results = 10,
  sort_by = arxiv.SortCriterion.SubmittedDate
)
results = client.results(search)

Для найденных статей можно получить аннотацию, авторов, год публикации, ссылку на скачивание в случае открытого доступа и т.д:

papers = [
    {
          "title": result.title,
          "authors": ", ".join([author.name for author in result.authors]),
          "journal": result.journal_ref,
          "year": result.published.year,
          "abstract": result.summary,
          "openAccessPdf": {"url": result.pdf_url},
          "paperId": result.entry_id.split("/")[-1], # last part of url after slash
    } for result in results
]

Статьи можно скачать с помощью следующего кода:

results = client.results(search)
paper_to_download = next(results)
paper_to_download.download_pdf(dirpath=".", filename="some-article.pdf")

Для Semantic Scholar примеры использования API есть в репозитории.

Аналогичный по содержанию код для Semantic Scholar API может выглядеть следующим образом:

import requests
import json
from datetime import datetime

x_api_key = "your-api-key" # you can get it from official site

class SemanticScholarClient:
    def __init__(self):
        self.base_url = "https://api.semanticscholar.org/graph/v1"

    def search(self, query, max_results=10, sort_by="relevance"):
        endpoint = f"{self.base_url}/paper/search"
        
        params = {
            "query": query,
            "limit": max_results,
            "fields": "paperId,title,isOpenAccess,openAccessPdf,abstract,year,authors,citationCount,url,venue",
            "sort": sort_by
        }

        headers = {
            "X-API-KEY": x_api_key
        }

        response = requests.get(endpoint, params=params, headers=headers)
        response.raise_for_status()
        return response.json()['data']

class SemanticScholarSearch:
    def __init__(self, query, max_results=10, sort_by="relevance"):
        self.query = query
        self.max_results = max_results
        self.sort_by = sort_by

# Usage
client = SemanticScholarClient()

# Search for the 10 most recent articles matching the keyword "quantum."
search = SemanticScholarSearch(
    query="quantum",
    max_results=10,
    sort_by="publicationDate:desc"  # Sort by publication date, descending
)
results = client.search(search.query, search.max_results, search.sort_by)

papers = [
    {
        "title": result['title'],
        "authors": ", ".join([author['name'] for author in result['authors']]),
        "journal": result.get('venue', ''),
        "year": result['year'],
        "abstract": result.get('abstract', ''),
        "openAccessPdf": {"url": result.get('openAccessPdf', {}).get('url', '')} if result.get('openAccessPdf', {}) else None,
        "paperId": result['paperId'],
    } for result in results if result
]


# Download papers
for paper in papers:
    if paper['openAccessPdf']:
        print(f"Title: {paper['title']}")
        response = requests.get(paper['openAccessPdf']['url'])
        response.raise_for_status()

        filepath = f"{paper['title']}.pdf"

        with open(filepath, 'wb') as f:
            f.write(response.content)

Выделение объектов

Для автоматизации выделения объектов из статьи можно использовать библиотеку pdfplumber. В написании кода с использованием этой библиотеки очень помогла статья. С помощью парсинга элементов в PDF и регулярных выражений можно извлекать существующие в статье ссылки на гитхаб:

import pdfplumber
import re

def extract_text_from_pdf(pdf):
      text = ""
      for page in pdf.pages:
          text += page.extract_text() + "\n"
      return text.strip()

pdf = pdfplumber.open("some-article.pdf")
text_from_pdf = extract_text_from_pdf(pdf)

github_regex = r"https?://(?:www\.)?github\.com/[\w-]+/[\w.-]+[\w-]"
github_links = re.findall(github_regex, text_from_pdf)

Для извлечения картинок подходит библиотека PyMuPDF. Вот подробный гайд про использование библиотеки. Для сохранения картинок из статьи можно написать следующий код:

import fitz
doc = fitz.Document("some-article.pdf")

for i in tqdm(range(len(doc)), desc="pages"):
    cnt = 0 # counter to divide images on one page
    for img in tqdm(doc.get_page_images(i), desc="page_images"):
        xref = img[0]
        image = doc.extract_image(xref)
        pix = fitz.Pixmap(doc, xref)
        pix.save("img_%s_%s.png" % (i, cnt))
        cnt += 1

Не во всех случаях указанным методом удаётся извлечь все изображения из статей, поэтому в этой задаче есть огромное пространство для размышлений и идей по улучшению метода.

Перечисленные этапы реализованы в репозитории Researcher-Helper. На данный момент в нём поддерживается поиск по Semantic Scholar и arxiv. В результате он позволяет сгенерировать по запросу таблицу со статьями в формате html. Буду рада обратной связи, идеям по улучшению и советам в реализации описанных этапов!

© Habrahabr.ru