DEV Community

Rita Carolina
Rita Carolina

Posted on • Edited on

Automatize sua Revisão: Extração de BibTeX em massa do Springer com Python e ChatGPT

🚀 Automatize sua Revisão: Extração de BibTeX em massa do Springer com Python e ChatGPT

Realizar revisões sistemáticas ou construir um repositório bibliográfico de forma eficiente pode ser um desafio, especialmente quando os repositórios acadêmicos não oferecem exportação em lote das citações. Com base nessa dor real, desenvolvi — com apoio do ChatGPT — um script completo que automatiza a extração de referências do SpringerLink via RSS e as transforma em um arquivo .bib pronto para uso no Rayyan, Zotero, Overleaf, entre outros.

Abaixo explico como funciona e como você pode executar o código em poucos minutos no Google Colab ou localmente no seu terminal.


🧠 O que o script faz?

  1. Acessa o RSS de busca do Springer com os filtros definidos (tema, data, idioma, tipo de publicação).
  2. Percorre várias páginas automaticamente (você define quantas).
  3. Extrai os DOIs de cada artigo publicado.
  4. Consulta a API do CrossRef usando content negotiation para obter o BibTeX.
  5. Salva tudo em um único arquivo .bib.

✅ Como rodar no Google Colab

  1. Acesse o Google Colab
  2. Crie um novo notebook e cole o código abaixo
  3. Rode célula por célula
  4. Faça o download do arquivo .bib gerado ao final

💻 Código completo

import requests
import xml.etree.ElementTree as ET
from tqdm import tqdm

def get_dois_from_rss(page_limit=10):
    base_url = "https://link.springer.com/search.rss"
    query_params = {
        "new-search": "true",
        "query": '( "process management" OR "business process management" ) AND ( "artificial intelligence" OR "AI" OR "machine learning" OR "deep learning" OR "neural networks" OR "generative AI" )',
        "content-type": ["Conference Paper", "Article", "Research"],
        "language": "En",
        "date": "custom",
        "dateFrom": "2020",
        "dateTo": "2025",
        "sortBy": "relevance"
    }

    all_dois = []

    for page in range(1, page_limit + 1):
        query_params["page"] = str(page)
        print(f"🔄 Buscando página {page}...")
        try:
            response = requests.get(base_url, params=query_params)
            if response.status_code != 200:
                print(f"⚠️ Falha ao acessar página {page}: {response.status_code}")
                break
            root = ET.fromstring(response.content)
            items = root.findall(".//item")
            if not items:
                print("🚫 Nenhum item encontrado. Parando paginação.")
                break
            for item in items:
                guid = item.find("guid")
                if guid is not None and guid.text.startswith("10."):
                    print(f"✅ DOI encontrado: {guid.text}")
                    all_dois.append(guid.text)
        except Exception as e:
            print(f"❌ Erro na página {page}: {e}")
            break

    return all_dois

def fetch_bibtex(dois):
    bibtex_entries = []
    headers = {"Accept": "application/x-bibtex"}
    for doi in tqdm(dois, desc="🔃 Baixando BibTeX via CrossRef"):
        try:
            r = requests.get(f"https://doi.org/{doi}", headers=headers, timeout=10)
            if r.status_code == 200:
                bibtex_entries.append(r.text)
        except Exception as e:
            print(f"Erro ao buscar BibTeX para DOI {doi}: {e}")
    return bibtex_entries

# 🔧 Executar
dois_coletados = get_dois_from_rss(page_limit=20)
bibtex_data = fetch_bibtex(dois_coletados)

# 💾 Salvar
with open("springer_all_pages.bib", "w", encoding="utf-8") as f:
    for entry in bibtex_data:
        f.write(entry + "\n\n")

print(f"\n✅ Arquivo gerado: springer_all_pages.bib ({len(bibtex_data)} entradas)")
Enter fullscreen mode Exit fullscreen mode

📌 Personalize conforme sua busca

Você pode alterar facilmente:

  • O termo da query (ex: "process mining" ou "knowledge graph")
  • O intervalo de tempo (dateFrom, dateTo)
  • O número de páginas (page_limit=20)

📎 Resultado

Ao final da execução, você terá um arquivo chamado:

springer_all_pages.bib
Enter fullscreen mode Exit fullscreen mode

Pronto para subir no Rayyan, importar no Zotero ou usar em seu projeto em LaTeX.


🧠 Por que usar esse script?

  • Evita o trabalho manual de baixar artigo por artigo.
  • Garante alta confiabilidade (dados vindos direto da CrossRef).
  • Ideal para quem faz revisão sistemática, mapeamento ou meta-análise.

Se quiser adaptar o script para outras bases com RSS, como IEEE Xplore ou PubMed, também é possível. E claro, o ChatGPT pode te ajudar nisso também. 😉

Nova versão que extrai com abstract

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests
import xml.etree.ElementTree as ET
from tqdm import tqdm
import json
import re


def get_rss_from_springer(page_limit=10):
    """Extrai RSS do Springer página por página"""
    base_url = "https://link.springer.com/search.rss"
    query_params = {
        "new-search": "true",
        "query": '''
        ("implicit dependencies" OR "hidden dependencies" OR "undocumented dependencies" OR "implicit relationships" OR "hidden relationships" OR "activity relationships"
        OR "process relationships" OR "tacit dependencies" OR "emergent dependencies") AND ("business process*" OR "organizational process*" OR "process model*"
        OR "process redesign" OR "process reengineering" OR "workflow*" OR "BPMN")
        ''',
        "content-type": ["Conference Paper", "Article", "Research"],
        "language": "En",
        "date": "custom",
        "dateFrom": "2020",
        "dateTo": "2025",
        "sortBy": "relevance"
    }

    all_items = []

    for page in range(1, page_limit + 1):
        query_params["page"] = str(page)
        print(f"🔄 Página {page}...")
        try:
            response = requests.get(base_url, params=query_params, timeout=15)
            if response.status_code != 200:
                print(f"⚠️ Erro: {response.status_code}")
                break

            root = ET.fromstring(response.content)
            items = root.findall(".//item")

            if not items:
                print("🚫 Sem mais itens. Parando.")
                break

            all_items.extend(items)
            print(f"{len(items)} artigos")

        except Exception as e:
            print(f"❌ Erro: {e}")
            break

    return all_items


def extract_article_data(item):
    """Extrai: título, DOI, abstract da tag <description>"""
    data = {}

    title = item.find("title")
    data['title'] = title.text if title is not None else "N/A"

    guid = item.find("guid")
    data['doi'] = guid.text if guid is not None else "N/A"

    desc = item.find("description")
    if desc is not None:
        parts = []
        if desc.text:
            parts.append(desc.text)
        for child in desc:
            if child.text:
                parts.append(child.text)
            if child.tail:
                parts.append(child.tail)

        abstract = ' '.join(parts)
        abstract = re.sub(r'<[^>]+>', '', abstract)
        abstract = re.sub(r'\s+', ' ', abstract).strip()
        data['abstract'] = abstract
    else:
        data['abstract'] = ""

    print(f"DEBUG - Abstract extraído para '{data['title']}...': {data['abstract']}...")

    link = item.find("link")
    data['link'] = link.text if link is not None else "N/A"

    return data


def fetch_crossref_data(doi):
    """Busca dados do CrossRef para enriquecer o BibTeX"""
    try:
        url = f"https://api.crossref.org/works/{doi}"
        response = requests.get(url, timeout=10)

        if response.status_code == 200:
            return response.json()
        return None
    except:
        return None


def extract_from_crossref(metadata):
    """Extrai campos úteis do CrossRef"""
    data = {
        'authors': [],
        'journal': '',
        'volume': '',
        'issue': '',
        'pages': '',
        'year': '',
        'abstract': ''
    }

    if not metadata or 'message' not in metadata:
        return data

    msg = metadata['message']

    try:
        for author in msg.get('author', []):
            name_parts = []
            if 'given' in author:
                name_parts.append(author['given'])
            if 'family' in author:
                name_parts.append(author['family'])
            if name_parts:
                data['authors'].append(' '.join(name_parts))
    except:
        pass

    try:
        container = msg.get('container-title', [])
        data['journal'] = container[0] if isinstance(container, list) else container
    except:
        pass

    data['volume'] = msg.get('volume', '')
    data['issue'] = msg.get('issue', '')
    data['pages'] = msg.get('page', '')

    try:
        if 'issued' in msg and 'date-parts' in msg['issued']:
            date_parts = msg['issued']['date-parts'][0]
            data['year'] = str(date_parts[0]) if date_parts else ''
    except:
        pass

    data['abstract'] = msg.get('abstract', '')

    return data


def generate_bibtex(article, crossref_data):
    """Gera entrada BibTeX completa"""
    key = article['doi'].replace('/', '_').replace('.', '_')

    bibtex = f"@article{{{key},\n"
    bibtex += f"  title = {{{article['title']}}},\n"

    if crossref_data and crossref_data['authors']:
        bibtex += f"  author = {{{' and '.join(crossref_data['authors'])}}},\n"

    if crossref_data and crossref_data['journal']:
        bibtex += f"  journal = {{{crossref_data['journal']}}},\n"

    if crossref_data and crossref_data['volume']:
        bibtex += f"  volume = {{{crossref_data['volume']}}},\n"

    if crossref_data and crossref_data['issue']:
        bibtex += f"  issue = {{{crossref_data['issue']}}},\n"

    if crossref_data and crossref_data['pages']:
        bibtex += f"  pages = {{{crossref_data['pages']}}},\n"

    if crossref_data and crossref_data['year']:
        bibtex += f"  year = {{{crossref_data['year']}}},\n"

    bibtex += f"  doi = {{{article['doi']}}},\n"
    bibtex += f"  url = {{{article['link']}}},\n"

    abstract = article['abstract']
    if not abstract and crossref_data:
        abstract = crossref_data['abstract']

    if abstract:
        abstract_clean = abstract.replace('"', '\\"').replace('\n', ' ')
        bibtex += f"  abstract = {{{abstract_clean}}}\n"
    else:
        bibtex = bibtex.rstrip(',\n') + "\n"

    bibtex += "}\n"
    return bibtex


def main():
    print("\n" + "="*80)
    print("🔍 SPRINGER RSS → BIBTEX COM ABSTRACTS")
    print("="*80 + "\n")

    print("📋 Extraindo RSS...\n")
    items = get_rss_from_springer(page_limit=65)

    if not items:
        print("❌ Nenhum artigo encontrado")
        return

    print(f"\n✅ Total: {len(items)} artigos\n")

    print("🔍 Processando artigos...\n")
    articles = [extract_article_data(item) for item in items]

    print("📡 Buscando CrossRef e gerando BibTeX...\n")

    bibtex_list = []
    all_data = []

    for article in tqdm(articles, desc=""):
        metadata = fetch_crossref_data(article['doi'])
        crossref_data = extract_from_crossref(metadata) if metadata else {}

        bibtex = generate_bibtex(article, crossref_data)
        bibtex_list.append(bibtex)

        article['crossref'] = crossref_data
        article['bibtex'] = bibtex
        all_data.append(article)

    print("\n💾 Salvando...\n")

    with open("springer_articles.bib", "w", encoding="utf-8") as f:
        f.write("\n\n".join(bibtex_list))
    print(f"✅ springer_articles.bib ({len(bibtex_list)} entradas)")

    with open("springer_articles.json", "w", encoding="utf-8") as f:
        json.dump(all_data, f, indent=2, ensure_ascii=False)
    print(f"✅ springer_articles.json")

    print("\n" + "="*80)
    print("📊 RESUMO")
    print("="*80 + "\n")

    for i, article in enumerate(all_data[:5], 1):
        print(f"{i}. {article['title'][:60]}...")
        print(f"   DOI: {article['doi']}")
        if article['abstract']:
            print(f"   Abstract: {article['abstract'][:80]}...")
        print()

    if len(all_data) > 5:
        print(f"... +{len(all_data)-5} artigos\n")

    print("="*80)
    print(f"✅ Processados: {len(all_data)}")
    print(f"✅ Com abstract: {sum(1 for a in all_data if a['abstract'])}")
    print("="*80)


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)