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

ChatGPT Python tool · Python 3.x · standard library: csv, pathlib

  • La ejecución terminó correctamente para 6 referencias.
  • Se confirmó 1 fila con DOI faltante: R004.
  • Se confirmó 1 grupo de DOI duplicado: el DOI normalizado 10.0000/demo.alpha compartido por R001 y R003.
  • Se confirmó que issues.csv contiene exactamente 3 filas de datos: R001 DUPLICATE_DOI, R003 DUPLICATE_DOI y R004 MISSING_DOI.
  • Se confirmó que R005 se normalizó de doi:10.0000/DEMO.GAMMA a 10.0000/demo.gamma.
  • Se confirmó que hay 4 valores distintos de DOI normalizado no vacío entre las 5 filas que contienen DOI.
  • La salida de consola coincidió exactamente con el artículo: 6 referencias, 1 fila con DOI faltante, 1 grupo de DOI duplicado, 3 filas de incidencias y la carpeta de salida esperada.
  • El issues.csv generado coincidió exactamente con el CSV esperado del artículo.
  • La segunda ejecución produjo FileExistsError para outputs/reference_list_result, confirmando la protección contra sobrescritura.
  • La comparación SHA-256 confirmó que references.csv permaneció sin cambios.
Límites de la verificación
  • El script solo realiza normalización de texto; no se verificaron externamente el registro o resolución de DOI, los metadatos de título o autor, las retractaciones ni la corrección bibliográfica.
  • El entorno de ejecución emitió en stderr una advertencia no relacionada del calentamiento del entorno de hojas de cálculo; no afectó al estado de salida del script, a los archivos generados ni a los resultados de verificación.

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.