← Files Pesquisa MirandasTechARCHIVED FILE
scripts/orcid_lattes_diff.py
4.31 KB · Oct 2, 2026 · 00:33 UTC
#!/usr/bin/env python3
"""Compara os artigos do registro ORCID (API pública) com os do Currículo Lattes (XML exportado).
Uso:
python3 orcid_lattes_diff.py --orcid 0000-0002-1825-0097 [--lattes curriculo.xml | curriculo.zip] [--saida DIFERENCAS.md]
- ORCID: lê pub.orcid.org/v3.0/<iD>/works (registro público; só o que está visível para «Everyone»).
- Lattes: lê o XML exportado do currículo (menu «Exportar» no Lattes; aceita o .zip com curriculo.xml)
e extrai ARTIGO-PUBLICADO (título, ano, DOI, periódico).
- Compara por DOI (normalizado) e, sem DOI, por título; lista o que só está num lado.
O script não altera nenhum registro: ele mostra a lista para o autor decidir o que incluir onde.
"""
import argparse, json, re, sys, urllib.request, zipfile, io
import xml.etree.ElementTree as ET
from pathlib import Path
def norm_doi(d):
d = (d or "").strip().lower(); d = re.sub(r"^https?://(dx\.)?doi\.org/", "", d); return d or None
def norm_title(t): return re.sub(r"[^a-z0-9]+", " ", (t or "").lower()).strip()
def orcid_works(iD):
if not re.fullmatch(r"\d{4}-\d{4}-\d{4}-\d{3}[\dX]", iD): sys.exit("iD ORCID inválido: use o formato 0000-0000-0000-0000")
req = urllib.request.Request(f"https://pub.orcid.org/v3.0/{iD}/works", headers={"Accept": "application/json"})
d = json.load(urllib.request.urlopen(req, timeout=30)); w = []
for g in d.get("group", []):
s = g["work-summary"][0]; ids = {i["external-id-type"]: i["external-id-value"] for i in g["external-ids"]["external-id"]}
ano = (s.get("publication-date") or {}).get("year") or {}
w.append({"titulo": s["title"]["title"]["value"], "ano": ano.get("value", "—"), "doi": norm_doi(ids.get("doi")),
"tipo": s.get("type", ""), "fonte": (s["source"].get("source-name") or {}).get("value", "")})
return w
def lattes_artigos(caminho):
p = Path(caminho); raw = p.read_bytes()
if p.suffix.lower() == ".zip":
with zipfile.ZipFile(io.BytesIO(raw)) as z: raw = z.read([n for n in z.namelist() if n.lower().endswith(".xml")][0])
root = ET.fromstring(raw); arts = [] # o parser respeita a declaração de codificação do XML (o Lattes exporta em ISO-8859-1)
for a in root.iter("ARTIGO-PUBLICADO"):
b = a.find("DADOS-BASICOS-DO-ARTIGO"); det = a.find("DETALHAMENTO-DO-ARTIGO")
arts.append({"titulo": b.get("TITULO-DO-ARTIGO", ""), "ano": b.get("ANO-DO-ARTIGO", "—"), "doi": norm_doi(b.get("DOI")),
"periodico": det.get("TITULO-DO-PERIODICO-OU-REVISTA", "") if det is not None else ""})
return arts
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--orcid", required=True); ap.add_argument("--lattes"); ap.add_argument("--saida", default="DIFERENCAS.md")
a = ap.parse_args()
o = orcid_works(a.orcid); print(f"ORCID {a.orcid}: {len(o)} trabalho(s) público(s)")
for w in o: print(f" {w['ano']} {w['titulo'][:70]} DOI {w['doi'] or '—'} [{w['fonte']}]")
if not a.lattes:
print("sem --lattes: exporte o XML do currículo (Lattes → Exportar) para comparar."); return
l = lattes_artigos(a.lattes); print(f"Lattes ({Path(a.lattes).name}): {len(l)} artigo(s) publicado(s)")
key = lambda x: x["doi"] or "t:" + norm_title(x["titulo"])
ko = {key(w): w for w in o}; kl = {key(x): x for x in l}
so_orcid = [ko[k] for k in ko if k not in kl]; so_lattes = [kl[k] for k in kl if k not in ko]; ambos = [k for k in ko if k in kl]
out = [f"# ORCID × Lattes — {a.orcid}", "", f"- ORCID: {len(o)} trabalho(s) · Lattes: {len(l)} artigo(s) · em ambos: {len(ambos)}", ""]
for t, lst in (("Só no ORCID (candidatos a incluir no Lattes)", so_orcid), ("Só no Lattes (candidatos a incluir no ORCID, por DOI)", so_lattes)):
out += [f"## {t} — {len(lst)}", "", "| Ano | Título | DOI |", "|---|---|---|"] + [f"| {x['ano']} | {x['titulo']} | {x['doi'] or '—'} |" for x in lst] + [""]
out += ["Chave de comparação: DOI normalizado; sem DOI, o título. Nada foi alterado em nenhum registro — inclusão é decisão do autor."]
Path(a.saida).write_text("\n".join(out) + "\n", encoding="utf-8")
print(f"só no ORCID: {len(so_orcid)} · só no Lattes: {len(so_lattes)} · em ambos: {len(ambos)} → gravado: {a.saida}")
if __name__ == "__main__":
main()
SHA-256: a2ec1339b918eb540c88dc216569fbe4dae83120ddb3b160932bb6cbccb078d7