Claude CodeでORCID APIの業績を取り込み、研究室サイトの一覧に整える
ORCID iDからPublic APIでworksを取得し、DOIの欠落・重複・種別の抜けをClaude Codeで洗って研究室サイトの業績一覧に整える手順です。スクリプトと設定例つきです。
研究室サイトの業績一覧は、論文が出るたびに手で足すと更新が止まりがちです。ORCIDに業績が載っているなら、Public APIで取得して一覧の下書きを自動で作れます。Claude Codeには、取得結果の「DOIがない」「同じ論文が二重に出ている」「種別や誌名が抜けている」を洗わせます。
この記事では、読み取り用の資格情報を作ってから、標準ライブラリだけのPythonスクリプトを1本置き、要確認フラグ付きの一覧を出すところまでを扱います。ORCIDの記録そのものを直すのは本人の作業なので、Claude Codeには「直す箇所の一覧」を作らせる形にします。
取得から一覧までの流れ
業績一覧ができるまでの4段階
- 1
読み取り用の資格情報を作る
ORCIDにサインインして開発者ツールからPublic APIの資格情報を登録します。
- 2
アクセストークンを取る
client credentialsのOAuthで、
/read-publicのトークンを取得します。 - 3
worksを取得して整える
/worksの応答をグループ単位で読み、代表の1件を選んで行にします。 - 4
要確認フラグを見て直す
Claude Codeがフラグ付きの行を一覧にし、直すのは記録の持ち主が行います。
資格情報とトークンを用意する
Public APIの読み取り用資格情報は、誰でも登録できます。ORCIDのレコードにサインインし、名前のメニューからDeveloper Toolsを開いて、Public APIの利用規約に同意してから登録します。Developer Toolsに入るにはメールアドレスの確認が済んでいる必要があります。
アプリケーション名・URL・説明を入力し、リダイレクトURIを1つ以上登録すると、クライアントIDとシークレットが発行されます。公開データを読むだけなら、利用者の許可を取る必要はありません。ORCID公式は、まずsandboxで試してから本番に移ることを勧めています。
トークンは、client credentialsの流れで取ります。公式の例はsandboxのURLで書かれていて、本番で使うときはホスト名を読み替えます。
curl -s -X POST https://sandbox.orcid.org/oauth/token \
-H "Accept: application/json" \
-d client_id="$ORCID_CLIENT_ID" \
-d client_secret="$ORCID_CLIENT_SECRET" \
-d grant_type=client_credentials \
-d scope=/read-public返ってくる access_token は、有効期間が約20年と長く、何度も使えます。長命なトークンは、漏れたときの影響が長く残ります。環境変数に入れて、リポジトリには置かない運用にします。
worksの応答は「グループ」で返る
業績は https://pub.orcid.org/v3.0/<ORCID iD>/works で取ります。Public APIのホストは pub.orcid.org で、Member APIの api.orcid.org とは別です。Accept: application/json を付けるとJSONで返り、XMLも選べます。
この応答は、論文1本ごとの配列ではなく、group の配列です。ORCIDは識別子が同じ業績を1つのグループにまとめます。グループの中の work-summary には、登録元(Crossref、Scopus、本人など)ごとの記録が並びます。
見るべき点は3つです。
- 代表の1件は
display-indexが最大のもの。研究者本人が順位を付けていないAPI経由の項目は1が入り、グループ内の既定の代表になります - 各項目の
put-codeは、個別取得に使う番号。/work/<put-code>で1件、/works/<put-code1>,<put-code2>,...で最大100件をまとめて取れます visibilityがpublicの項目だけが、トークンさえあれば誰でも読める。信頼先限定(limited)の項目は、研究者本人が許可した/read-limitedのトークンがないと返りません
グルーピングの規則は、識別子とその関係で決まります。関係には「self」「part-of」「version-of」「funded-by」の4種類があります。論文本体のDOIは self、掲載誌のISSNは part-of で付くので、ISSNが同じだけで別論文が1グループになることはありません。
Claude Codeに渡す指示と権限
スクリプトを書かせる前に、守らせたいことをCLAUDE.mdに置きます。次は、このタスク用の断片の例です。
## 業績一覧(ORCID)
- ORCIDの取得は `scripts/orcid_works.py` だけで行う。資格情報は環境変数から読み、値を出力しない
- worksは `group` ごとに1行にする。代表は `display-index` が最大の項目
- DOIがない・同題同年が複数ある・種別と誌名が噛み合わない行は、直さずに「要確認」として一覧にする
- ORCIDの記録は書き換えない。直す箇所は報告だけする権限は、トークンを誤って読み書きしないように絞ります。Claude Codeの権限は .claude/settings.json の permissions で、deny が最優先で評価されます。
{
"permissions": {
"allow": ["Bash(python3 scripts/orcid_works.py:*)"],
"deny": ["Read(./.env)", "Read(./.env.*)"]
}
}Read のdenyは、Claudeのファイルツールが対象です。cat .env のようなBashからの読み出しは別の規則で扱われるので、資格情報はリポジトリ外に置き、スクリプトを起動するシェルにだけ渡すのが確実です。
取得と整形のスクリプト
次のスクリプトは、ORCID iDを受け取って /works を取り、グループごとに代表の1件を選んで行にします。同時に、要確認のフラグを付けます。標準ライブラリだけで動きます。
"""ORCID Public API から works を取得し、業績一覧の下書きと要確認フラグを出す。"""
import json, os, re, sys, urllib.parse, urllib.request
BASE = "https://pub.orcid.org/v3.0"
TOKEN_URL = os.environ.get("ORCID_TOKEN_URL", "https://orcid.org/oauth/token")
def get_token():
body = urllib.parse.urlencode({
"client_id": os.environ["ORCID_CLIENT_ID"],
"client_secret": os.environ["ORCID_CLIENT_SECRET"],
"grant_type": "client_credentials",
"scope": "/read-public",
}).encode()
req = urllib.request.Request(TOKEN_URL, body, {"Accept": "application/json"})
return json.load(urllib.request.urlopen(req))["access_token"]
def fetch_works(orcid):
req = urllib.request.Request(
f"{BASE}/{orcid}/works",
headers={"Accept": "application/json",
"Authorization": f"Bearer {os.environ.get('ORCID_TOKEN') or get_token()}"})
return json.load(urllib.request.urlopen(req))
def norm(s):
return re.sub(r"[^0-9a-z-ヿ一-鿿]", "", s.lower())
def rows(data):
out = []
for g in data["group"]:
ws = g["work-summary"]
best = max(ws, key=lambda w: int(w.get("display-index") or 0))
dois = [e["external-id-value"] for e in g["external-ids"]["external-id"]
if e["external-id-type"] == "doi"]
year = ((best.get("publication-date") or {}).get("year") or {}).get("value")
out.append({
"put_code": best["put-code"], "year": year,
"title": best["title"]["title"]["value"], "type": best["type"],
"journal": (best.get("journal-title") or {}).get("value"),
"doi": dois[0].lower() if dois else None, "sources": len(ws),
})
seen, flags = {}, []
for r in out:
f = []
if not r["doi"]: f.append("DOIなし")
if not r["year"]: f.append("年なし")
if r["type"] == "journal-article" and not r["journal"]: f.append("誌名なし")
key = (norm(r["title"]), r["year"])
if key in seen: f.append(f"タイトル重複:{seen[key]}")
seen.setdefault(key, r["put_code"])
r["flags"] = f
return sorted(out, key=lambda r: r["year"] or "0000", reverse=True)
if __name__ == "__main__":
data = json.load(open(sys.argv[2])) if sys.argv[1] == "--file" else fetch_works(sys.argv[1])
for r in rows(data):
print(r["year"], r["put_code"], r["type"], r["doi"] or "-", r["sources"],
",".join(r["flags"]) or "-", r["title"][:30], sep="\t")ORCID_TOKEN が環境変数にあればそれを使い、なければクライアントIDとシークレットから取り直します。保存済みのJSONで試すときは --file を使います。
export ORCID_CLIENT_ID=... ORCID_CLIENT_SECRET=...
python3 scripts/orcid_works.py 0000-0002-1825-0097実際の応答と、フラグの読み方
テスト用に公開されているレコード(0000-0002-1825-0097)の /works を、このスクリプトの整形部分に通すと次の6行になります。構造は応答の実物から取っており、登録元が複数ある業績も含まれています。
2012 9543020 journal-article 10.5555/12345680 2 誌名なし A Methodology for the Emulatio
2012 4562455 journal-article 10.5555/666655554444 1 誌名なし The Memory Bus Considered Harm
2011 4562454 journal-article 10.5555/987654321 1 誌名なし The Impact of Interactive Epis
2008 4562453 journal-article 10.5555/12345679 1 誌名なし Developing Thin Clients Using
2008 4562456 journal-article 10.5555/12345678 1 誌名なし Toward a Unified Theory of Hig
1987 19980729 journal-article 10.1109/tps.1987.4316723 1 - Bulk and surface plasmons in a見どころは2つあります。1行目は、Crossref由来と本人登録の2つの項目が1グループにまとまっています。列の「2」がその項目の数です。代表に選ばれたのはCrossref由来の項目で、display-index が2と、本人登録の1より大きいからです。
もう1つは「誌名なし」の5行です。この5件は journal-title が空で、誌名が title の subtitle に入っていました。残る1件(Scopus由来)は journal-title に誌名が入っています。登録元によって誌名の置き場所が違うので、journal-title だけを見る一覧は誌名が欠けます。そこで、フラグを「直す指示」にせず「置き場所の確認」にして、subtitle を誌名として使うかどうかは人が決めます。
フラグごとの対処
フラグと次の一手
DOIなし
書籍・学会発表・ソフトウェアなど、DOIがもともとない業績が含まれます。論文なのにDOIがない行だけを、Claude Codeに候補として挙げさせます。
タイトル重複
同じ論文が識別子違いで別グループになっている疑いです。ORCIDのグルーピングは識別子で決まるため、DOIがなくタイトルだけ同じ記録は別々に並びます。
誌名なし・年なし
journal-titleが空でもsubtitleに誌名があることがあります。年がない行は並べ替えで末尾に回るため、一覧を出す前に確認します。
重複の判定は、記号と空白を除いた小文字のタイトルに年を合わせた文字列で行います。これは本記事のスクリプトの規則で、ORCIDの規則ではありません。同じ題名の別論文(続編など)を重複と誤る余地があるので、判定は「要確認」にとどめます。
一覧への出力とサイトへの載せ方
行が整ったら、Claude Codeに出力形式への変換を頼みます。静的サイトならJSONを1ファイル、Markdownならサイトの業績ページに貼る表です。次のように依頼すると、一覧の元データと「要確認」の別表が分かれて出ます。
scripts/orcid_works.py の出力から、年の降順で業績一覧のMarkdownを作って。
フラグ付きの行は本文に入れず、末尾の「要確認」表に put-code と理由を並べて。要確認の行は、持ち主がORCIDの画面で直すのが筋です。Claude Codeが直せるのは自分の手元の一覧だけで、ORCIDのレコードの書き換えは、Public APIの範囲に入っていません。ORCIDの説明では、更新の許可を求められるのはMember APIを使える会員組織です。
つまずきやすい点
- sandboxと本番の取り違え: 資格情報はsandboxと本番のそれぞれで登録します。sandboxで試したあとは、トークンのURLと取得先のホストの両方を本番に替えます
- リダイレクトURI: 登録フォームで事前に入力します。本番で受け付けられるのはHTTPSのURIだけで、ドメインは使うものと完全に一致させます
- 公開されない業績がある:
/read-publicで読めるのはvisibilityがpublicの項目です。研究者本人が非公開にしたものは、一覧に出ません - 件数が合わない: 業績の件数は
groupの数で数えます。work-summaryを全部足すと、登録元ごとの項目の分だけ多くなります - 応答の形が違うとき: URLの
v3.0がAPIのバージョンです。欠けたフィールドはNoneで返る項目が多いので、スクリプトは空を許して読み、形が想定と違えば止まるようにしておきます
研究業績の別の取り方との使い分け
ORCIDは、本人が許可した登録元(Crossrefなど)が書き込んだ記録が集まる場所です。そのぶん、本人が連携を設定していない業績は載りません。引用関係で読む順を決めたいときはOpenAlex APIの記事、手元で管理している文献を表にしたいときはZoteroのWeb APIの記事が合います。研究室サイトの業績ページを「著者本人が管理する正本」から作るなら、ORCIDが入口になります。
業績ページの更新は、新しい論文が出たときにスクリプトを走らせ直す運用が基本です。出力の差分をgitで見れば、新しく入った行と、フラグが付いた行が分かります。トークンは約20年有効なので、保管するのは長期のトークン1つで足ります。
まとめ
ORCIDのPublic APIは、worksをグループ単位で返します。代表の1件は display-index で選び、DOIなし・題名重複・誌名の置き場所の違いは「要確認」として人に回すのが、一覧を壊さない進め方です。Claude Codeには取得と整形、フラグ付きの一覧づくりを任せ、ORCIDの記録を直す作業は持ち主の手元に残します。