Investigación y fuentes

Gestionar una lista de referencias en CSV y comprobar DOI duplicados o ausentes

Mantén una pequeña lista de referencias de investigación en CSV, normaliza el texto DOI para compararlo y genera un archivo de revisión para valores DOI duplicados o ausentes. El workflow conserva el CSV original y no afirma que un DOI sea válido solo porque esté presente.

Ver el índice

La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. English

Para quién esEsta guía está dirigida a investigadores que mantienen listas de referencias en hojas de cálculo o archivos CSV y quieren una comprobación sencilla de DOI duplicados y ausentes.

Preparación
  • Python 3.12 y un comando de terminal que inicie esa versión.
  • Un editor de texto o programa de hojas de cálculo que pueda guardar archivos CSV en UTF-8.
  • Una carpeta de trabajo donde el script pueda crear una nueva carpeta dentro de outputs.
  • Solo se requiere la biblioteca estándar de Python: csv y pathlib.

01Decidir qué puede y qué no puede decirte la comprobación de DOI

Este workflow comprueba dos problemas prácticos: una reference no tiene DOI value, o dos rows contienen el mismo DOI después de una simple text normalization. No contacta con un DOI registry y, por tanto, no demuestra que un DOI exista, se resuelva correctamente o pertenezca al article citado.

El script elimina textual prefixes comunes como https://doi.org/ y doi:, recorta whitespace y convierte el DOI text restante a lowercase para compararlo. El original DOI text se conserva en el output junto al normalized value.

02Crear una lista de referencias sintética

La siguiente reference list es sintética y fue creada específicamente para este artículo. Las DOI-like strings son datos de demostración y no se presentan como publicaciones reales o resolubles. Guarda el archivo como references.csv.

csv
ref_id,title,year,doi
R001,Synthetic bracket study,2024,10.0000/demo.alpha
R002,Synthetic vibration study,2025,10.0000/demo.beta
R003,Synthetic bracket study copy,2024,https://doi.org/10.0000/demo.alpha
R004,Synthetic fatigue note,2023,
R005,Synthetic surrogate study,2026,doi:10.0000/DEMO.GAMMA
R006,Synthetic optimization study,2026,10.0000/demo.delta

Hay 6 reference rows. Cinco contienen DOI text y una, R004, no tiene DOI. R001 y R003 utilizan textual forms diferentes, pero se normalizan al mismo value. R005 demuestra que la capitalization y un prefix doi: también pueden normalizarse para la comparación.

03Determinar manualmente los problemas esperados

ref_idOriginal DOINormalized DOIIssue esperado
R00110.0000/demo.alpha10.0000/demo.alphaDUPLICATE_DOI
R00210.0000/demo.beta10.0000/demo.betaNone
R003https://doi.org/10.0000/demo.alpha10.0000/demo.alphaDUPLICATE_DOI
R004MISSING_DOI
R005doi:10.0000/DEMO.GAMMA10.0000/demo.gammaNone
R00610.0000/demo.delta10.0000/demo.deltaNone

Los recuentos esperados son 6 references, 5 rows con DOI text, 1 row con missing DOI y 1 duplicate DOI group que contiene 2 rows. Después de la normalización, las cinco DOI entries no vacías contienen 4 distinct DOI values.

04Normalizar el texto DOI y escribir archivos de revisión

Guarda el script siguiente como reference_list_check.py. Escribe una normalized copy de toda la reference list y un issues.csv separado que contiene únicamente las rows que necesitan review. El references.csv original es de solo lectura.

python
import csv
from pathlib import Path

SOURCE = Path("references.csv")
OUTPUT_DIR = Path("outputs") / "reference_list_result"
NORMALIZED = OUTPUT_DIR / "normalized_references.csv"
ISSUES = OUTPUT_DIR / "issues.csv"
REQUIRED = {"ref_id", "title", "year", "doi"}


def normalize_doi(value: str) -> str:
    text = value.strip()
    lowered = text.lower()
    prefixes = (
        "https://doi.org/",
        "http://doi.org/",
        "doi:",
    )
    for prefix in prefixes:
        if lowered.startswith(prefix):
            text = text[len(prefix):].strip()
            break
    return text.lower()


def main() -> None:
    if not SOURCE.is_file():
        raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")

    rows = []
    with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream)
        if reader.fieldnames is None or not REQUIRED.issubset(reader.fieldnames):
            raise ValueError("CSV is missing a required column.")

        seen_ids = set()
        for row in reader:
            ref_id = row["ref_id"].strip()
            if not ref_id:
                raise ValueError("ref_id must not be blank.")
            if ref_id in seen_ids:
                raise ValueError(f"Duplicate ref_id: {ref_id}")
            seen_ids.add(ref_id)

            rows.append({
                "ref_id": ref_id,
                "title": row["title"],
                "year": row["year"],
                "doi": row["doi"],
                "normalized_doi": normalize_doi(row["doi"]),
            })

    by_doi = {}
    for row in rows:
        doi = row["normalized_doi"]
        if doi:
            by_doi.setdefault(doi, []).append(row["ref_id"])

    duplicate_dois = {
        doi: ids for doi, ids in by_doi.items() if len(ids) > 1
    }

    issue_rows = []
    for row in rows:
        doi = row["normalized_doi"]
        if not doi:
            issue_rows.append({
                "ref_id": row["ref_id"],
                "issue": "MISSING_DOI",
                "normalized_doi": "",
                "related_ref_ids": "",
            })
        elif doi in duplicate_dois:
            issue_rows.append({
                "ref_id": row["ref_id"],
                "issue": "DUPLICATE_DOI",
                "normalized_doi": doi,
                "related_ref_ids": ";".join(duplicate_dois[doi]),
            })

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()

    normalized_fields = ["ref_id", "title", "year", "doi", "normalized_doi"]
    with NORMALIZED.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=normalized_fields)
        writer.writeheader()
        writer.writerows(rows)

    issue_fields = ["ref_id", "issue", "normalized_doi", "related_ref_ids"]
    with ISSUES.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=issue_fields)
        writer.writeheader()
        writer.writerows(issue_rows)

    missing_count = sum(row["issue"] == "MISSING_DOI" for row in issue_rows)
    print(f"References checked: {len(rows)}.")
    print(f"Missing DOI rows: {missing_count}.")
    print(f"Duplicate DOI groups: {len(duplicate_dois)}.")
    print(f"Issue rows: {len(issue_rows)}.")
    print(f"Output folder: {OUTPUT_DIR.as_posix()}")


if __name__ == "__main__":
    main()

05Comparar el informe de revisión esperado

issues.csv debería contener tres data rows: R001 y R003 por el normalized DOI duplicado, más R004 por el missing DOI.

csv
ref_id,issue,normalized_doi,related_ref_ids
R001,DUPLICATE_DOI,10.0000/demo.alpha,R001;R003
R003,DUPLICATE_DOI,10.0000/demo.alpha,R001;R003
R004,MISSING_DOI,,

La salida de consola esperada que aparece a continuación se obtuvo manualmente a partir de la synthetic list y el script. No es un execution log capturado.

text
References checked: 6.
Missing DOI rows: 1.
Duplicate DOI groups: 1.
Issue rows: 3.
Output folder: outputs/reference_list_result

06Revisar las filas marcadas en lugar de eliminarlas automáticamente

  • Confirma que R001 y R003 se normalicen al mismo DOI text.
  • Confirma que R004 se conserve en la normalized reference list aunque falte su DOI.
  • Comprueba paper title, authors, year y publication details antes de decidir si las duplicate-DOI rows deben fusionarse.
  • Para las missing DOI rows, verifica si la publication tiene realmente un DOI antes de añadir uno.
  • Ejecuta de nuevo el script sin cambiar OUTPUT_DIR. Debería detenerse con FileExistsError en lugar de sustituir la review anterior.
ProblemaQué comprobar
El mismo paper se introdujo dos veces con distinto DOI formattingNormaliza los DOI URL y prefixes doi: comunes antes de comparar.
Un DOI en blanco se trata como duplicadoMantén la missing DOI detection separada del duplicate DOI grouping.
Papers diferentes comparten un title parecidoNo utilices únicamente title similarity como prueba de que los records son duplicates.
Hay una string que parece un DOILa presencia no demuestra validity; verifica las references importantes con una trusted metadata source.
Se sobrescribe un result existenteUtiliza una output folder nueva para conservar la previous review evidence.

07Comprender los límites de la deduplicación basada en DOI

DOI matching es útil cuando hay DOI metadata, pero un missing DOI no significa que una reference sea invalid o incomplete. Algunas publications o document types pueden no tener DOI, y older records pueden requerir otros identifiers o manual bibliographic checks.

Este script realiza únicamente text normalization. No resuelve DOI links, consulta Crossref u otra registration agency, verifica titles o authors, detecta retractions ni determina si dos DOI values distintos corresponden a related versions de una obra.

Para una research database real, conserva fields como authors, title, year, journal, volume, pages, DOI y another local reference ID. Trata la automatic duplicate detection como una ayuda para review y no como una instrucción para eliminar bibliographic records.

Registro de ejecución y verificación

2026-09-20 · ejemplo revisado manualmente · objetivo: Python 3.12 · biblioteca estándar: csv, pathlib · sin ejecución

  • Se contaron manualmente 6 synthetic reference rows, 5 rows con DOI y 1 missing DOI row.
  • Se normalizaron manualmente R001 y R003 al mismo value, 10.0000/demo.alpha.
  • Se normalizó manualmente R005 de doi:10.0000/DEMO.GAMMA a 10.0000/demo.gamma.
  • Se calcularon 4 distinct normalized DOI values entre las 5 DOI rows no vacías.
  • Se obtuvo el expected issue report como 2 duplicate-DOI rows más 1 missing-DOI row.
  • Se revisó el script para duplicate ref_id checks, DOI normalization, tratamiento separado de missing y duplicate, output collision protection y conservación del CSV original.
Límites de la verificación
  • El código no fue ejecutado por el autor de esta respuesta; no se crearon archivos CSV de salida.
  • Las synthetic DOI-like strings no se comprobaron para registration o resolution y no se presentan como publicaciones reales.
  • No se probaron DOI validity, bibliographic metadata accuracy, title similarity, retractions ni related article versions.
  • Las URL de la documentación oficial se proporcionaron a partir de ubicaciones de documentación conocidas, pero no se comprobaron en línea.

Criterios de redacción y verificación de todo el sitio

Fuentes de referencia

Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.