1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで国立国会図書館(NDL)APIから書籍情報を取得する方法

1
Last updated at Posted at 2026-01-06

はじめに

国立国会図書館が提供する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)

実際のプロジェクトでの使用例

実際のプロジェクトでは、以下のような処理を行いました:

  1. 年齢別の検索: 「〇〇歳の」「〇〇歳からの」というパターンで0歳から100歳まで検索
  2. 重複排除: タイトルベースで重複をチェック
  3. データの保存: 取得したデータを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の詳細な仕様については、国立国会図書館の公式ドキュメントを参照してください。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?