Claude Codeでresearchmap APIから業績を取り、公募との照合表を作る
URAがresearchmap APIで学内研究者の研究キーワード・論文・研究課題を取り、公募の対象分野と突き合わせて声かけ候補の表にする手順です。名寄せ漏れと古い業績を人が確かめる欄つきです。
外部資金の公募が出るたびに、学内の誰に声をかけるかをURAが記憶と検索で探していると、見落としが出ます。researchmapには研究者が登録した研究キーワード・論文・研究課題があり、APIで機関単位に取り出せます。公募の対象分野のキーワードと突き合わせれば、声かけ候補の表の下書きが作れます。
この記事では、researchmap.v2 APIの取得設計書に沿って、Python製の照合スクリプトを1本置き、点数と根拠つきのCSVを出すところまでを扱います。スクリプトの作成と修正はClaude Codeに任せます。一方で「この人は本当に本人の業績か」「この業績は古すぎないか」の判断は表の確認欄に残し、URAが確かめます。
始める前に決まる条件 — APIキーは機関単位
このAPIは、誰でも自由に叩ける公開APIではありません。取得設計書の表紙は「データ交換機関向け」で、利用にはJSTへの利用申請が必要です。申請は原則として機関単位とされています。受理されるとJSTから機関へAPIキー(クライアントIDを含む複数の情報)が発行されます。
つまり、URAが個人として試す前に、学内で申請済みかどうかを確かめる必要があります。設計書の用語集では、機関側の窓口の権限として「データ交換担当者」「所属機関の事務担当者」「機関編集者」が挙がっています。申請済みなら、鍵の扱いを含めて、その担当者から利用範囲を聞く流れになります。
取れる範囲にも条件があります。APIキーに紐づいた機関に所属する一般会員の情報が対象で、研究者ごとの権限設定と機関の権限設定で可否が決まります。所属機関以外の研究者は、「公開」の情報に限って取得できます。
設計書に載っている数値の条件
1ページの上限
1000件
limitの既定は100件。超えて指定すると1000件で返る
アクセストークンの有効期間
3600秒
応答例のexpires_inの値。切れたら取り直す
JWTの有効期限
2分未満
通常の設定として示されている。iatは現在時刻と5分以上ずれないこと
取得してよい範囲を先に絞る
researchmapの公開フラグは3種類あります。公開(disclosed)、研究者のみに公開(researchers_only)、非公開(closed)です。設計書は、非公開や研究者のみに公開の情報を取得した場合に、研究者総覧などで機関外部へ公開しないよう求めています。
声かけ候補の表は学内で回す資料ですが、表の根拠欄に業績の題名が入るため、あとで学外に出る可能性があります。そこで、スクリプトのトークン要求では public_only を指定します。設計書のscopeの説明では、public_only は「公開情報に限り閲覧可」で、read や write と併用した場合は public_only が優先されます。非公開の情報が表に混ざる経路を、最初から塞げます。
achievements は業績情報すべてを対象にするscopeです。利用申請で許可された範囲を超える指定はできないので、申請の取得範囲が「プロフィール情報のみ」だった場合は、業績を取れない可能性があります。
全体の流れ
照合表ができるまでの5段階
- 1
研究者の一覧と公募の一覧を用意する
researchersは氏名とpermalinkのCSV、callsは公募名・締切・対象分野キーワードのCSVです。
- 2
トークンを取る
APIキーのクライアントIDと秘密鍵でJWTを作り、トークンAPIに送ります。
- 3
業績リストを取る
研究者ごとに研究キーワード・論文・研究課題の3種別を、最後のページまで取ります。
- 4
公募キーワードと突き合わせる
どの種別で当たったかに重みをつけ、点数と根拠を出します。
- 5
確認欄を人が埋める
名寄せ漏れと古い業績の疑いが付いた行を、URAが確かめます。
入力の2ファイルと、鍵の置き場所
研究者の一覧には、学内の人事情報などから作った氏名と、researchmapのpermalinkを入れます。permalinkは https://researchmap.jp/{パーマリンク} というURLの末尾の部分です。姓の列は、あとで説明する「別人の疑い」の判定に使います。
# researchers.csv
name_ja,family_ja,permalink
山田 太郎,山田,taro_yamada
# calls.csv(keywords は ; 区切り)
call_id,title,deadline,keywords
C01,地域防災の社会実装を支える研究,2026-12-15,防災;避難;レジリエンス;disaster鍵とクライアントIDは、環境変数で渡します。置き場所はこのリポジトリの外にします。理由は次の節で説明します。
# 人間が自分のシェルで設定する(Claude Codeに値を見せない)
export RMAP_CLIENT_ID="(JSTから発行されたクライアントID)"
export RMAP_KEY_FILE="$HOME/.secrets/rmap_private.pem"Claude Codeに渡す指示と権限
作業用のフォルダのルートに CLAUDE.md を置きます。Claude Codeはセッションの開始時にこのファイルを読むので、扱うデータの範囲と禁止事項を先に書いておけます。ただし、CLAUDE.md の記述はモデルが読んで従うもので、従うことが保証される仕組みではありません。守らせたい線は、次の設定で権限に落とします。
# researchmap 照合作業
- 目的: 学内研究者の公開業績と公募キーワードを突き合わせ、声かけ候補の表(CSV)を作る
- 実行: python3 match_calls.py researchers.csv calls.csv out.csv
- 鍵とクライアントIDは環境変数 RMAP_KEY_FILE / RMAP_CLIENT_ID で渡す。値を表示・ログ・ファイルに書かない
- 取得するのは公開情報だけ(scope は public_only)。非公開・研究者のみに公開の情報は扱わない
- 変更後は calls.csv の小さなサンプルで実行し、out.csv の先頭5行を見せる
- 表の「人の確認」列は空のまま出す。埋めるのは担当者権限は、作業フォルダの .claude/settings.json に書きます。許可するコマンドを絞り、鍵のありかを指す読み取りを拒否します。
{
"permissions": {
"allow": ["Bash(python3 match_calls.py *)"],
"deny": [
"Read(~/.secrets/**)",
"Read(./*.pem)",
"WebFetch"
]
}
}ここに落とし穴があります。Read の拒否ルールは、Claude Codeの組み込みのファイルツールと、cat や head などClaude Codeが認識するファイル読み取りコマンドに効きます。ところが、PythonやNodeのスクリプトがファイルを自分で開く場合には効きません。今回の match_calls.py は、まさに鍵ファイルを自分で開きます。つまり、拒否ルールがあっても、スクリプトを書き換えて鍵を表示させる経路までは塞げません。
落とし穴
拒否ルールだけでは鍵は守れない
Read の拒否は、スクリプトが自分で開くファイルには及びません。鍵をリポジトリの外に置くことに加え、サンドボックスでプロセス単位の制限をかけるか、スクリプトの変更を差分で必ず確認する運用にします。後者なら、match_calls.py の open と print の変更が目に入ります。
サンドボックスや権限の画面は、sandbox-runtimeの記事と/permissionsコマンドの記事で扱っています。
取得と照合のスクリプト
Claude Codeに依頼する文は、次の程度で足ります。
match_calls.py を新規に作って。researchmap API の取得設計書(V4.7)に沿って、
OAuth2 JWT Bearer Flow でトークンを取り、researchers.csv の各 permalink から
research_interests / published_papers / research_projects を取得する。
calls.csv の keywords と突き合わせて点数と根拠を出し、out.csv に書く。次の形になれば要件を満たします。依頼文の細部によって実装は変わるので、書かれた内容そのものを読んで確認してください。例えば、jwt(PyJWT)と、RS256の署名に必要な cryptography は、あらかじめ pip install pyjwt cryptography で入れておきます。
match_calls.py の全文(約110行)
"""researchmap API から業績を取り、公募キーワードとの照合表を CSV に出す。"""
import csv, json, os, sys, time
import urllib.parse, urllib.request, urllib.error
from datetime import date
import jwt # PyJWT(RS256 署名に cryptography が必要)
BASE = "https://api.researchmap.jp"
TOKEN_URL = BASE + "/oauth2/token"
TYPES = ["research_interests", "published_papers", "research_projects"]
WEIGHT = {"research_interests": 3, "research_projects": 2, "published_papers": 1}
YEARS = int(os.environ.get("RMAP_YEARS", "5")) # 何年前までの業績を見るか
def get_token():
now = int(time.time())
claim = {"iss": os.environ["RMAP_CLIENT_ID"], "aud": TOKEN_URL,
"sub": "0", "iat": now - 10, "exp": now + 90}
with open(os.environ["RMAP_KEY_FILE"], "rb") as f: # 鍵はリポジトリの外に置く
assertion = jwt.encode(claim, f.read(), algorithm="RS256")
body = urllib.parse.urlencode({
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": assertion, "scope": "public_only achievements", "version": "2",
}).encode()
with urllib.request.urlopen(urllib.request.Request(TOKEN_URL, data=body)) as r:
return json.load(r)["access_token"]
def fetch_list(token, permalink, kind, from_year):
"""業績リストを最後のページまで取る。失敗は (None, 理由) で返す。"""
url = f"{BASE}/{permalink}/{kind}?limit=1000&format=json"
if kind != "research_interests":
url += f"&from_date={from_year}"
items = []
while url:
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
try:
with urllib.request.urlopen(req) as r:
data = json.load(r)
except urllib.error.HTTPError as e:
return None, f"HTTP {e.code}"
items += data.get("items", [])
nxt = data.get("_links", {}).get("next")
url = nxt["href"] if nxt else None
return items, ""
def text_of(kind, it):
def ja_en(d):
return " ".join((d or {}).get(k, "") for k in ("ja", "en"))
if kind == "research_interests":
return ja_en(it.get("keyword"))
if kind == "published_papers":
return ja_en(it.get("paper_title"))
return " ".join([ja_en(it.get("research_project_title")),
ja_en(it.get("system_name")), ja_en(it.get("description"))])
def score(blobs, keywords):
"""blobs: {kind: [text,...]}。キーワードごとに最初に当たった種別だけ数える。"""
total, hits = 0, []
for kw in keywords:
for kind in TYPES:
hit = next((t for t in blobs.get(kind, []) if kw.lower() in t.lower()), None)
if hit:
total += WEIGHT[kind]
hits.append(f"{kw}←{kind}:{hit[:30]}")
break
return total, hits
def main(researchers_csv, calls_csv, out_csv):
token = get_token()
from_year = date.today().year - YEARS
people = []
for r in csv.DictReader(open(researchers_csv, encoding="utf-8-sig")):
blobs, flags, papers = {}, [], []
for kind in TYPES:
items, err = fetch_list(token, r["permalink"], kind, from_year)
if items is None:
flags.append(f"{kind}取得失敗({err})")
continue
blobs[kind] = [text_of(kind, i) for i in items]
if kind == "published_papers":
papers = items
if not any(blobs.values()):
flags.append("公開業績なし(permalink 違い・非公開・自機関外の可能性)")
elif papers and not any(r["family_ja"] in json.dumps(p.get("authors", {}), ensure_ascii=False)
for p in papers):
flags.append("著者欄に姓が出ない(別人の permalink か表記違い)")
if not blobs.get("published_papers"):
flags.append(f"直近{YEARS}年の論文なし(古い業績のみの可能性)")
people.append((r, blobs, flags))
rows = []
for c in csv.DictReader(open(calls_csv, encoding="utf-8-sig")):
kws = [k.strip() for k in c["keywords"].split(";") if k.strip()]
for r, blobs, flags in people:
s, hits = score(blobs, kws)
if s:
rows.append({"call_id": c["call_id"], "公募名": c["title"], "締切": c["deadline"],
"研究者": r["name_ja"], "permalink": r["permalink"], "点数": s,
"根拠": " / ".join(hits), "要確認": " ; ".join(flags), "人の確認": ""})
rows.sort(key=lambda x: (x["call_id"], -x["点数"]))
with open(out_csv, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0]) if rows else ["call_id"])
w.writeheader(); w.writerows(rows)
print(f"{len(rows)} 行を {out_csv} に書きました", file=sys.stderr)
if __name__ == "__main__":
main(*sys.argv[1:4])設計書と対応づけて読む箇所は4つです。
- claimの
audはhttps://api.researchmap.jp/oauth2/tokenです。設計書は/をエスケープしたhttps:\/\/api.researchmap.jp\/oauth2\/tokenの形で示しています。JSONでは\/と/は同じ文字なので、PythonのJSONでは素のURLを書きます。 subは固定で"0"、algはRS256、typはJWTです。- 業績リストは
GET https://api.researchmap.jp/{permalink}/{業績種別}で取ります。limitは最大1000で、続きは応答の_links.next.hrefをたどります。 from_dateで期間を絞ると、論文は出版年月、研究課題は研究期間で絞られます。研究キーワードは期間の概念がないので絞りません。
3種別から何を読み、どう点数にするか
照合に使う業績は3種別です。点数の重みは、このスクリプトの設計判断で、設計書の規定ではありません。研究者自身が選んだ語に近い順に、重くしています。
| 業績種別 | 読む項目 | 重み | 見方 |
|---|---|---|---|
| research_interests | 読む項目keyword(ja・en) | 重み3 | 見方研究者が自分で付けた語。外れにくい |
| research_projects | 読む項目research_project_title・system_name・description | 重み2 | 見方過去の採択課題。制度名で公募との近さが分かる |
| published_papers | 読む項目paper_title(ja・en) | 重み1 | 見方題名だけの一致は偶然も多い |
同じキーワードが複数の種別で当たっても、最初に当たった種別だけを数えます。研究キーワードで当たった語を、論文の題名でも重ねて加点しないためです。
研究分野(research_areas)は、使っていません。レスポンスは discipline_number(A999の形式の大分類番号)と item_number で、中身は分野コード表の番号です。公募の対象分野を番号で突き合わせるなら、コード表を別に用意する必要があります。まずは語で突き合わせ、番号の照合は後から足す順が現実的です。
研究課題には、identifiers の中に grant_number(課題番号)があります。過去の課題が公募と同じ制度かどうかを見たいときは、system_name の語を calls.csv のキーワードに入れておくと、同じ仕組みで当たります。
出力は、次のような形になります。以下は形を示すための架空の例です。
call_id,公募名,締切,研究者,permalink,点数,根拠,要確認,人の確認
C01,地域防災の社会実装を支える研究,2026-12-15,山田 太郎,taro_yamada,5,防災←research_interests:地域 防災 / 避難←published_papers:避難行動の分析,,
C01,地域防災の社会実装を支える研究,2026-12-15,鈴木 花子,hanako_s,1,disaster←published_papers:Disaster risk of coastal,直近5年の論文なし(古い業績のみの可能性),名寄せ漏れと古い業績を、人が確かめる
表に載るのは「公開業績があり、公募の語に当たった人」だけです。載らなかった人の中に、本当は候補になる人がいる状況を、名寄せ漏れと呼んでおきます。要確認の列は、その疑いを拾うための印です。
要確認の印は、どの疑いに対応するか
名寄せ側の疑い
「著者欄に姓が出ない」はpermalinkが別人の可能性、「公開業績なし」はpermalinkの誤りや非公開、自機関外の可能性を示します。
鮮度側の疑い
「直近5年の論文なし」は、点数が古い業績や研究キーワードだけから出ている可能性を示します。
確かめる順番は、次のとおりです。
- 要確認が付いていない行でも、点数の上位は根拠の業績を1件開き、題名が公募の趣旨に合うかを見ます。
- 「著者欄に姓が出ない」の行は、researchmapの本人ページと学内の名簿を見比べます。同姓の別人や、旧姓・ローマ字の表記違いが典型です。
- 「公開業績なし」の行は、本人が非公開にしているのか、permalinkが違うのかを分けます。非公開なら、この方法では候補に出せません。
- 点数が高いのに「直近の論文なし」の行は、研究キーワードの更新日が古くないかを、本人ページで見ます。
落とし穴
permalinkは変わることがある
設計書によると、本人以外がアカウントを作った場合、本人の初回ログイン時に一度だけpermalinkを好きな文字列に変更できます。そのため、会員を一意に特定する用途には、permalinkではなく会員ID(R000282839のような形式)を使うよう勧められています。researchers.csvのpermalinkが急に取れなくなったら、まず変更を疑います。
古い業績の扱いも決めておきます。スクリプトの RMAP_YEARS は既定で5年です。この値を変えれば、論文と研究課題の対象期間が変わります。公募の趣旨が「継続的な研究実績」なのか「直近の新しい取り組み」なのかで、年数を変えて見比べると、候補の顔ぶれがどれだけ動くかが分かります。
実行でつまずきやすい点
- トークン要求が通らないときは、JWTの
iatとexpを見ます。設計書は、発行時間とリクエスト時間のずれが5分以上にならないよう求めています。手元の時計がずれていると弾かれやすくなります。 - 大学全体の研究者を一度に回すと、アクセストークンの有効期間(応答例では3600秒)を超える可能性があります。切れたらトークンを取り直します。
- 研究者が多い場合は、研究者ごとの取得を一括取得APIに置き換える方法もあります。設計書には
_bulkというエンドポイントがあり、自機関の研究者の情報だけが取れると書かれています。件数が多いときは、利用申請の担当窓口に使い方を相談します。 - 応答の
itemsが空でも、取得の失敗とは限りません。公開の業績が登録されていないだけのことがあるので、「公開業績なし」の印で人に回します。
他の研究者データとの使い分け
本人が連携を設定した業績を見るなら、ORCID Public APIの記事の方法が合います。ORCIDは本人の記録が前提で、機関のAPIキーは要りません。引用関係で読む順を決めるならOpenAlex APIの記事です。
researchmapのAPIは、機関単位の鍵が前提のかわりに、学内の研究者を一覧で扱える点が違います。URAの業務に合うのは、この「学内全員を、同じ条件で毎回取り直せる」ところです。
候補が決まったあとの作業は、別の記事に続きます。科研費の申請書の点検は研究計画調書のチェック手順、公募要領に合わせたデータ管理の書類はデータマネジメントプランの下書きにあります。
まとめ
researchmap APIで公募との照合表を作る作業は、技術よりも前に、学内でAPIキーの利用申請が済んでいるかで可否が決まります。済んでいれば、public_only で公開情報だけを取り、研究キーワード・研究課題・論文の3種別を重みつきで公募の語に当てる形で、候補の下書きは作れます。
Claude Codeに任せるのはスクリプトの作成と修正です。鍵はリポジトリの外に置き、スクリプトの変更は差分で確認します。表の確認欄に残る名寄せと鮮度の判断は、研究者本人のページを開けるURAの仕事として残ります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。