BibexPy 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bibex_core/MergeDB.py +1189 -0
- bibex_core/__init__.py +13 -0
- bibex_core/modules/__init__.py +35 -0
- bibex_core/modules/api_utils.py +1172 -0
- bibex_core/modules/c1_utils.py +423 -0
- bibex_core/modules/file_utils.py +71 -0
- bibex_core/modules/ml_utils.py +332 -0
- bibex_core/modules/post_process.py +50 -0
- bibex_core/modules/stats_utils.py +216 -0
- bibex_core/scp2xlsx.py +235 -0
- bibex_core/wos2xlsx.py +159 -0
- bibex_core/xlsx2vos.py +185 -0
- bibexpy/__init__.py +9 -0
- bibexpy/__main__.py +6 -0
- bibexpy/_server/__init__.py +1 -0
- bibexpy/_server/config.py +92 -0
- bibexpy/_server/jobs/__init__.py +3 -0
- bibexpy/_server/jobs/runner.py +301 -0
- bibexpy/_server/main.py +211 -0
- bibexpy/_server/models/__init__.py +0 -0
- bibexpy/_server/models/project.py +25 -0
- bibexpy/_server/routers/__init__.py +0 -0
- bibexpy/_server/routers/audit.py +65 -0
- bibexpy/_server/routers/convert.py +101 -0
- bibexpy/_server/routers/disambiguate.py +177 -0
- bibexpy/_server/routers/downloads.py +26 -0
- bibexpy/_server/routers/enrich.py +42 -0
- bibexpy/_server/routers/export.py +116 -0
- bibexpy/_server/routers/export_folder.py +97 -0
- bibexpy/_server/routers/filter.py +131 -0
- bibexpy/_server/routers/jobs.py +72 -0
- bibexpy/_server/routers/merge.py +437 -0
- bibexpy/_server/routers/prepare.py +241 -0
- bibexpy/_server/routers/projects.py +49 -0
- bibexpy/_server/routers/quality.py +288 -0
- bibexpy/_server/routers/records.py +204 -0
- bibexpy/_server/routers/report.py +111 -0
- bibexpy/_server/routers/settings.py +383 -0
- bibexpy/_server/routers/system.py +209 -0
- bibexpy/_server/routers/tools.py +257 -0
- bibexpy/_server/routers/upload.py +110 -0
- bibexpy/_server/services/__init__.py +0 -0
- bibexpy/_server/services/analyses.py +364 -0
- bibexpy/_server/services/audit.py +340 -0
- bibexpy/_server/services/bibex_adapter.py +47 -0
- bibexpy/_server/services/bibtex_writer.py +104 -0
- bibexpy/_server/services/converter.py +205 -0
- bibexpy/_server/services/disambiguation/__init__.py +19 -0
- bibexpy/_server/services/disambiguation/blocking.py +581 -0
- bibexpy/_server/services/disambiguation/cache.py +47 -0
- bibexpy/_server/services/disambiguation/deepseek_client.py +203 -0
- bibexpy/_server/services/disambiguation/orcid.py +261 -0
- bibexpy/_server/services/disambiguation/pipeline.py +985 -0
- bibexpy/_server/services/disambiguation/similarity.py +91 -0
- bibexpy/_server/services/enricher.py +338 -0
- bibexpy/_server/services/exporter.py +112 -0
- bibexpy/_server/services/filter_engine.py +300 -0
- bibexpy/_server/services/merger.py +202 -0
- bibexpy/_server/services/methodology.py +102 -0
- bibexpy/_server/services/report_export.py +300 -0
- bibexpy/_server/services/ris_writer.py +62 -0
- bibexpy/_server/services/smart_merger.py +967 -0
- bibexpy/_server/services/storage.py +165 -0
- bibexpy/_web/404/index.html +1 -0
- bibexpy/_web/404.html +1 -0
- bibexpy/_web/_next/static/chunks/117-7ad5186126fa076d.js +2 -0
- bibexpy/_web/_next/static/chunks/156-75beffb8ae92a7f3.js +1 -0
- bibexpy/_web/_next/static/chunks/169-301c04fcec22686f.js +4 -0
- bibexpy/_web/_next/static/chunks/2-ecc1c36a5632ac61.js +1 -0
- bibexpy/_web/_next/static/chunks/244-be343e0f8aa00fb5.js +1 -0
- bibexpy/_web/_next/static/chunks/367-b7457c2b95f6932b.js +1 -0
- bibexpy/_web/_next/static/chunks/382-9cd163eed632e241.js +1 -0
- bibexpy/_web/_next/static/chunks/392-afa8bfa4c1a9893d.js +1 -0
- bibexpy/_web/_next/static/chunks/507-63607ded9f83c1aa.js +1 -0
- bibexpy/_web/_next/static/chunks/624-cae565632f47217d.js +1 -0
- bibexpy/_web/_next/static/chunks/739-a00c6b9c0f11813e.js +1 -0
- bibexpy/_web/_next/static/chunks/862-8e1bd5f93c082c02.js +1 -0
- bibexpy/_web/_next/static/chunks/app/_not-found/page-873ba59b5eb7c342.js +1 -0
- bibexpy/_web/_next/static/chunks/app/layout-0d5fc68ac8971a04.js +1 -0
- bibexpy/_web/_next/static/chunks/app/page-cf2bea5edc74459a.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/convert/page-ab6dd645ae113b64.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/disambiguate/page-dd98482cc47344e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/enrich/page-4727e431468514e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/export/page-f9005d164bb22cb0.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/layout-07755fda55b3ae1f.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/merge/page-45b95318ea7beccf.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/records/page-bfad80457da9eb0b.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/report/page-f05767e74158e995.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/[id]/upload/page-8df78ca9cdecbcb8.js +1 -0
- bibexpy/_web/_next/static/chunks/app/projects/page-99960711ac6a13e9.js +1 -0
- bibexpy/_web/_next/static/chunks/app/settings/page-002df809d6c5a227.js +1 -0
- bibexpy/_web/_next/static/chunks/app/tools/page-27eee1c6a590859f.js +1 -0
- bibexpy/_web/_next/static/chunks/fd9d1056-f2718be92181f426.js +1 -0
- bibexpy/_web/_next/static/chunks/framework-f66176bb897dc684.js +1 -0
- bibexpy/_web/_next/static/chunks/main-app-d138a6e2f8c847f6.js +1 -0
- bibexpy/_web/_next/static/chunks/main-c4b9ddbf514503ef.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_app-72b849fbd24ac258.js +1 -0
- bibexpy/_web/_next/static/chunks/pages/_error-7ba65e1336b92748.js +1 -0
- bibexpy/_web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- bibexpy/_web/_next/static/chunks/webpack-c81f7fd28659d64f.js +1 -0
- bibexpy/_web/_next/static/css/1c4638f3e39cf127.css +3 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_buildManifest.js +1 -0
- bibexpy/_web/_next/static/ypx2IQAkZiSaFiKWnzZGB/_ssgManifest.js +1 -0
- bibexpy/_web/images/PROMPTS.md +83 -0
- bibexpy/_web/images/about-illustration.png +0 -0
- bibexpy/_web/images/authors/alperen-sahin.jpg +0 -0
- bibexpy/_web/images/authors/burak-can-kara.jpg +0 -0
- bibexpy/_web/images/authors/taskin-dirsehan.jpg +0 -0
- bibexpy/_web/images/bibexpy-logo-full.png +0 -0
- bibexpy/_web/images/bibexpy-logo-header.png +0 -0
- bibexpy/_web/images/bibexpy-logo-in.png +0 -0
- bibexpy/_web/images/bibexpy-logo.webp +0 -0
- bibexpy/_web/images/hero-illustration.png +0 -0
- bibexpy/_web/images/loader.gif +0 -0
- bibexpy/_web/images/workflow-diagram.png +0 -0
- bibexpy/_web/index.html +1 -0
- bibexpy/_web/index.txt +11 -0
- bibexpy/_web/projects/_/convert/index.html +1 -0
- bibexpy/_web/projects/_/convert/index.txt +12 -0
- bibexpy/_web/projects/_/disambiguate/index.html +1 -0
- bibexpy/_web/projects/_/disambiguate/index.txt +12 -0
- bibexpy/_web/projects/_/enrich/index.html +1 -0
- bibexpy/_web/projects/_/enrich/index.txt +12 -0
- bibexpy/_web/projects/_/export/index.html +1 -0
- bibexpy/_web/projects/_/export/index.txt +12 -0
- bibexpy/_web/projects/_/merge/index.html +1 -0
- bibexpy/_web/projects/_/merge/index.txt +12 -0
- bibexpy/_web/projects/_/records/index.html +1 -0
- bibexpy/_web/projects/_/records/index.txt +12 -0
- bibexpy/_web/projects/_/report/index.html +1 -0
- bibexpy/_web/projects/_/report/index.txt +12 -0
- bibexpy/_web/projects/_/upload/index.html +1 -0
- bibexpy/_web/projects/_/upload/index.txt +12 -0
- bibexpy/_web/projects/index.html +1 -0
- bibexpy/_web/projects/index.txt +11 -0
- bibexpy/_web/settings/index.html +1 -0
- bibexpy/_web/settings/index.txt +11 -0
- bibexpy/_web/tools/README.md +65 -0
- bibexpy/_web/tools/bibexcel.svg +6 -0
- bibexpy/_web/tools/bibliometrix.png +0 -0
- bibexpy/_web/tools/biblioshiny.svg +12 -0
- bibexpy/_web/tools/citavi.svg +6 -0
- bibexpy/_web/tools/citespace.svg +7 -0
- bibexpy/_web/tools/citnetexplorer.svg +8 -0
- bibexpy/_web/tools/endnote.svg +6 -0
- bibexpy/_web/tools/excel.svg +1 -0
- bibexpy/_web/tools/gephi.svg +66 -0
- bibexpy/_web/tools/histcite.svg +7 -0
- bibexpy/_web/tools/index.html +1 -0
- bibexpy/_web/tools/index.txt +11 -0
- bibexpy/_web/tools/jabref.svg +122 -0
- bibexpy/_web/tools/latex.svg +1 -0
- bibexpy/_web/tools/mendeley.svg +1 -0
- bibexpy/_web/tools/openrefine.svg +15 -0
- bibexpy/_web/tools/overleaf.svg +1 -0
- bibexpy/_web/tools/papers.svg +7 -0
- bibexpy/_web/tools/powerbi.svg +1 -0
- bibexpy/_web/tools/python.svg +1 -0
- bibexpy/_web/tools/r.svg +1 -0
- bibexpy/_web/tools/refworks.svg +5 -0
- bibexpy/_web/tools/scite.svg +5 -0
- bibexpy/_web/tools/tableau.svg +1 -0
- bibexpy/_web/tools/vosviewer.png +0 -0
- bibexpy/_web/tools/zotero.svg +1 -0
- bibexpy/cli.py +242 -0
- bibexpy-2.0.0.dist-info/METADATA +105 -0
- bibexpy-2.0.0.dist-info/RECORD +171 -0
- bibexpy-2.0.0.dist-info/WHEEL +5 -0
- bibexpy-2.0.0.dist-info/entry_points.txt +2 -0
- bibexpy-2.0.0.dist-info/licenses/LICENSE +674 -0
- bibexpy-2.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import difflib
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
# Load .env file
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
# Tüm dış API çağrıları için (connect, read) timeout — tek bir API takılırsa
|
|
15
|
+
# tüm enrichment işinin süresiz kilitlenmesini önler.
|
|
16
|
+
REQUEST_TIMEOUT = (5, 20)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_with_retry(url: str, *, headers: dict | None = None, attempts: int = 3, **kw):
|
|
20
|
+
"""requests.get + zorunlu timeout + 429/503 üstel backoff (Retry-After saygılı)."""
|
|
21
|
+
last = None
|
|
22
|
+
for i in range(attempts):
|
|
23
|
+
try:
|
|
24
|
+
r = requests.get(url, headers=headers, timeout=REQUEST_TIMEOUT, **kw)
|
|
25
|
+
except requests.RequestException as e:
|
|
26
|
+
last = e
|
|
27
|
+
time.sleep(min(2 ** i, 8))
|
|
28
|
+
continue
|
|
29
|
+
if r.status_code in (429, 503):
|
|
30
|
+
retry_after = r.headers.get("Retry-After")
|
|
31
|
+
try:
|
|
32
|
+
delay = float(retry_after) if retry_after else min(2 ** i, 8)
|
|
33
|
+
except (TypeError, ValueError):
|
|
34
|
+
delay = min(2 ** i, 8)
|
|
35
|
+
time.sleep(min(delay, 15))
|
|
36
|
+
last = r
|
|
37
|
+
continue
|
|
38
|
+
return r
|
|
39
|
+
if isinstance(last, requests.Response):
|
|
40
|
+
return last
|
|
41
|
+
raise last if last else requests.RequestException(f"request failed: {url}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Ters DOI arama (eksik DI'yi başlık+yıl+yazardan bul) ────────────────────────
|
|
45
|
+
def _norm_title(s) -> str:
|
|
46
|
+
s = re.sub(r"[^a-z0-9 ]", " ", str(s).lower())
|
|
47
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _title_sim(a, b) -> float:
|
|
51
|
+
na, nb = _norm_title(a), _norm_title(b)
|
|
52
|
+
if not na or not nb:
|
|
53
|
+
return 0.0
|
|
54
|
+
return difflib.SequenceMatcher(None, na, nb).ratio()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _first_surname(authors) -> str:
|
|
58
|
+
"""'ZHANG C; GUO R' / 'Zhang, Chen; ...' -> ilk yazarın soyadı (küçük harf)."""
|
|
59
|
+
if not authors:
|
|
60
|
+
return ""
|
|
61
|
+
first = str(authors).split(";")[0].strip()
|
|
62
|
+
first = first.split(",")[0].strip()
|
|
63
|
+
parts = first.split()
|
|
64
|
+
return (parts[0] if parts else "").lower()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _verify_candidate(c_title, c_year, c_surname, title, year, surname) -> bool:
|
|
68
|
+
"""Dengeli ama KESİN doğrulama: başlık benzerliği >= 0.90, yıl ±1, ilk yazar soyadı.
|
|
69
|
+
|
|
70
|
+
Kesinlik (precision) > geri-çağırma: emin olunmayan aday REDDEDİLİR (yanlış DOI
|
|
71
|
+
yazmaktansa boş bırak).
|
|
72
|
+
• Yıl: ikisi de varsa ve parse edilebiliyorsa ±1; parse edilemezse aday REDDEDİLİR
|
|
73
|
+
(sessizce atlanmaz — eski hata: except: pass yılı tamamen devre dışı bırakıyordu).
|
|
74
|
+
• Soyad: ikisi de varsa TAM eşleşmeli (substring değil — 'li' artık 'oliveira'yı
|
|
75
|
+
kabul etmez).
|
|
76
|
+
"""
|
|
77
|
+
if _title_sim(c_title, title) < 0.90:
|
|
78
|
+
return False
|
|
79
|
+
if year and c_year:
|
|
80
|
+
try:
|
|
81
|
+
if abs(int(float(str(c_year))) - int(float(str(year)))) > 1:
|
|
82
|
+
return False
|
|
83
|
+
except (TypeError, ValueError):
|
|
84
|
+
return False # aday yılı yorumlanamıyor → güvenli tarafta kal, reddet
|
|
85
|
+
if surname and c_surname and surname != c_surname:
|
|
86
|
+
return False # tam eşleşme şart (substring kabul edilmez)
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _crossref_candidates(title: str, crossref_email: str) -> list:
|
|
91
|
+
out = []
|
|
92
|
+
try:
|
|
93
|
+
params = {"query.bibliographic": title, "rows": 5}
|
|
94
|
+
if crossref_email:
|
|
95
|
+
params["mailto"] = crossref_email
|
|
96
|
+
r = _get_with_retry("https://api.crossref.org/works", params=params)
|
|
97
|
+
if r.ok:
|
|
98
|
+
for it in r.json().get("message", {}).get("items", []):
|
|
99
|
+
ti = (it.get("title") or [""])[0]
|
|
100
|
+
issued = (it.get("issued") or {}).get("date-parts") or [[None]]
|
|
101
|
+
cy = issued[0][0] if issued and issued[0] else None
|
|
102
|
+
au = it.get("author") or []
|
|
103
|
+
csur = (au[0].get("family", "") if au else "").lower()
|
|
104
|
+
out.append((it.get("DOI", ""), ti, cy, csur))
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _openalex_candidates(title: str, year: str, crossref_email: str) -> list:
|
|
111
|
+
out = []
|
|
112
|
+
try:
|
|
113
|
+
flt = f"title.search:{title}"
|
|
114
|
+
if year:
|
|
115
|
+
flt += f",publication_year:{year}"
|
|
116
|
+
params = {"filter": flt, "per_page": 5}
|
|
117
|
+
if crossref_email:
|
|
118
|
+
params["mailto"] = crossref_email
|
|
119
|
+
r = _get_with_retry("https://api.openalex.org/works", params=params)
|
|
120
|
+
if r.ok:
|
|
121
|
+
for w in r.json().get("results", []):
|
|
122
|
+
doi = (w.get("doi") or "").replace("https://doi.org/", "")
|
|
123
|
+
ti = w.get("display_name") or w.get("title") or ""
|
|
124
|
+
cy = w.get("publication_year")
|
|
125
|
+
auth = w.get("authorships") or []
|
|
126
|
+
nm = ((auth[0].get("author") or {}).get("display_name", "") if auth else "")
|
|
127
|
+
csur = nm.split()[-1].lower() if nm else ""
|
|
128
|
+
out.append((doi, ti, cy, csur))
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_DOI_LOOKUP_CACHE: dict = {}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _resolve_doi_cached(title: str, year: str, surname: str, crossref_email: str) -> str:
|
|
138
|
+
key = (title, year, surname, crossref_email)
|
|
139
|
+
if key in _DOI_LOOKUP_CACHE:
|
|
140
|
+
return _DOI_LOOKUP_CACHE[key]
|
|
141
|
+
cands = _crossref_candidates(title, crossref_email) + _openalex_candidates(title, year, crossref_email)
|
|
142
|
+
best_doi, best_sim = "", 0.0
|
|
143
|
+
for doi, c_title, c_year, c_surname in cands:
|
|
144
|
+
if not doi:
|
|
145
|
+
continue
|
|
146
|
+
if not _verify_candidate(c_title, c_year, c_surname, title, year, surname):
|
|
147
|
+
continue
|
|
148
|
+
sim = _title_sim(c_title, title)
|
|
149
|
+
if sim > best_sim:
|
|
150
|
+
best_sim, best_doi = sim, doi
|
|
151
|
+
# YALNIZ başarılı sonucu cache'le — geçici ağ hatası "boş" olarak kalıcı yapışmasın.
|
|
152
|
+
if best_doi:
|
|
153
|
+
_DOI_LOOKUP_CACHE[key] = best_doi
|
|
154
|
+
return best_doi
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def resolve_doi(title, authors=None, year=None, *, crossref_email: str | None = None):
|
|
158
|
+
"""Eksik DOI'yi başlık+yıl+yazardan ters arar (CrossRef -> OpenAlex), doğrular, döndürür.
|
|
159
|
+
|
|
160
|
+
Dengeli eşik: başlık ~0.90, yıl ±1, (varsa) ilk yazar soyadı. Emin değilse None
|
|
161
|
+
döner (asla uydurmaz). Sonuç (title, year, surname, email) ile cache'lenir.
|
|
162
|
+
"""
|
|
163
|
+
if not title or not str(title).strip():
|
|
164
|
+
return None
|
|
165
|
+
surname = _first_surname(authors)
|
|
166
|
+
yr = ""
|
|
167
|
+
if year not in (None, ""):
|
|
168
|
+
try:
|
|
169
|
+
yr = str(int(float(str(year))))
|
|
170
|
+
except (TypeError, ValueError):
|
|
171
|
+
yr = ""
|
|
172
|
+
doi = _resolve_doi_cached(str(title).strip()[:300], yr, surname, crossref_email or "")
|
|
173
|
+
return doi or None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _decode_inverted_abstract(inv: dict) -> str:
|
|
177
|
+
"""OpenAlex abstract_inverted_index ({token: [pozisyonlar]}) → düz metin."""
|
|
178
|
+
if not isinstance(inv, dict) or not inv:
|
|
179
|
+
return ""
|
|
180
|
+
positions: list[tuple[int, str]] = []
|
|
181
|
+
for token, idxs in inv.items():
|
|
182
|
+
if isinstance(idxs, list):
|
|
183
|
+
for p in idxs:
|
|
184
|
+
positions.append((int(p), token))
|
|
185
|
+
positions.sort(key=lambda x: x[0])
|
|
186
|
+
return " ".join(tok for _, tok in positions)
|
|
187
|
+
|
|
188
|
+
def load_api_config() -> dict:
|
|
189
|
+
"""Load API configuration from API_config.json.
|
|
190
|
+
|
|
191
|
+
Once BIBEXPY_CONFIG_DIR (paket modu, orn. ~/.bibexpy), sonra kaynak-agaci
|
|
192
|
+
konumuna bakar. Bulunamazsa sessizce bos dict doner — v2 sunucusunda API
|
|
193
|
+
anahtarlari env/config.py'den gelir, bu dosya zorunlu degildir.
|
|
194
|
+
"""
|
|
195
|
+
candidates = []
|
|
196
|
+
cfg_dir = os.environ.get("BIBEXPY_CONFIG_DIR")
|
|
197
|
+
if cfg_dir:
|
|
198
|
+
candidates.append(os.path.join(os.path.expanduser(cfg_dir), 'API_config.json'))
|
|
199
|
+
candidates.append(
|
|
200
|
+
os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'API_config.json')
|
|
201
|
+
)
|
|
202
|
+
config_path = next((p for p in candidates if os.path.exists(p)), None)
|
|
203
|
+
if not config_path:
|
|
204
|
+
return {}
|
|
205
|
+
try:
|
|
206
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
207
|
+
return json.load(f)
|
|
208
|
+
except Exception as e:
|
|
209
|
+
print(f"\nError loading API configuration: {str(e)}")
|
|
210
|
+
return {}
|
|
211
|
+
|
|
212
|
+
def get_api_credential(service: str, field: str = 'api_key') -> str:
|
|
213
|
+
"""Get API credential for a specific service"""
|
|
214
|
+
config = load_api_config()
|
|
215
|
+
if service in config and field in config[service]:
|
|
216
|
+
return config[service][field]
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
@lru_cache(maxsize=4096)
|
|
220
|
+
def extract_metadata_from_crossref(doi: str, email: str = None) -> dict:
|
|
221
|
+
"""Extract metadata from CrossRef API (Free, no key required)"""
|
|
222
|
+
try:
|
|
223
|
+
url = f"https://api.crossref.org/works/{doi}"
|
|
224
|
+
headers = {
|
|
225
|
+
'User-Agent': f'BibexPy/2.0 (mailto:{email or "info@bibexpy.org"})'
|
|
226
|
+
}
|
|
227
|
+
response = _get_with_retry(url, headers=headers)
|
|
228
|
+
|
|
229
|
+
if response.status_code == 200:
|
|
230
|
+
data = response.json()
|
|
231
|
+
work = data['message']
|
|
232
|
+
|
|
233
|
+
# Extract relevant fields
|
|
234
|
+
metadata = {}
|
|
235
|
+
|
|
236
|
+
# DOI
|
|
237
|
+
metadata['DI'] = work.get('DOI', '')
|
|
238
|
+
|
|
239
|
+
# Document Type
|
|
240
|
+
if 'type' in work:
|
|
241
|
+
metadata['DT'] = work['type']
|
|
242
|
+
|
|
243
|
+
# Authors (+ ORCID)
|
|
244
|
+
if 'author' in work:
|
|
245
|
+
authors = [f"{a.get('given', '')} {a.get('family', '')}".strip() for a in work['author']]
|
|
246
|
+
metadata['AU'] = '; '.join(authors)
|
|
247
|
+
# ORCID (OI) — CrossRef'te çoğu zaman boş ama varsa kesin sinyal.
|
|
248
|
+
orcids = []
|
|
249
|
+
for a in work['author']:
|
|
250
|
+
oid = str(a.get('ORCID') or "").replace("https://orcid.org/", "").replace("http://orcid.org/", "").strip()
|
|
251
|
+
if oid:
|
|
252
|
+
nm = f"{a.get('given', '')} {a.get('family', '')}".strip()
|
|
253
|
+
orcids.append(f"{nm}/{oid}")
|
|
254
|
+
if orcids:
|
|
255
|
+
metadata['OI'] = '; '.join(orcids)
|
|
256
|
+
|
|
257
|
+
# Title
|
|
258
|
+
if 'title' in work:
|
|
259
|
+
metadata['TI'] = work['title'][0]
|
|
260
|
+
|
|
261
|
+
# Publication Year
|
|
262
|
+
if 'issued' in work and 'date-parts' in work['issued']:
|
|
263
|
+
metadata['PY'] = work['issued']['date-parts'][0][0]
|
|
264
|
+
|
|
265
|
+
# Journal/Container Title
|
|
266
|
+
if 'container-title' in work:
|
|
267
|
+
metadata['SO'] = work['container-title'][0]
|
|
268
|
+
|
|
269
|
+
# Publisher
|
|
270
|
+
if 'publisher' in work:
|
|
271
|
+
metadata['PU'] = work['publisher']
|
|
272
|
+
|
|
273
|
+
# Publisher Address/Location
|
|
274
|
+
if 'publisher-location' in work:
|
|
275
|
+
metadata['PA'] = work['publisher-location']
|
|
276
|
+
|
|
277
|
+
# Abstract
|
|
278
|
+
if 'abstract' in work:
|
|
279
|
+
metadata['AB'] = work['abstract']
|
|
280
|
+
|
|
281
|
+
# ISSN
|
|
282
|
+
if 'ISSN' in work:
|
|
283
|
+
metadata['SN'] = '; '.join(work['ISSN'])
|
|
284
|
+
|
|
285
|
+
# URL
|
|
286
|
+
if 'URL' in work:
|
|
287
|
+
metadata['UR'] = work['URL']
|
|
288
|
+
|
|
289
|
+
# Subject -> yalnız anahtar kelime (DE). WC/SC gerçek WoS/Scopus
|
|
290
|
+
# kategorileri DEĞİLDİR; onları ML enrichment doldurur (de-collapse).
|
|
291
|
+
if 'subject' in work and work['subject']:
|
|
292
|
+
metadata['DE'] = '; '.join(work['subject'])
|
|
293
|
+
|
|
294
|
+
# License
|
|
295
|
+
if 'license' in work and work['license']:
|
|
296
|
+
license_urls = [lic.get('URL', '') for lic in work['license']]
|
|
297
|
+
metadata['LI'] = '; '.join(filter(None, license_urls))
|
|
298
|
+
|
|
299
|
+
return metadata
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(f"CrossRef API Error: {str(e)}")
|
|
302
|
+
return {}
|
|
303
|
+
|
|
304
|
+
def truncate_url(url: str, max_length: int = 2079) -> str:
|
|
305
|
+
"""Excel'in URL karakter limitine uygun olarak URL'yi kısalt"""
|
|
306
|
+
if not url or len(url) <= max_length:
|
|
307
|
+
return url
|
|
308
|
+
return url[:max_length-3] + "..."
|
|
309
|
+
|
|
310
|
+
@lru_cache(maxsize=4096)
|
|
311
|
+
def extract_metadata_from_openalex(doi: str, email: str = None) -> dict:
|
|
312
|
+
"""Extract metadata from OpenAlex API (Free, no key required)"""
|
|
313
|
+
try:
|
|
314
|
+
url = f"https://api.openalex.org/works/doi:{doi}"
|
|
315
|
+
if email:
|
|
316
|
+
url += f"?mailto={email}"
|
|
317
|
+
response = _get_with_retry(url)
|
|
318
|
+
|
|
319
|
+
if response.status_code == 200:
|
|
320
|
+
work = response.json()
|
|
321
|
+
|
|
322
|
+
# Extract relevant fields
|
|
323
|
+
metadata = {}
|
|
324
|
+
|
|
325
|
+
# DOI
|
|
326
|
+
if 'doi' in work:
|
|
327
|
+
metadata['DI'] = work['doi']
|
|
328
|
+
|
|
329
|
+
# Document Type
|
|
330
|
+
if 'type' in work:
|
|
331
|
+
metadata['DT'] = work['type']
|
|
332
|
+
|
|
333
|
+
# Title
|
|
334
|
+
if 'title' in work:
|
|
335
|
+
metadata['TI'] = work['title']
|
|
336
|
+
elif 'display_name' in work:
|
|
337
|
+
metadata['TI'] = work['display_name']
|
|
338
|
+
|
|
339
|
+
# Authors and Institutions
|
|
340
|
+
if 'authorships' in work:
|
|
341
|
+
# Authors
|
|
342
|
+
authors = []
|
|
343
|
+
institutions = []
|
|
344
|
+
author_institutions = [] # For author-institution pairs
|
|
345
|
+
orcids = [] # WoS OI: 'Author/0000-0000-0000-0000'
|
|
346
|
+
researcher_ids = [] # WoS RI: 'Author/<OpenAlex A-ID>' (yedek kimlik)
|
|
347
|
+
rors = [] # benzersiz ROR kurum URI'leri
|
|
348
|
+
countries = [] # ISO ülke kodları (yazar kurumlarından)
|
|
349
|
+
|
|
350
|
+
for authorship in work['authorships']:
|
|
351
|
+
if 'author' in authorship and 'display_name' in authorship['author']:
|
|
352
|
+
author = authorship['author']
|
|
353
|
+
author_name = author['display_name']
|
|
354
|
+
authors.append(author_name)
|
|
355
|
+
|
|
356
|
+
# ORCID (OI) — disambiguation'ın altın standardı
|
|
357
|
+
orcid = author.get('orcid')
|
|
358
|
+
if orcid:
|
|
359
|
+
oid = str(orcid).replace("https://orcid.org/", "").strip()
|
|
360
|
+
if oid:
|
|
361
|
+
orcids.append(f"{author_name}/{oid}")
|
|
362
|
+
# Yazar kimliği (RI yedek) — OpenAlex A-ID
|
|
363
|
+
aid = author.get('id')
|
|
364
|
+
if aid:
|
|
365
|
+
researcher_ids.append(f"{author_name}/{str(aid).replace('https://openalex.org/', '').strip()}")
|
|
366
|
+
|
|
367
|
+
# Get institutions for this author (+ ROR + country)
|
|
368
|
+
if 'institutions' in authorship:
|
|
369
|
+
author_insts = []
|
|
370
|
+
for inst in authorship['institutions']:
|
|
371
|
+
if 'display_name' in inst:
|
|
372
|
+
inst_name = inst['display_name']
|
|
373
|
+
institutions.append(inst_name)
|
|
374
|
+
author_insts.append(inst_name)
|
|
375
|
+
if inst.get('ror'):
|
|
376
|
+
rors.append(str(inst['ror']).replace("https://ror.org/", "").strip())
|
|
377
|
+
if inst.get('country_code'):
|
|
378
|
+
countries.append(str(inst['country_code']).strip().upper())
|
|
379
|
+
|
|
380
|
+
if author_insts:
|
|
381
|
+
author_institutions.append(f"{author_name} [{'; '.join(author_insts)}]")
|
|
382
|
+
# Yazar-seviyesi countries[] (kurum yoksa da gelebilir)
|
|
383
|
+
for cc in authorship.get('countries', []) or []:
|
|
384
|
+
if cc:
|
|
385
|
+
countries.append(str(cc).strip().upper())
|
|
386
|
+
|
|
387
|
+
if authors:
|
|
388
|
+
metadata['AU'] = '; '.join(authors)
|
|
389
|
+
# Unique institutions
|
|
390
|
+
if institutions:
|
|
391
|
+
metadata['C1'] = '; '.join(list(dict.fromkeys(institutions)))
|
|
392
|
+
# Author-Institution pairs
|
|
393
|
+
if author_institutions:
|
|
394
|
+
metadata['AF'] = '; '.join(author_institutions)
|
|
395
|
+
# ORCID (OI) + ResearcherID (RI) + ROR + ülke kodları
|
|
396
|
+
if orcids:
|
|
397
|
+
metadata['OI'] = '; '.join(orcids)
|
|
398
|
+
if researcher_ids:
|
|
399
|
+
metadata['RI'] = '; '.join(researcher_ids)
|
|
400
|
+
if rors:
|
|
401
|
+
metadata['ROR'] = '; '.join(dict.fromkeys(rors))
|
|
402
|
+
if countries:
|
|
403
|
+
metadata['CC'] = '; '.join(dict.fromkeys(countries))
|
|
404
|
+
|
|
405
|
+
# Publication Year
|
|
406
|
+
if 'publication_year' in work:
|
|
407
|
+
metadata['PY'] = work['publication_year']
|
|
408
|
+
|
|
409
|
+
# Journal/Publisher — yeni şema primary_location.source (host_venue deprecated).
|
|
410
|
+
venue = {}
|
|
411
|
+
ploc = work.get('primary_location') or {}
|
|
412
|
+
if isinstance(ploc, dict) and isinstance(ploc.get('source'), dict):
|
|
413
|
+
venue = ploc['source']
|
|
414
|
+
elif isinstance(work.get('host_venue'), dict):
|
|
415
|
+
venue = work['host_venue']
|
|
416
|
+
if venue:
|
|
417
|
+
if venue.get('display_name'):
|
|
418
|
+
metadata['SO'] = venue['display_name']
|
|
419
|
+
if venue.get('host_organization_name') or venue.get('publisher'):
|
|
420
|
+
metadata['PU'] = venue.get('host_organization_name') or venue.get('publisher')
|
|
421
|
+
if venue.get('issn_l'):
|
|
422
|
+
metadata['SN'] = venue['issn_l']
|
|
423
|
+
if isinstance(ploc, dict) and ploc.get('landing_page_url'):
|
|
424
|
+
metadata['UR'] = truncate_url(ploc['landing_page_url'])
|
|
425
|
+
|
|
426
|
+
# Abstract — OpenAlex 'abstract_inverted_index' verir; düz 'abstract' yok.
|
|
427
|
+
if work.get('abstract'):
|
|
428
|
+
metadata['AB'] = work['abstract']
|
|
429
|
+
elif work.get('abstract_inverted_index'):
|
|
430
|
+
ab = _decode_inverted_abstract(work['abstract_inverted_index'])
|
|
431
|
+
if ab:
|
|
432
|
+
metadata['AB'] = ab
|
|
433
|
+
|
|
434
|
+
# Concepts -> DE (anahtar kelime).
|
|
435
|
+
if 'concepts' in work:
|
|
436
|
+
concepts = [c['display_name'] for c in work['concepts'] if 'display_name' in c]
|
|
437
|
+
if concepts:
|
|
438
|
+
metadata['DE'] = '; '.join(concepts)
|
|
439
|
+
|
|
440
|
+
# Topics -> WC (subfield, daha özel) + SC (field, daha geniş). OpenAlex konu
|
|
441
|
+
# sınıflandırması; WoS taksonomisiyle birebir değil ama açık/şeffaf. WC ve SC
|
|
442
|
+
# ikisi de boş olan kayıtlarda devreye girer (first-non-empty-wins).
|
|
443
|
+
topics = work.get('topics') or ([work['primary_topic']] if work.get('primary_topic') else [])
|
|
444
|
+
if topics:
|
|
445
|
+
subfields, fields = [], []
|
|
446
|
+
for tp in topics:
|
|
447
|
+
if not isinstance(tp, dict):
|
|
448
|
+
continue
|
|
449
|
+
sf = (tp.get('subfield') or {}).get('display_name')
|
|
450
|
+
fd = (tp.get('field') or {}).get('display_name')
|
|
451
|
+
if sf:
|
|
452
|
+
subfields.append(sf)
|
|
453
|
+
if fd:
|
|
454
|
+
fields.append(fd)
|
|
455
|
+
if subfields:
|
|
456
|
+
metadata['WC'] = '; '.join(dict.fromkeys(subfields))
|
|
457
|
+
if fields:
|
|
458
|
+
metadata['SC'] = '; '.join(dict.fromkeys(fields))
|
|
459
|
+
|
|
460
|
+
# Citations
|
|
461
|
+
if 'cited_by_count' in work:
|
|
462
|
+
metadata['TC'] = work['cited_by_count']
|
|
463
|
+
|
|
464
|
+
# Open Access Information
|
|
465
|
+
if 'open_access' in work:
|
|
466
|
+
oa_info = work['open_access']
|
|
467
|
+
oa_details = []
|
|
468
|
+
if 'is_oa' in oa_info:
|
|
469
|
+
oa_details.append(f"is_oa: {oa_info['is_oa']}")
|
|
470
|
+
if 'oa_status' in oa_info:
|
|
471
|
+
oa_details.append(f"status: {oa_info['oa_status']}")
|
|
472
|
+
if 'oa_url' in oa_info:
|
|
473
|
+
oa_details.append(f"url: {oa_info['oa_url']}")
|
|
474
|
+
if oa_details:
|
|
475
|
+
metadata['OA'] = '; '.join(oa_details)
|
|
476
|
+
|
|
477
|
+
# Referenced Works — tümünü birleştir (Excel limiti için kısalt)
|
|
478
|
+
if work.get('referenced_works'):
|
|
479
|
+
metadata['CR'] = truncate_url_list('; '.join(work['referenced_works']))
|
|
480
|
+
|
|
481
|
+
return metadata
|
|
482
|
+
except Exception as e:
|
|
483
|
+
print(f"OpenAlex API Error: {str(e)}")
|
|
484
|
+
return {}
|
|
485
|
+
|
|
486
|
+
@lru_cache(maxsize=4096)
|
|
487
|
+
def extract_metadata_from_scopus(doi: str, api_key: str) -> dict:
|
|
488
|
+
"""Extract metadata from Scopus API (Requires API key)"""
|
|
489
|
+
try:
|
|
490
|
+
url = f"https://api.elsevier.com/content/abstract/doi/{doi}"
|
|
491
|
+
headers = {
|
|
492
|
+
'X-ELS-APIKey': api_key,
|
|
493
|
+
'Accept': 'application/json'
|
|
494
|
+
}
|
|
495
|
+
response = _get_with_retry(url, headers=headers)
|
|
496
|
+
|
|
497
|
+
if response.status_code == 200:
|
|
498
|
+
data = response.json()
|
|
499
|
+
if 'abstracts-retrieval-response' in data:
|
|
500
|
+
work = data['abstracts-retrieval-response']
|
|
501
|
+
metadata = {}
|
|
502
|
+
|
|
503
|
+
# Coredata'yı kontrol et
|
|
504
|
+
if 'coredata' in work:
|
|
505
|
+
coredata = work['coredata']
|
|
506
|
+
|
|
507
|
+
# DOI
|
|
508
|
+
if 'prism:doi' in coredata:
|
|
509
|
+
metadata['DI'] = coredata['prism:doi']
|
|
510
|
+
elif 'dc:identifier' in coredata:
|
|
511
|
+
metadata['DI'] = coredata['dc:identifier']
|
|
512
|
+
|
|
513
|
+
# Document Type
|
|
514
|
+
if 'prism:aggregationType' in coredata:
|
|
515
|
+
metadata['DT'] = coredata['prism:aggregationType']
|
|
516
|
+
|
|
517
|
+
# Title
|
|
518
|
+
if 'dc:title' in coredata:
|
|
519
|
+
metadata['TI'] = coredata['dc:title']
|
|
520
|
+
|
|
521
|
+
# Journal Name
|
|
522
|
+
if 'prism:publicationName' in coredata:
|
|
523
|
+
metadata['SO'] = coredata['prism:publicationName']
|
|
524
|
+
|
|
525
|
+
# Publisher
|
|
526
|
+
if 'dc:publisher' in coredata:
|
|
527
|
+
metadata['PU'] = coredata['dc:publisher']
|
|
528
|
+
|
|
529
|
+
# Volume
|
|
530
|
+
if 'prism:volume' in coredata:
|
|
531
|
+
metadata['VL'] = coredata['prism:volume']
|
|
532
|
+
|
|
533
|
+
# Issue
|
|
534
|
+
if 'prism:issueIdentifier' in coredata:
|
|
535
|
+
metadata['IS'] = coredata['prism:issueIdentifier']
|
|
536
|
+
|
|
537
|
+
# Page Numbers
|
|
538
|
+
if 'prism:startingPage' in coredata:
|
|
539
|
+
metadata['BP'] = coredata['prism:startingPage']
|
|
540
|
+
if 'prism:endingPage' in coredata:
|
|
541
|
+
metadata['EP'] = coredata['prism:endingPage']
|
|
542
|
+
|
|
543
|
+
# Publication Year
|
|
544
|
+
if 'prism:coverDate' in coredata:
|
|
545
|
+
metadata['PY'] = coredata['prism:coverDate'][:4]
|
|
546
|
+
|
|
547
|
+
# ISSN
|
|
548
|
+
if 'prism:issn' in coredata:
|
|
549
|
+
metadata['SN'] = coredata['prism:issn']
|
|
550
|
+
|
|
551
|
+
# Abstract
|
|
552
|
+
if 'dc:description' in coredata:
|
|
553
|
+
metadata['AB'] = coredata['dc:description']
|
|
554
|
+
|
|
555
|
+
# URLs
|
|
556
|
+
if 'link' in coredata:
|
|
557
|
+
urls = []
|
|
558
|
+
for link in coredata['link']:
|
|
559
|
+
if '@href' in link:
|
|
560
|
+
urls.append(link['@href'])
|
|
561
|
+
if urls:
|
|
562
|
+
metadata['UR'] = '; '.join(urls)
|
|
563
|
+
|
|
564
|
+
# Citation Count
|
|
565
|
+
if 'citedby-count' in coredata:
|
|
566
|
+
metadata['TC'] = coredata['citedby-count']
|
|
567
|
+
|
|
568
|
+
# Authors ve Affiliations
|
|
569
|
+
if 'authors' in work:
|
|
570
|
+
authors = []
|
|
571
|
+
affiliations = []
|
|
572
|
+
author_affiliations = []
|
|
573
|
+
orcids = [] # WoS OI
|
|
574
|
+
|
|
575
|
+
if 'author' in work['authors']:
|
|
576
|
+
for author in work['authors']['author']:
|
|
577
|
+
author_name = []
|
|
578
|
+
if 'ce:given-name' in author:
|
|
579
|
+
author_name.append(author['ce:given-name'])
|
|
580
|
+
if 'ce:surname' in author:
|
|
581
|
+
author_name.append(author['ce:surname'])
|
|
582
|
+
|
|
583
|
+
if author_name:
|
|
584
|
+
full_name = ' '.join(author_name)
|
|
585
|
+
authors.append(full_name)
|
|
586
|
+
# ORCID (Scopus '@orcid')
|
|
587
|
+
oid = str(author.get('@orcid') or "").strip()
|
|
588
|
+
if oid:
|
|
589
|
+
orcids.append(f"{full_name}/{oid}")
|
|
590
|
+
|
|
591
|
+
# Yazar affiliations
|
|
592
|
+
if 'affiliation' in author:
|
|
593
|
+
author_affs = []
|
|
594
|
+
if isinstance(author['affiliation'], list):
|
|
595
|
+
for aff in author['affiliation']:
|
|
596
|
+
if 'affilname' in aff:
|
|
597
|
+
affiliations.append(aff['affilname'])
|
|
598
|
+
author_affs.append(aff['affilname'])
|
|
599
|
+
else:
|
|
600
|
+
aff = author['affiliation']
|
|
601
|
+
if 'affilname' in aff:
|
|
602
|
+
affiliations.append(aff['affilname'])
|
|
603
|
+
author_affs.append(aff['affilname'])
|
|
604
|
+
|
|
605
|
+
if author_affs:
|
|
606
|
+
author_affiliations.append(f"{full_name} [{'; '.join(author_affs)}]")
|
|
607
|
+
|
|
608
|
+
if authors:
|
|
609
|
+
metadata['AU'] = '; '.join(authors)
|
|
610
|
+
if affiliations:
|
|
611
|
+
metadata['C1'] = '; '.join(list(dict.fromkeys(affiliations)))
|
|
612
|
+
if author_affiliations:
|
|
613
|
+
metadata['AF'] = '; '.join(author_affiliations)
|
|
614
|
+
if orcids:
|
|
615
|
+
metadata['OI'] = '; '.join(orcids)
|
|
616
|
+
|
|
617
|
+
# Subject Areas (ASJC) -> DE + WC/SC. Yalnız Scopus key aktifse buraya gelinir.
|
|
618
|
+
# NOT: tek alan olduğunda Elsevier 'subject-area'yı liste yerine TEK dict döndürür.
|
|
619
|
+
sa = (work.get('subject-areas') or {}).get('subject-area')
|
|
620
|
+
if isinstance(sa, dict):
|
|
621
|
+
sa = [sa]
|
|
622
|
+
if isinstance(sa, list):
|
|
623
|
+
subjects = [s['$'] for s in sa if isinstance(s, dict) and '$' in s]
|
|
624
|
+
if subjects:
|
|
625
|
+
uniq = '; '.join(dict.fromkeys(subjects))
|
|
626
|
+
metadata['DE'] = uniq
|
|
627
|
+
metadata['WC'] = uniq
|
|
628
|
+
metadata['SC'] = uniq
|
|
629
|
+
|
|
630
|
+
return metadata
|
|
631
|
+
except Exception as e:
|
|
632
|
+
print(f"Scopus API Error: {str(e)}")
|
|
633
|
+
return {}
|
|
634
|
+
|
|
635
|
+
@lru_cache(maxsize=4096)
|
|
636
|
+
def extract_metadata_from_datacite(doi: str) -> dict:
|
|
637
|
+
"""Extract metadata from DataCite API (Free, no key required)"""
|
|
638
|
+
try:
|
|
639
|
+
url = f"https://api.datacite.org/dois/{doi}"
|
|
640
|
+
headers = {
|
|
641
|
+
'Accept': 'application/vnd.api+json'
|
|
642
|
+
}
|
|
643
|
+
response = _get_with_retry(url, headers=headers)
|
|
644
|
+
|
|
645
|
+
if response.status_code == 200:
|
|
646
|
+
data = response.json()
|
|
647
|
+
if 'data' in data and 'attributes' in data['data']:
|
|
648
|
+
work = data['data']['attributes']
|
|
649
|
+
|
|
650
|
+
# Extract relevant fields
|
|
651
|
+
metadata = {}
|
|
652
|
+
|
|
653
|
+
# Authors
|
|
654
|
+
if 'creators' in work:
|
|
655
|
+
authors = []
|
|
656
|
+
for creator in work['creators']:
|
|
657
|
+
if isinstance(creator, dict):
|
|
658
|
+
name_parts = []
|
|
659
|
+
if 'givenName' in creator:
|
|
660
|
+
name_parts.append(creator['givenName'])
|
|
661
|
+
if 'familyName' in creator:
|
|
662
|
+
name_parts.append(creator['familyName'])
|
|
663
|
+
if name_parts:
|
|
664
|
+
authors.append(' '.join(name_parts))
|
|
665
|
+
elif 'name' in creator:
|
|
666
|
+
authors.append(creator['name'])
|
|
667
|
+
if authors:
|
|
668
|
+
metadata['AU'] = '; '.join(authors)
|
|
669
|
+
|
|
670
|
+
# Title
|
|
671
|
+
if 'titles' in work and work['titles']:
|
|
672
|
+
metadata['TI'] = work['titles'][0].get('title', '')
|
|
673
|
+
|
|
674
|
+
# Publication Year
|
|
675
|
+
if 'publicationYear' in work:
|
|
676
|
+
metadata['PY'] = work['publicationYear']
|
|
677
|
+
|
|
678
|
+
# Document Type
|
|
679
|
+
if 'types' in work and 'resourceTypeGeneral' in work['types']:
|
|
680
|
+
metadata['DT'] = work['types']['resourceTypeGeneral']
|
|
681
|
+
|
|
682
|
+
# Keywords, Science Categories ve Web of Science Categories
|
|
683
|
+
if 'subjects' in work and work['subjects']:
|
|
684
|
+
all_subjects = [subj.get('subject', '') for subj in work['subjects'] if 'subject' in subj]
|
|
685
|
+
if all_subjects:
|
|
686
|
+
metadata['DE'] = '; '.join(all_subjects) # yalnız DE; WC/SC ML'den
|
|
687
|
+
|
|
688
|
+
# Language
|
|
689
|
+
if 'language' in work:
|
|
690
|
+
metadata['LA'] = work['language']
|
|
691
|
+
|
|
692
|
+
# Publisher/Journal
|
|
693
|
+
if 'publisher' in work:
|
|
694
|
+
metadata['SO'] = work['publisher']
|
|
695
|
+
|
|
696
|
+
# Abstract
|
|
697
|
+
if 'descriptions' in work:
|
|
698
|
+
abstracts = [desc.get('description', '') for desc in work['descriptions']
|
|
699
|
+
if desc.get('descriptionType', '').lower() == 'abstract']
|
|
700
|
+
if abstracts:
|
|
701
|
+
metadata['AB'] = abstracts[0]
|
|
702
|
+
|
|
703
|
+
# Contributors (including affiliations)
|
|
704
|
+
if 'contributors' in work:
|
|
705
|
+
affiliations = []
|
|
706
|
+
for contrib in work['contributors']:
|
|
707
|
+
if isinstance(contrib, dict) and 'affiliation' in contrib:
|
|
708
|
+
if isinstance(contrib['affiliation'], list):
|
|
709
|
+
affiliations.extend([aff.get('name', '') for aff in contrib['affiliation'] if 'name' in aff])
|
|
710
|
+
elif isinstance(contrib['affiliation'], str):
|
|
711
|
+
affiliations.append(contrib['affiliation'])
|
|
712
|
+
if affiliations:
|
|
713
|
+
metadata['C1'] = '; '.join(set(affiliations)) # Remove duplicates
|
|
714
|
+
|
|
715
|
+
return metadata
|
|
716
|
+
except Exception as e:
|
|
717
|
+
print(f"DataCite API Error: {str(e)}")
|
|
718
|
+
return {}
|
|
719
|
+
|
|
720
|
+
@lru_cache(maxsize=4096)
|
|
721
|
+
def extract_metadata_from_unpaywall(doi: str, email: str) -> dict:
|
|
722
|
+
"""Extract metadata from Unpaywall API (Free, requires email)"""
|
|
723
|
+
try:
|
|
724
|
+
url = f"https://api.unpaywall.org/v2/{doi}?email={email}"
|
|
725
|
+
response = _get_with_retry(url)
|
|
726
|
+
|
|
727
|
+
if response.status_code == 200:
|
|
728
|
+
work = response.json()
|
|
729
|
+
metadata = {}
|
|
730
|
+
|
|
731
|
+
# Authors
|
|
732
|
+
if 'z_authors' in work:
|
|
733
|
+
authors = [f"{a.get('given', '')} {a.get('family', '')}" for a in work['z_authors']]
|
|
734
|
+
metadata['AU'] = '; '.join(authors)
|
|
735
|
+
|
|
736
|
+
# Title
|
|
737
|
+
if 'title' in work:
|
|
738
|
+
metadata['TI'] = work['title']
|
|
739
|
+
|
|
740
|
+
# Journal
|
|
741
|
+
if 'journal_name' in work:
|
|
742
|
+
metadata['SO'] = work['journal_name']
|
|
743
|
+
|
|
744
|
+
# Publication Year
|
|
745
|
+
if 'published_date' in work:
|
|
746
|
+
try:
|
|
747
|
+
metadata['PY'] = work['published_date'][:4]
|
|
748
|
+
except:
|
|
749
|
+
pass
|
|
750
|
+
|
|
751
|
+
# Document Type
|
|
752
|
+
if 'genre' in work:
|
|
753
|
+
metadata['DT'] = work['genre']
|
|
754
|
+
|
|
755
|
+
return metadata
|
|
756
|
+
except Exception as e:
|
|
757
|
+
print(f"Unpaywall API Error: {str(e)}")
|
|
758
|
+
return {}
|
|
759
|
+
|
|
760
|
+
@lru_cache(maxsize=4096)
|
|
761
|
+
def extract_metadata_from_europepmc(doi: str) -> dict:
|
|
762
|
+
"""Extract metadata from Europe PMC API (Free)"""
|
|
763
|
+
try:
|
|
764
|
+
url = f"https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=DOI:{doi}&format=json"
|
|
765
|
+
response = _get_with_retry(url)
|
|
766
|
+
|
|
767
|
+
if response.status_code == 200:
|
|
768
|
+
data = response.json()
|
|
769
|
+
if 'resultList' in data and 'result' in data['resultList'] and data['resultList']['result']:
|
|
770
|
+
work = data['resultList']['result'][0]
|
|
771
|
+
metadata = {}
|
|
772
|
+
|
|
773
|
+
# DOI
|
|
774
|
+
if 'doi' in work:
|
|
775
|
+
metadata['DI'] = work['doi']
|
|
776
|
+
|
|
777
|
+
# Document Type
|
|
778
|
+
if 'pubType' in work:
|
|
779
|
+
metadata['DT'] = work['pubType']
|
|
780
|
+
|
|
781
|
+
# Title
|
|
782
|
+
if 'title' in work:
|
|
783
|
+
metadata['TI'] = work['title']
|
|
784
|
+
|
|
785
|
+
# Authors
|
|
786
|
+
if 'authorString' in work:
|
|
787
|
+
metadata['AU'] = work['authorString']
|
|
788
|
+
|
|
789
|
+
# Journal Title
|
|
790
|
+
if 'journalTitle' in work:
|
|
791
|
+
metadata['SO'] = work['journalTitle']
|
|
792
|
+
|
|
793
|
+
# Volume
|
|
794
|
+
if 'journalVolume' in work:
|
|
795
|
+
metadata['VL'] = work['journalVolume']
|
|
796
|
+
|
|
797
|
+
# Issue
|
|
798
|
+
if 'journalIssue' in work:
|
|
799
|
+
metadata['IS'] = work['journalIssue']
|
|
800
|
+
|
|
801
|
+
# Page Information
|
|
802
|
+
if 'pageInfo' in work:
|
|
803
|
+
pages = work['pageInfo'].split('-')
|
|
804
|
+
if len(pages) == 2:
|
|
805
|
+
metadata['BP'] = pages[0].strip() # Beginning Page
|
|
806
|
+
metadata['EP'] = pages[1].strip() # Ending Page
|
|
807
|
+
|
|
808
|
+
# Publication Year
|
|
809
|
+
if 'pubYear' in work:
|
|
810
|
+
metadata['PY'] = work['pubYear']
|
|
811
|
+
|
|
812
|
+
# ISSN
|
|
813
|
+
if 'journalIssn' in work:
|
|
814
|
+
metadata['SN'] = work['journalIssn']
|
|
815
|
+
|
|
816
|
+
# Abstract
|
|
817
|
+
if 'abstractText' in work:
|
|
818
|
+
metadata['AB'] = work['abstractText']
|
|
819
|
+
|
|
820
|
+
# URLs
|
|
821
|
+
urls = []
|
|
822
|
+
if 'sourceUrl' in work:
|
|
823
|
+
urls.append(work['sourceUrl'])
|
|
824
|
+
if 'fullTextUrlList' in work and 'fullTextUrl' in work['fullTextUrlList']:
|
|
825
|
+
for url_info in work['fullTextUrlList']['fullTextUrl']:
|
|
826
|
+
if 'url' in url_info:
|
|
827
|
+
urls.append(url_info['url'])
|
|
828
|
+
if urls:
|
|
829
|
+
metadata['UR'] = '; '.join(urls)
|
|
830
|
+
|
|
831
|
+
# Citations
|
|
832
|
+
if 'citedByCount' in work:
|
|
833
|
+
metadata['TC'] = work['citedByCount']
|
|
834
|
+
|
|
835
|
+
# Open Access Information
|
|
836
|
+
if 'isOpenAccess' in work:
|
|
837
|
+
oa_info = []
|
|
838
|
+
oa_info.append(f"is_oa: {work['isOpenAccess']}")
|
|
839
|
+
if 'fullTextUrlList' in work and 'fullTextUrl' in work['fullTextUrlList']:
|
|
840
|
+
for url_info in work['fullTextUrlList']['fullTextUrl']:
|
|
841
|
+
if 'availability' in url_info:
|
|
842
|
+
oa_info.append(f"availability: {url_info['availability']}")
|
|
843
|
+
if 'availabilityCode' in url_info:
|
|
844
|
+
oa_info.append(f"status: {url_info['availabilityCode']}")
|
|
845
|
+
if oa_info:
|
|
846
|
+
metadata['OA'] = '; '.join(oa_info)
|
|
847
|
+
|
|
848
|
+
# Additional Information
|
|
849
|
+
additional_info = []
|
|
850
|
+
if 'inEPMC' in work:
|
|
851
|
+
additional_info.append(f"in_epmc: {work['inEPMC']}")
|
|
852
|
+
if 'inPMC' in work:
|
|
853
|
+
additional_info.append(f"in_pmc: {work['inPMC']}")
|
|
854
|
+
if 'hasPDF' in work:
|
|
855
|
+
additional_info.append(f"has_pdf: {work['hasPDF']}")
|
|
856
|
+
if 'hasReferences' in work:
|
|
857
|
+
additional_info.append(f"has_references: {work['hasReferences']}")
|
|
858
|
+
if 'hasTextMinedTerms' in work:
|
|
859
|
+
additional_info.append(f"has_text_mined_terms: {work['hasTextMinedTerms']}")
|
|
860
|
+
if 'hasDbCrossReferences' in work:
|
|
861
|
+
additional_info.append(f"has_db_cross_references: {work['hasDbCrossReferences']}")
|
|
862
|
+
if additional_info:
|
|
863
|
+
metadata['AI'] = '; '.join(additional_info)
|
|
864
|
+
|
|
865
|
+
return metadata
|
|
866
|
+
except Exception as e:
|
|
867
|
+
print(f"Europe PMC API Error: {str(e)}")
|
|
868
|
+
return {}
|
|
869
|
+
|
|
870
|
+
@lru_cache(maxsize=4096)
|
|
871
|
+
def extract_metadata_from_semantic_scholar(doi: str, api_key: str = None) -> dict:
|
|
872
|
+
"""Extract metadata from Semantic Scholar API (Free, API key optional but recommended)"""
|
|
873
|
+
try:
|
|
874
|
+
url = f"https://api.semanticscholar.org/v1/paper/{doi}"
|
|
875
|
+
headers = {'x-api-key': api_key} if api_key else {}
|
|
876
|
+
response = _get_with_retry(url, headers=headers)
|
|
877
|
+
|
|
878
|
+
if response.status_code == 200:
|
|
879
|
+
work = response.json()
|
|
880
|
+
metadata = {}
|
|
881
|
+
|
|
882
|
+
# Paper ID
|
|
883
|
+
if 'paperId' in work:
|
|
884
|
+
metadata['ID'] = work['paperId']
|
|
885
|
+
|
|
886
|
+
# DOI
|
|
887
|
+
if 'externalIds' in work and 'DOI' in work['externalIds']:
|
|
888
|
+
metadata['DI'] = work['externalIds']['DOI']
|
|
889
|
+
|
|
890
|
+
# Title
|
|
891
|
+
if 'title' in work:
|
|
892
|
+
metadata['TI'] = work['title']
|
|
893
|
+
|
|
894
|
+
# Authors
|
|
895
|
+
if 'authors' in work:
|
|
896
|
+
authors = []
|
|
897
|
+
for author in work['authors']:
|
|
898
|
+
if 'name' in author:
|
|
899
|
+
authors.append(author['name'])
|
|
900
|
+
|
|
901
|
+
if authors:
|
|
902
|
+
metadata['AU'] = '; '.join(authors)
|
|
903
|
+
|
|
904
|
+
# Abstract
|
|
905
|
+
if 'abstract' in work:
|
|
906
|
+
metadata['AB'] = work['abstract']
|
|
907
|
+
|
|
908
|
+
# Publication Year
|
|
909
|
+
if 'year' in work:
|
|
910
|
+
metadata['PY'] = work['year']
|
|
911
|
+
|
|
912
|
+
# Citations
|
|
913
|
+
if 'citationCount' in work:
|
|
914
|
+
metadata['TC'] = work['citationCount']
|
|
915
|
+
|
|
916
|
+
# Reference Count
|
|
917
|
+
if 'referenceCount' in work:
|
|
918
|
+
metadata['RC'] = work['referenceCount']
|
|
919
|
+
|
|
920
|
+
# URL
|
|
921
|
+
if 'url' in work:
|
|
922
|
+
metadata['UR'] = work['url']
|
|
923
|
+
|
|
924
|
+
# Fields of Study -> yalnız DE; WC/SC ML'den
|
|
925
|
+
if 'fieldsOfStudy' in work and work['fieldsOfStudy']:
|
|
926
|
+
metadata['DE'] = '; '.join(work['fieldsOfStudy'])
|
|
927
|
+
|
|
928
|
+
# Additional Identifiers
|
|
929
|
+
if 'externalIds' in work:
|
|
930
|
+
external_ids = []
|
|
931
|
+
for id_type, id_value in work['externalIds'].items():
|
|
932
|
+
if id_type != 'DOI': # DOI already handled
|
|
933
|
+
external_ids.append(f"{id_type}: {id_value}")
|
|
934
|
+
if external_ids:
|
|
935
|
+
metadata['EI'] = '; '.join(external_ids)
|
|
936
|
+
|
|
937
|
+
return metadata
|
|
938
|
+
except Exception as e:
|
|
939
|
+
print(f"Semantic Scholar API Error: {str(e)}")
|
|
940
|
+
return {}
|
|
941
|
+
|
|
942
|
+
def truncate_url_list(url_list: str, max_length: int = 2079) -> str:
|
|
943
|
+
"""Excel'in URL uzunluk sınırına uygun şekilde URL listesini kısalt"""
|
|
944
|
+
# Eğer URL listesi boşsa veya zaten sınırın altındaysa direkt döndür
|
|
945
|
+
if not url_list or len(url_list) <= max_length:
|
|
946
|
+
return url_list
|
|
947
|
+
|
|
948
|
+
# URL'leri ayır
|
|
949
|
+
urls = url_list.split('; ')
|
|
950
|
+
result = []
|
|
951
|
+
current_length = 0
|
|
952
|
+
|
|
953
|
+
# Her URL için
|
|
954
|
+
for i, url in enumerate(urls):
|
|
955
|
+
# İlk URL için ayırıcı gerekmez, diğerleri için '; ' (2 karakter) gerekir
|
|
956
|
+
separator_length = 0 if not result else 2
|
|
957
|
+
|
|
958
|
+
# Bu URL'yi eklersek toplam uzunluk
|
|
959
|
+
new_length = current_length + len(url) + separator_length
|
|
960
|
+
|
|
961
|
+
# Eğer bu URL'yi eklemek sınırı aşmayacaksa
|
|
962
|
+
if new_length <= max_length - 3: # "..." için 3 karakter rezerve et
|
|
963
|
+
result.append(url)
|
|
964
|
+
current_length = new_length
|
|
965
|
+
else:
|
|
966
|
+
# Sınıra ulaşıldı, "..." ekle ve döngüyü kır
|
|
967
|
+
result.append("...")
|
|
968
|
+
break
|
|
969
|
+
|
|
970
|
+
# URL'leri birleştir
|
|
971
|
+
return '; '.join(result)
|
|
972
|
+
|
|
973
|
+
def extract_metadata(doi: str, current_data: dict, scopus_api_key: str = None, semantic_scholar_key: str = None, unpaywall_email: str = None, crossref_email: str = None) -> dict:
|
|
974
|
+
"""Try to extract metadata from multiple sources"""
|
|
975
|
+
metadata = current_data.copy()
|
|
976
|
+
api_sources = {} # Track which fields came from which API
|
|
977
|
+
|
|
978
|
+
# Erken çıkış için: API'lerin doldurabileceği alanlar. OI(ORCID)/RI/ROR/CC yeni
|
|
979
|
+
# eklendi (yazar/kurum kimliği — disambiguation için).
|
|
980
|
+
_FILLABLE = ("DI", "DT", "AU", "AF", "TI", "PY", "SO", "PU", "SN", "UR", "AB", "DE",
|
|
981
|
+
"C1", "TC", "CR", "LA", "WC", "SC", "OI", "RI", "ROR", "CC")
|
|
982
|
+
|
|
983
|
+
def _is_blank(v) -> bool:
|
|
984
|
+
if v is None:
|
|
985
|
+
return True
|
|
986
|
+
try:
|
|
987
|
+
if pd.isna(v):
|
|
988
|
+
return True
|
|
989
|
+
except (TypeError, ValueError):
|
|
990
|
+
pass
|
|
991
|
+
return str(v).strip() in ("", "nan", "NaN", "None")
|
|
992
|
+
|
|
993
|
+
def _remaining() -> bool:
|
|
994
|
+
return any(_is_blank(metadata.get(f)) for f in _FILLABLE)
|
|
995
|
+
|
|
996
|
+
try:
|
|
997
|
+
# Get API credentials from config file
|
|
998
|
+
if not scopus_api_key:
|
|
999
|
+
scopus_api_key = get_api_credential('scopus', 'api_key')
|
|
1000
|
+
if not semantic_scholar_key:
|
|
1001
|
+
semantic_scholar_key = get_api_credential('semantic_scholar', 'api_key')
|
|
1002
|
+
if not unpaywall_email:
|
|
1003
|
+
unpaywall_email = get_api_credential('unpaywall', 'email')
|
|
1004
|
+
if not crossref_email:
|
|
1005
|
+
crossref_email = get_api_credential('crossref', 'email')
|
|
1006
|
+
|
|
1007
|
+
if not scopus_api_key:
|
|
1008
|
+
print("\nWarning: Scopus API key not found in API_config.json")
|
|
1009
|
+
print("You can add it to enable Scopus metadata enrichment.")
|
|
1010
|
+
print("Continuing with other data sources...")
|
|
1011
|
+
|
|
1012
|
+
if not unpaywall_email:
|
|
1013
|
+
print("\nWarning: Unpaywall email not found in API_config.json")
|
|
1014
|
+
print("You can add it to enable Unpaywall metadata enrichment.")
|
|
1015
|
+
print("Continuing with other data sources...")
|
|
1016
|
+
|
|
1017
|
+
# CrossRef
|
|
1018
|
+
print(f"\nTrying CrossRef API...", end='')
|
|
1019
|
+
try:
|
|
1020
|
+
crossref_data = extract_metadata_from_crossref(doi, crossref_email)
|
|
1021
|
+
if crossref_data:
|
|
1022
|
+
for key, value in crossref_data.items():
|
|
1023
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1024
|
+
# URL içeren alanları kısalt
|
|
1025
|
+
if isinstance(value, str) and ('http://' in value or 'https://' in value):
|
|
1026
|
+
value = truncate_url_list(value)
|
|
1027
|
+
metadata[key] = value
|
|
1028
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1029
|
+
api_sources[key] = 'CrossRef'
|
|
1030
|
+
print(" [SUCCESS]")
|
|
1031
|
+
else:
|
|
1032
|
+
print(" [NO DATA]")
|
|
1033
|
+
except Exception as e:
|
|
1034
|
+
print(f" [ERROR: {str(e)}]")
|
|
1035
|
+
|
|
1036
|
+
# OpenAlex
|
|
1037
|
+
print(f"Trying OpenAlex API...", end='')
|
|
1038
|
+
try:
|
|
1039
|
+
openalex_data = extract_metadata_from_openalex(doi, crossref_email)
|
|
1040
|
+
if openalex_data:
|
|
1041
|
+
for key, value in openalex_data.items():
|
|
1042
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1043
|
+
# URL içeren alanları kısalt
|
|
1044
|
+
if isinstance(value, str) and ('http://' in value or 'https://' in value):
|
|
1045
|
+
value = truncate_url_list(value)
|
|
1046
|
+
metadata[key] = value
|
|
1047
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1048
|
+
api_sources[key] = 'OpenAlex'
|
|
1049
|
+
print(" [SUCCESS]")
|
|
1050
|
+
else:
|
|
1051
|
+
print(" [NO DATA]")
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
print(f" [ERROR: {str(e)}]")
|
|
1054
|
+
|
|
1055
|
+
# Erken çıkış: CrossRef + OpenAlex sonrası doldurulacak alan kalmadıysa
|
|
1056
|
+
# diğer 5 API'yi hiç deneme (en büyük hız kazancı).
|
|
1057
|
+
if not _remaining():
|
|
1058
|
+
for field in metadata:
|
|
1059
|
+
if isinstance(metadata[field], str) and ('http://' in metadata[field] or 'https://' in metadata[field]):
|
|
1060
|
+
metadata[field] = truncate_url_list(metadata[field])
|
|
1061
|
+
if api_sources:
|
|
1062
|
+
metadata['API_Sources'] = api_sources
|
|
1063
|
+
return metadata
|
|
1064
|
+
|
|
1065
|
+
# Scopus
|
|
1066
|
+
if scopus_api_key:
|
|
1067
|
+
print(f"Trying Scopus API...", end='')
|
|
1068
|
+
try:
|
|
1069
|
+
scopus_data = extract_metadata_from_scopus(doi, scopus_api_key)
|
|
1070
|
+
if scopus_data:
|
|
1071
|
+
for key, value in scopus_data.items():
|
|
1072
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1073
|
+
metadata[key] = value
|
|
1074
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1075
|
+
api_sources[key] = 'Scopus'
|
|
1076
|
+
print(" [SUCCESS]")
|
|
1077
|
+
else:
|
|
1078
|
+
response = requests.get(f"https://api.elsevier.com/content/abstract/doi/{doi}",
|
|
1079
|
+
headers={'X-ELS-APIKey': scopus_api_key, 'Accept': 'application/json'}, timeout=REQUEST_TIMEOUT)
|
|
1080
|
+
print(f" [NO DATA - Status: {response.status_code}, Response: {response.text[:100]}...]")
|
|
1081
|
+
except Exception as e:
|
|
1082
|
+
print(f" [ERROR: {str(e)}]")
|
|
1083
|
+
|
|
1084
|
+
# DataCite
|
|
1085
|
+
print(f"Trying DataCite API...", end='')
|
|
1086
|
+
try:
|
|
1087
|
+
response = requests.get(f"https://api.datacite.org/dois/{doi}", headers={'Accept': 'application/vnd.api+json'}, timeout=REQUEST_TIMEOUT)
|
|
1088
|
+
if response.status_code != 200:
|
|
1089
|
+
print(f" [NO DATA - Status: {response.status_code}]")
|
|
1090
|
+
else:
|
|
1091
|
+
datacite_data = extract_metadata_from_datacite(doi)
|
|
1092
|
+
if datacite_data:
|
|
1093
|
+
for key, value in datacite_data.items():
|
|
1094
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1095
|
+
metadata[key] = value
|
|
1096
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1097
|
+
api_sources[key] = 'DataCite'
|
|
1098
|
+
print(" [SUCCESS]")
|
|
1099
|
+
else:
|
|
1100
|
+
print(f" [NO DATA - Empty Response]")
|
|
1101
|
+
except Exception as e:
|
|
1102
|
+
print(f" [ERROR: {str(e)}]")
|
|
1103
|
+
|
|
1104
|
+
# Unpaywall
|
|
1105
|
+
if unpaywall_email:
|
|
1106
|
+
print(f"Trying Unpaywall API...", end='')
|
|
1107
|
+
try:
|
|
1108
|
+
unpaywall_data = extract_metadata_from_unpaywall(doi, unpaywall_email)
|
|
1109
|
+
if unpaywall_data:
|
|
1110
|
+
for key, value in unpaywall_data.items():
|
|
1111
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1112
|
+
metadata[key] = value
|
|
1113
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1114
|
+
api_sources[key] = 'Unpaywall'
|
|
1115
|
+
print(" [SUCCESS]")
|
|
1116
|
+
else:
|
|
1117
|
+
print(" [NO DATA]")
|
|
1118
|
+
except Exception as e:
|
|
1119
|
+
print(f" [ERROR: {str(e)}]")
|
|
1120
|
+
|
|
1121
|
+
# Europe PMC
|
|
1122
|
+
print(f"Trying Europe PMC API...", end='')
|
|
1123
|
+
try:
|
|
1124
|
+
response = requests.get(f"https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=DOI:{doi}&format=json", timeout=REQUEST_TIMEOUT)
|
|
1125
|
+
if response.status_code != 200:
|
|
1126
|
+
print(f" [NO DATA - Status: {response.status_code}]")
|
|
1127
|
+
else:
|
|
1128
|
+
europepmc_data = extract_metadata_from_europepmc(doi)
|
|
1129
|
+
if europepmc_data:
|
|
1130
|
+
for key, value in europepmc_data.items():
|
|
1131
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1132
|
+
metadata[key] = value
|
|
1133
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1134
|
+
api_sources[key] = 'Europe PMC'
|
|
1135
|
+
print(" [SUCCESS]")
|
|
1136
|
+
else:
|
|
1137
|
+
data = response.json()
|
|
1138
|
+
hit_count = data.get('hitCount', 0)
|
|
1139
|
+
print(f" [NO DATA - Hit Count: {hit_count}]")
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
print(f" [ERROR: {str(e)}]")
|
|
1142
|
+
|
|
1143
|
+
# Semantic Scholar
|
|
1144
|
+
print(f"Trying Semantic Scholar API...", end='')
|
|
1145
|
+
try:
|
|
1146
|
+
semantic_data = extract_metadata_from_semantic_scholar(doi, semantic_scholar_key)
|
|
1147
|
+
if semantic_data:
|
|
1148
|
+
for key, value in semantic_data.items():
|
|
1149
|
+
if pd.isna(metadata.get(key, None)) or str(metadata.get(key, '')).strip() == '':
|
|
1150
|
+
metadata[key] = value
|
|
1151
|
+
if pd.notna(value) and str(value).strip() != '':
|
|
1152
|
+
api_sources[key] = 'Semantic Scholar'
|
|
1153
|
+
print(" [SUCCESS]")
|
|
1154
|
+
else:
|
|
1155
|
+
print(" [NO DATA]")
|
|
1156
|
+
except Exception as e:
|
|
1157
|
+
print(f" [ERROR: {str(e)}]")
|
|
1158
|
+
|
|
1159
|
+
# API kaynaklarını ekle
|
|
1160
|
+
if api_sources:
|
|
1161
|
+
metadata['API_Sources'] = api_sources
|
|
1162
|
+
|
|
1163
|
+
# Son kontrol: Tüm URL içeren alanları kontrol et ve gerekirse kısalt
|
|
1164
|
+
for field in metadata:
|
|
1165
|
+
if isinstance(metadata[field], str) and ('http://' in metadata[field] or 'https://' in metadata[field]):
|
|
1166
|
+
metadata[field] = truncate_url_list(metadata[field])
|
|
1167
|
+
|
|
1168
|
+
return metadata
|
|
1169
|
+
|
|
1170
|
+
except Exception as e:
|
|
1171
|
+
print(f"Error processing DOI {doi}: {str(e)}")
|
|
1172
|
+
return current_data
|