はじめに
国立国会図書館が提供するOpenSearch APIを使用して、書籍情報を取得する方法を紹介します。この記事では、実際のプロジェクトで使用したコードを基に、APIの使い方や実装のポイントを解説します。
NDL APIとは
国立国会図書館(NDL)が提供するOpenSearch APIは、国立国会図書館の蔵書検索システムから書籍情報を取得できるREST APIです。無料で利用でき、認証も不要です。
APIエンドポイント
https://ndlsearch.ndl.go.jp/api/opensearch
主なパラメータ
-
title: タイトルで検索 -
creator: 著者で検索 -
cnt: 取得件数(1回あたり最大500件) -
idx: 取得開始位置(1から始まる)
実装の詳細
必要なライブラリ
import requests
import xml.etree.ElementTree as ET
基本的なAPI呼び出し
url = "https://ndlsearch.ndl.go.jp/api/opensearch"
params = {
"title": "Python",
"cnt": 500,
"idx": 1
}
res = requests.get(url, params=params, timeout=30)
res.raise_for_status()
XML形式と名前空間の基礎知識
XML形式とは
NDL APIは、データをXML(eXtensible Markup Language)形式で返します。XMLは、データを構造化して表現するためのマークアップ言語です。
XMLの基本構造
XMLは、HTMLと似た構造を持っていますが、タグ名を自由に定義できる点が特徴です。NDL APIでは、Webで広く使われるRSS 2.0形式に近い構造を採用しています。
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<item>
<title>Python入門</title>
<author>山田太郎</author>
</item>
</channel>
</rss>
NDL APIが返すXMLの構造例
NDL APIが返す実際のXMLレスポンスは、RSS 2.0形式をベースに、書誌情報を詳細に表現するために拡張されています。
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">
<channel>
<item>
<title>Python入門</title>
<link>https://...</link>
<dc:title>Python入門</dc:title>
<dc:creator>山田太郎</dc:creator>
<dc:date>2023-01-15</dc:date>
<dc:subject>プログラミング</dc:subject>
</item>
</channel>
</rss>
名前空間(Namespace)とは
XMLでは、異なる仕様や標準から来る要素を区別するために 名前空間(Namespace)という仕組みが使われます。
なぜ名前空間が必要なのか
例えば、「title」という要素名は、書籍のタイトルを表す場合もあれば、HTMLのページタイトルを表す場合もあります。名前空間を使うことで、同じ要素名でも異なる意味を持つ要素を区別できます。
<title>Python入門</title>
<title>ページタイトル</title>
<book:title>Python入門</book:title>
<html:title>ページタイトル</html:title>
名前空間の仕組み
名前空間は、プレフィックス(接頭辞)とURI(Uniform Resource Identifier)の組み合わせで定義されます。
-
プレフィックス:
dc:,dcterms:など、要素名の前に付ける短い識別子 -
URI: 名前空間を一意に識別するためのURL(例:
http://purl.org/dc/elements/1.1/)
<data xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/">
<dc:title>Python入門</dc:title>
<dcterms:issued>2023-01-15</dcterms:issued>
</data>
NDL APIで使用される主な名前空間
NDL APIでは、以下の名前空間が使用されます:
| プレフィックス | URI | 用途 |
|---|---|---|
dc |
http://purl.org/dc/elements/1.1/ |
Dublin Coreの基本要素(タイトル、著者、日付など) |
dcterms |
http://purl.org/dc/terms/ |
Dublin Coreの拡張要素(発行日など) |
Dublin Coreとは、メタデータ(データに関するデータ)を記述するための国際標準です。図書館やアーカイブで広く使用されています。
PythonでのXMLパースと名前空間の扱い
Pythonのxml.etree.ElementTreeモジュールでXMLをパースする際、名前空間を正しく指定する必要があります。
名前空間を指定しない場合の問題
名前空間を指定せずに要素を検索すると、要素が見つかりません:
import xml.etree.ElementTree as ET
# XMLレスポンスをパース
root = ET.fromstring(res.content)
# ❌ 名前空間を指定しない場合:要素が見つからない
title = root.find(".//dc:title") # Noneが返される
print(title) # None
名前空間を正しく指定する方法
名前空間を辞書として定義し、find()メソッドの第2引数に渡します。
なお、RSS形式の <item> タグ自体には名前空間が付いていないため、findall で取得する際は名前空間の指定は不要です。その中にある <dc:title> などを取得する際に指定します。
# 名前空間の定義
namespaces = {
'dc': 'http://purl.org/dc/elements/1.1/',
'dcterms': 'http://purl.org/dc/terms/',
}
# ✅ 名前空間なしで item 要素を取得
root = ET.fromstring(res.content)
items = root.findall(".//item")
for item in items:
# item の中にある dc:title 要素を取得(ここでは名前空間が必要)
dc_title = item.find(".//dc:title", namespaces)
if dc_title is not None and dc_title.text:
print(dc_title.text) # "Python入門" など
名前空間の指定方法の詳細
find()やfindall()メソッドでは、XPath形式で要素を検索します:
-
.//dc:title: 現在の要素から下の階層で、dc:title要素を検索 -
dc:title: 直接の子要素としてdc:titleを検索
# 例1: 子要素を直接取得
title = item.find("dc:title", namespaces)
# 例2: 子孫要素を再帰的に検索
title = item.find(".//dc:title", namespaces)
# 例3: 複数の要素を取得
creators = item.findall(".//dc:creator", namespaces)
実際のXMLレスポンスの確認方法
デバッグ時には、実際のXMLレスポンスを確認すると理解が深まります:
res = requests.get(url, params=params, timeout=30)
res.raise_for_status()
# XMLレスポンスの内容を確認(デバッグ用)
print(res.text) # 実際のXML構造を確認できる
# XMLをパース
root = ET.fromstring(res.content)
XMLパースと名前空間の扱い(実装例)
NDL APIから取得したXMLを正しくパースするための完全なコード例:
import requests
import xml.etree.ElementTree as ET
url = "https://ndlsearch.ndl.go.jp/api/opensearch"
params = {
"title": "Python",
"cnt": 500,
"idx": 1
}
res = requests.get(url, params=params, timeout=30)
res.raise_for_status()
# XMLパース
root = ET.fromstring(res.content)
# 名前空間の定義
# NDLのRSS形式では、タイトルや著者はDublin Core名前空間(dc)で提供されます
namespaces = {
'dc': 'http://purl.org/dc/elements/1.1/',
'dcterms': 'http://purl.org/dc/terms/',
}
# RSS形式では <item> タグが各書籍データに対応します
# <item>自体には名前空間がつかないため、引数は不要です
items = root.findall(".//item")
for item in items:
# タイトルの取得 (dc:title)
# 子要素を取得する際は名前空間の指定が必要です
dc_title = item.find("dc:title", namespaces)
if dc_title is not None and dc_title.text:
print(f"タイトル: {dc_title.text}")
名前空間に関するよくあるエラーと対処法
エラー1: 要素が見つからない(Noneが返される)
原因: 名前空間を指定していない
# ❌ 間違い
title = item.find("dc:title") # Noneが返される
# ✅ 正しい
namespaces = {'dc': 'http://purl.org/dc/elements/1.1/'}
title = item.find(".//dc:title", namespaces)
エラー2: 名前空間のURIが間違っている
原因: 名前空間のURIが正しくない
# ❌ 間違い(URIが間違っている)
namespaces = {'dc': 'http://example.com/dc'} # 間違ったURI
# ✅ 正しい
namespaces = {'dc': 'http://purl.org/dc/elements/1.1/'}
エラー3: プレフィックスが間違っている
原因: XMLで使用されているプレフィックスと一致していない
# XMLでは "dc:title" を使用しているが...
# ❌ 間違い(プレフィックスが違う)
title = item.find(".//dublin:title", namespaces)
# ✅ 正しい(XMLで使用されているプレフィックスと一致)
title = item.find(".//dc:title", namespaces)
データ取得のループ処理
1回のリクエストで取得できる件数に上限があるため、複数回リクエストを送信する必要があります。
step = 500 # NDLの1回の上限
current_idx = 1
while True:
params = {
"title": keyword,
"cnt": step,
"idx": current_idx
}
res = requests.get(url, params=params, timeout=30)
root = ET.fromstring(res.content)
# RSSのitemタグを取得
items = root.findall(".//item")
if not items:
break # データがなくなったら終了
# データ処理...
current_idx += step # 次の取得位置に移動
実装例:書籍情報の取得
以下は、実際のプロジェクトで使用したコードです。
完全な実装コード
import requests
import xml.etree.ElementTree as ET
def fetch_books_from_ndl(keyword, max_records=None):
"""
NDL APIから書籍情報を取得する関数
Args:
keyword: 検索キーワード
max_records: 最大取得件数(Noneの場合は全件取得)
Returns:
list: 書籍情報のリスト
"""
url = "https://ndlsearch.ndl.go.jp/api/opensearch"
step = 500 # NDLの1回の上限
processed_data = []
current_idx = 1
# 名前空間の定義
namespaces = {
'dc': 'http://purl.org/dc/elements/1.1/',
'dcterms': 'http://purl.org/dc/terms/',
}
while True:
# 最大件数に達したら終了
if max_records and len(processed_data) >= max_records:
break
params = {
"title": keyword,
"cnt": step,
"idx": current_idx
}
try:
res = requests.get(url, params=params, timeout=30)
res.raise_for_status()
# XMLパース
root = ET.fromstring(res.content)
items = root.findall(".//item")
if not items:
break # データがなくなったら終了
for item in items:
# タイトルの取得
title = ""
dc_title = item.find(".//dc:title", namespaces)
if dc_title is not None and dc_title.text:
title = dc_title.text.strip()
# 発行日の取得
date = ""
dc_date = item.find(".//dc:date", namespaces)
if dc_date is not None and dc_date.text:
date = dc_date.text.strip()
# フォールバック: dcterms:issued
elif item.find(".//dcterms:issued", namespaces) is not None:
dcterms_issued = item.find(".//dcterms:issued", namespaces)
if dcterms_issued.text:
date = dcterms_issued.text.strip()
# 著者の取得(複数著者の場合に対応)
creator = ""
dc_creators = item.findall(".//dc:creator", namespaces)
if dc_creators:
creators = [c.text.strip() for c in dc_creators if c.text]
creator = " / ".join(creators) if creators else ""
# 主題の取得(複数主題の場合に対応)
subject = ""
dc_subjects = item.findall(".//dc:subject", namespaces)
if dc_subjects:
subjects = [s.text.strip() for s in dc_subjects if s.text]
subject = " / ".join(subjects) if subjects else ""
# データを辞書に格納
book_data = {
"title": title,
"date": date,
"creator": creator,
"subject": subject
}
processed_data.append(book_data)
current_idx += step
except Exception as e:
print(f"エラー発生: {e}")
break
return processed_data
# 使用例
books = fetch_books_from_ndl("Python", max_records=100)
for book in books:
print(f"タイトル: {book['title']}")
print(f"著者: {book['creator']}")
print(f"発行日: {book['date']}")
print("---")
重要なポイント
1. 名前空間の指定
XMLパース時に名前空間を指定しないと、要素を取得できません。必ず名前空間を定義して使用してください。
なぜ名前空間が必要か
ElementTreeは、名前空間の情報がないと、プレフィックス付きの要素(dc:titleなど)を認識できません。名前空間の辞書を渡すことで、プレフィックスとURIの対応関係をElementTreeに伝える必要があります。
# ❌ 間違い:名前空間未指定
title = item.find("dc:title") # Noneが返される
print(title) # None
# ✅ 正しい:名前空間を指定
namespaces = {
'dc': 'http://purl.org/dc/elements/1.1/',
'dcterms': 'http://purl.org/dc/terms/',
}
title = item.find(".//dc:title", namespaces)
if title is not None:
print(title.text) # 正しく取得できる
名前空間の定義は一度だけ
名前空間の辞書は、関数の最初で一度定義すれば、その関数内で何度でも使用できます:
def fetch_books_from_ndl(keyword):
# 名前空間は関数の最初で一度定義
namespaces = {
'dc': 'http://purl.org/dc/elements/1.1/',
'dcterms': 'http://purl.org/dc/terms/',
}
# 以降、同じnamespaces辞書を何度でも使用可能
title = item.find(".//dc:title", namespaces)
creator = item.find(".//dc:creator", namespaces)
date = item.find(".//dc:date", namespaces)
# ...
2. 複数値の扱い
著者や主題などは複数の値が存在する場合があります。findall()を使用して全て取得し、適切に結合します。
# 複数著者の取得
dc_creators = item.findall(".//dc:creator", namespaces)
creators = [c.text.strip() for c in dc_creators if c.text]
creator = " / ".join(creators) if creators else ""
3. エラーハンドリング
API呼び出し時には適切なエラーハンドリングを実装しましょう。
try:
res = requests.get(url, params=params, timeout=30)
res.raise_for_status() # HTTPエラーをチェック
# 処理...
except requests.exceptions.RequestException as e:
print(f"リクエストエラー: {e}")
except ET.ParseError as e:
print(f"XMLパースエラー: {e}")
except Exception as e:
print(f"予期しないエラー: {e}")
4. タイムアウトの設定
長時間応答がない場合に備えて、タイムアウトを設定しましょう。
res = requests.get(url, params=params, timeout=30) # 30秒でタイムアウト
5. レート制限への配慮
大量のリクエストを送信する場合は、レート制限を考慮して適切な間隔を空けましょう。
import time
# リクエスト間に1秒待機
time.sleep(1)
実際のプロジェクトでの使用例
実際のプロジェクトでは、以下のような処理を行いました:
- 年齢別の検索: 「〇〇歳の」「〇〇歳からの」というパターンで0歳から100歳まで検索
- 重複排除: タイトルベースで重複をチェック
- データの保存: 取得したデータをGoogleスプレッドシートに保存
# 年齢別に検索
for age in range(0, 101):
keywords = [f"{age}歳の", f"{age}歳からの"]
for keyword in keywords:
books = fetch_books_from_ndl(keyword)
# 処理...
まとめ
NDL APIを使用することで、国立国会図書館の豊富な書籍データにアクセスできます。この記事で紹介したポイントを押さえることで、効率的にデータを取得できるようになります。
主なポイント
- ✅ 名前空間を正しく指定する
- ✅ 複数値の要素は
findall()で取得する - ✅ エラーハンドリングを実装する
- ✅ タイムアウトを設定する
- ✅ レート制限を考慮する
NDL APIの詳細な仕様については、国立国会図書館の公式ドキュメントを参照してください。