Claude Media
Claude CodeでZoteroのWeb APIを呼び、手元の文献から文献レビュー表を作る

Claude CodeでZoteroのWeb APIを呼び、手元の文献から文献レビュー表を作る

Zoteroに溜めた文献のタグ・メモ・メタデータをAPIで取り出し、Claude Codeで文献レビュー表にまとめる手順。Web APIとローカルAPIの選び方、取得スクリプト、件数の突合までを扱います。

論文を読むたびにZoteroへ入れ、タグを付け、メモを残してきた。その数百件を、レビュー表に並べ直したい。ここで検索サービスに頼ると、すでに手元にある「自分が読んで付けた印」が使えません。

ZoteroにはAPIがあり、ライブラリのタイトル・著者・年・タグ・子ノートをJSONで取り出せます。取得はスクリプトに任せ、Claude Codeには「取得スクリプトを書く」「取得結果を表に整える」「件数を突合する」の3つを頼む構成が扱いやすい形です。この記事では、Web APIとローカルAPIの選び方から、動くスクリプト、権限の絞り方、表を信用する前の点検までを順に扱います。

手元のライブラリを入口にすると表の質が変わる

外部の論文データベースから候補を集める方法は、Claude Consensusコネクタで文献レビューの検索式と抜け確認を作るやalphaXivコネクタでのarXiv論文調査が扱っています。どちらも入口は「世の中にある論文」です。

Zoteroを入口にすると、入口は「自分が拾った論文」に変わります。タグは自分の分類軸、子ノートは読んだ所感です。レビュー表の列にそのまま使えます。Claudeに一から要約を書かせるより、すでに書いた自分の言葉を並べるほうが、あとで原典に当たり直すときの手戻りも少なくなります。

一方で、Zoteroに入っていない論文は表に出ません。抜けの確認は外部の検索側の仕事です。この記事の表は「手元にあるものの棚卸し」と位置づけます。

Web APIとローカルAPIのどちらを叩くか

Zoteroは同じ形のAPIを2か所で公開しています。zotero.orgのクラウドにあるライブラリを読むWeb APIと、デスクトップ版のZoteroが自分のPC上で開くローカルAPIです。

観点Web APIローカルAPI
ベースURLWeb APIhttps://api.zotero.orgローカルAPIhttp://localhost:23119/api/
認証Web API非公開ライブラリはAPIキーが必要ローカルAPI読み取りは認証なし
ユーザーIDWeb API数値のユーザーIDローカルAPI0 か自分の数値ID
ページ送りWeb API1回の上限は100件ローカルAPI既定の上限なし(全件を1回で返す)
レート制限Web APIあり(429、Backoff)ローカルAPIなし
前提Web APIネットに接続できることローカルAPIZoteroが起動していて、設定で許可済みであること

読み取りだけならローカルAPIが楽です。キーを作らずに済み、オフラインでも動き、上限にも当たりません。許可はSettings → Advancedの「Allow other applications on this computer to communicate with Zotero」で行います。未許可だと403が返ります。

ローカルAPIは、認証なしで誰でもライブラリを読める作りです。ポートを外部に公開しないこと、という注意が公式ページに明記されています。共有PCやリモート接続の環境では、Web APIにAPIキーを使う側へ倒すほうが安全です。

なお、ローカルAPIはAtom形式に対応しません(501が返ります)。この記事はJSONだけを使うので影響はありません。

使うエンドポイント

ライブラリのURLは /users/<userID> か /groups/<groupID> で始まります。レビュー表づくりで使うのは次の4つです。

URI取れるもの
<prefix>/collections取れるものコレクション(フォルダ)の一覧
<prefix>/collections/<collectionKey>/items/top取れるものコレクション内の最上位アイテム
<prefix>/items/<itemKey>/children取れるもの子アイテム(ノートや添付)
<prefix>/tags取れるものライブラリのタグ一覧

items/top は子アイテムを除いた最上位だけを返します。論文1本が1行になるので、表づくりにはこちらが向きます。ノートや添付ファイルは子として別に入っているため、メモが必要なら children か itemType=note で取りにいきます。

取得の仕様で押さえる5点

Claude Codeにスクリプトを書かせる前に、仕様側の制約を依頼文へ織り込んでおくと手戻りが減ります。

  • バージョン指定: Zotero-API-Version: 3 ヘッダを付ける。本番コードでは版を明示する、とされています
  • 1回100件まで: Web APIの limit は1〜100で、既定は25件です。start で続きを取り、Total-Results ヘッダの件数と突き合わせます
  • 絞り込み: tag=foo(ANDは tag=foo&tag=bar、ORは ||、除外は先頭に -)と、itemType=-attachment のような除外が使えます
  • 並び順: sort に date、title、creator、publicationTitle などを指定できます。既定は dateModified です
  • 差分取得: 2回目以降は since=<前回のLast-Modified-Version> を付けると、変わったものだけ取れます

429(取りすぎ)が返ったら Retry-After の秒数だけ待ち、同時リクエストは4本以下に抑える、という指定もあります。Backoff ヘッダが付いた応答は、成功していても指定秒数だけ次のリクエストを控えます。取得スクリプトにはこの待ちを最初から入れておきます。

取得スクリプトをClaude Codeに書かせる

次の依頼文で、標準ライブラリだけの取得スクリプトを書かせます。仕様はこの記事の内容をそのまま貼れば足ります。

scripts/zotero_export.py を作って。条件:
- 環境変数 ZOTERO_BASE(既定 http://localhost:23119/api)、
  ZOTERO_PREFIX(既定 users/0)、ZOTERO_API_KEY(任意)を読む
- Zotero-API-Version: 3 を付け、limit=100 と start でページ送りする
- 429 は Retry-After、Backoff ヘッダは指定秒数だけ待つ
- 取得件数が Total-Results と一致しなければ異常終了する
- 最上位アイテムを review/items.csv に、子ノートを review/notes.csv に書く
- 書き込むのは review/ 配下だけ。Zoteroへの書き込みリクエストは送らない

仕様に沿ったスクリプトの一例を示します。細部はClaudeの出力ごとに変わります。

import csv, json, os, sys, time
import urllib.error, urllib.parse, urllib.request
 
BASE = os.environ.get("ZOTERO_BASE", "http://localhost:23119/api")
PREFIX = os.environ.get("ZOTERO_PREFIX", "users/0")
KEY = os.environ.get("ZOTERO_API_KEY")
 
 
def get(path, params):
    qs = urllib.parse.urlencode(params)
    req = urllib.request.Request(f"{BASE}/{PREFIX}/{path}?{qs}")
    req.add_header("Zotero-API-Version", "3")
    if KEY:
        req.add_header("Zotero-API-Key", KEY)
    for attempt in range(4):
        try:
            with urllib.request.urlopen(req) as r:
                body = json.load(r)
                total = int(r.headers.get("Total-Results", len(body)))
                time.sleep(int(r.headers.get("Backoff", 0)))
                return body, total
        except urllib.error.HTTPError as e:
            if e.code != 429:
                raise
            time.sleep(int(e.headers.get("Retry-After", 2 ** attempt)))
    sys.exit("429 が続いたので中断します")
 
 
def fetch_all(path, **params):
    rows, start = [], 0
    while True:
        body, total = get(path, {**params, "limit": 100, "start": start})
        rows += body
        start += len(body)
        if not body or start >= total:
            if len(rows) != total:
                sys.exit(f"件数不一致: {len(rows)} / {total}")
            return rows
 
 
def authors(creators):
    names = [c.get("name") or f'{c.get("lastName", "")} {c.get("firstName", "")}'.strip()
             for c in creators]
    return "; ".join(names)
 
 
collection = sys.argv[1] if len(sys.argv) > 1 else None
path = f"collections/{collection}/items/top" if collection else "items/top"
items = fetch_all(path, itemType="-attachment", sort="date", direction="asc")
os.makedirs("review", exist_ok=True)
 
with open("review/items.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["key", "date", "authors", "title", "publication", "tags", "abstract"])
    for it in items:
        d = it["data"]
        if d["itemType"] == "note":
            continue
        w.writerow([d["key"], d.get("date", ""), authors(d.get("creators", [])),
                    d.get("title", ""), d.get("publicationTitle", ""),
                    "; ".join(t["tag"] for t in d.get("tags", [])),
                    d.get("abstractNote", "")])
 
notes = fetch_all("items", itemType="note")
with open("review/notes.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["parent", "note"])
    for n in notes:
        d = n["data"]
        w.writerow([d.get("parentItem", ""), d.get("note", "")])

実行は次のとおりです。コレクションのキーは collections エンドポイントで一覧できます。

python3 scripts/zotero_export.py
python3 scripts/zotero_export.py ABCD2345   # コレクションを絞るとき

Web APIを使う場合は、ベースURLとユーザーIDを差し替えます。

ZOTERO_BASE=https://api.zotero.org ZOTERO_PREFIX=users/1234567 \
  python3 scripts/zotero_export.py

最初に1件だけ取って、フィールド名が想定どおりかを見てください。ノートの本文がどのフィールドにどんな形(プレーンテキストかマークアップ付きか)で入るか、publicationTitle のない種別(書籍など)で列が空になるか、はライブラリの中身で変わります。スクリプトを信じる前に review/notes.csv を数行開く、というひと手間で、表が空欄だらけになる事故を防げます。

研究室の共有ライブラリと、JSON以外の取り方

共有グループのライブラリも、/groups/<groupID> に変えるだけで同じ形で読めます。グループIDはグループ名とは別の数字で、/users/<userID>/groups の応答から引けます。ローカルAPIでは、グループのメタデータは識別に要る範囲に限られ、読み取り専用です。メンバー一覧や権限の細目は載りません。

表の元データが整っていれば、JSONを経由せずに書誌形式で受け取る手もあります。format や include に csv、csljson、bibtex、ris などを指定でき、書き出しにはCSVが直接使えます。ただしエクスポート形式を使うときは limit が必須です。メモや独自のタグ列まで表に入れたいなら、この記事のようにJSONを自分で整形するほうが自由に組めます。

権限とキーの扱いを先に固める

取得スクリプトは決まった形で何度も走るので、そのコマンドだけを許可し、鍵を含むファイルは読ませない設定にしておきます。.claude/settings.json の例です。

{
  "permissions": {
    "allow": [
      "Bash(python3 scripts/zotero_export.py *)",
      "Read(./review/**)"
    ],
    "deny": [
      "Read(./.env)"
    ]
  }
}

ルールはdeny、ask、allowの順に評価され、最初に当たったものが結果を決めます。Read(./.env) をdenyにすると、Claudeの組み込みファイル操作から .env を読めなくなります。ただし、ファイルの名前を挙げずに中身を読むスクリプトまでは止められません。OSレベルで塞ぎたいときはサンドボックスを使います。権限ルールの書き方は、法人番号Web-APIの名寄せの記事にも同じ型の例があります。

キーの扱いは次の3点で足ります。

  • ローカルAPIで済むなら、そもそもキーを作らない
  • Web APIのキーは、Zoteroのアカウント設定から専用に作る。読み取りだけなら書き込み権限を付けない
  • キーはクエリ文字列に載せず、Zotero-API-Key ヘッダで渡す(クエリ渡しは推奨されていません)

ローカルAPIにはZotero 10以降で書き込みも使えますが、書き込みには実行時にユーザーが許可するキーが要ります。この用途では不要なので、依頼文にも「書き込みリクエストは送らない」と入れておきます。

レビュー表への整理を依頼する

CSVができたら、整理はClaudeの得意分野です。プロジェクトの CLAUDE.md に、表のルールを置いておきます。

## 文献レビュー表のルール
- 入力は review/items.csv と review/notes.csv だけ。外の知識で列を埋めない
- 列は 著者・年 / タイトル / 掲載誌 / タグ / 自分のメモ の5つ
- 「自分のメモ」は notes.csv の該当箇所を短く言い換える。メモが無い行は「メモなし」
- abstract が空の行は、内容を推測せず「抄録なし」と書く
- key 列は表に残す(Zotero側で該当アイテムを開くため)

そのうえで、依頼は短くて足ります。

review/items.csv と review/notes.csv から、タグ「方法論」が付いた文献の
レビュー表を review/table-methods.md に作って。件数も先頭に書いて。

タグごとに表を分けると、レビューの章立てがそのまま表の単位になります。タグは人が付けた分類なので、Claudeが分類し直すより信頼でき、同じ論文が2つの表に載る扱いも明示できます。

非対話の実行で回したい場合は、claude -p に --allowedTools を付けます。自動承認の対象は最小にします。

claude -p "review/items.csv からタグ別のレビュー表を作成" \
  --allowedTools "Read,Edit"

引用文献としての書式が要る段階になったら、表ではなくZotero側の機能に戻します。format=bib で整形済みの文献リストをXHTMLで受け取れ、style=apa のようにスタイルを選べます。ただしこの形式は最大150件までです。大量の場合はコレクションやタグで分けて取ります。

表を信用する前の点検

Claudeが整えた表は、候補一覧として扱います。点検は次の3段で済みます。

  1. 件数: 表の先頭の件数と、Total-Results の値、Zotero画面でのコレクションの件数を並べる。スクリプトが不一致で異常終了する作りなので、通っていれば取りこぼしはありません
  2. 抜き取り: 表から数行を選び、key でZoteroのアイテムを開いて、著者・年・タイトルを見比べる
  3. 空欄: 「抄録なし」「メモなし」の行数を数える。空欄が多いなら、その列はClaudeが埋めたのではなく、元データが空だったという意味です

卒論や学位論文の引用そのものを点検する段階は、卒論の引用をClaudeプロジェクトで点検する手順に分けて書かれています。外部の抽出表づくりの考え方はElicitコネクタの抽出表と共通します。

つまずきやすい点

症状原因と対処
ローカルAPIが403原因と対処Zoteroの設定で他アプリとの通信が許可されていない。Settings → Advancedで有効にする
ローカルAPIが400原因と対処自分以外のユーザーIDを指定している。users/0 か自分のIDにする
Web APIが403原因と対処キーが無効、または対象ライブラリへの権限がない。キーの権限を見直す
429が続く原因と対処同時リクエストが多い。4本以下に抑え、Retry-After の秒数を待つ
件数が画面と合わない原因と対処子アイテムやゴミ箱が絡んでいる。items/top はゴミ箱のアイテムを含まず、includeTrashed=1 で含められる
ローカルのバージョン値を保存した原因と対処Zotero 10以降、ローカルのバージョンはWeb APIと無関係。since に使う値は取得元ごとに分けて保存する

最後の行は差分取得を組むときの落とし穴です。ローカルAPIの応答には Zotero-Server-ID ヘッダが付き、別のZotero環境に切り替わるとバージョンの意味が変わります。キャッシュを置くなら、サーバーIDごとに分けます。

まとめ

手元のZoteroを入口にした文献レビュー表では、タグと子ノートが表の列になり、Claudeの仕事は整形と突合に絞れます。読み取りはローカルAPIならキー不要で、Web APIに切り替えるのは外出先や共有環境のときです。

表を使う前に、件数、抜き取り、空欄の3点を確かめてください。そこまで通れば、あとはタグを足すたびにスクリプトを再実行して表を更新できます。

この記事を共有:XはてブLinkedIn