Rubyで国会図書館検索APIを使って書籍検索

##iss.ndl.go.jp/api/OpenSearch を使う

「国会図書館サーチ」サイトに「外部提供インタフェース（API）」の一覧があります。
http://iss.ndl.go.jp/information/api/ 

一覧によると検索用APIとして、<strong>SRU</strong>, <strong>SRW</strong>, <strong>OpenSearch</strong>, <strong>OpenURL</strong>, <strong>Z39.50</strong> の5種類があります。

このうち、手軽なURL(GET)で検索指定できて、結果をXMLで返してくれるのは、 <strong>SRU</strong> と <strong>OpenSearch</strong> です。

[外部提供インタフェース仕様書 ](http://iss.ndl.go.jp/information/wp-content/uploads/2018/09/ndlsearch_api_20180925_jp.pdf "外部提供インタフェース仕様書 ")には具体的な説明しかないですが、この二つの大雑把な違いは <strong>SUR</strong> の方がより詳細な検索対象項目と検索結果項目であるのに対し、 <strong>OpenSearch</strong> は取り回せる項目は少ないものの、例えば検索ワードの指定に曖昧さが許容されるなど柔軟性があるようです（後述）。

しかし意外なのですが、書籍の発行日付の情報である <strong>pubDateタグ</strong> のデータは <strong>OpenSearch</strong> でしか取得できないようです。同じ発行日付を示す <strong>issuedタグ</strong> または <strong>W3CDTF</strong> という別のタグもあるのですが、これは出版年だけしかないものがあります（多分登録が古いデータにはこの傾向があるみたいです）。

つまり、<strong>SRU</strong> では <strong>issuedタグ</strong>で発行年しかわからない書籍でも、<strong>OpenSearch</strong> には <strong>pubDateタグ</strong>データがあるので年月日まで取得できます。

しかも、<strong>pubDateタグ</strong>はRFC2822の記法なので、そのままTime.parse()できます。なぜ<strong>OpenSearch</strong> の方にだけこんな「おまけ」があるのか謎です。（もしかしたら、間違っているかもしれません。ご指摘ください。）

###FaradyとNokogiriを使った取得サンプル
GitHUBのGistにClass化にしたサンプルをあげました。下記URLです。
https://gist.github.com/smallmake/f3d71d6769cbf09179b86d1f0ddbdba0
以下その説明です。

単純に、書籍タイトルと著者（オプション）で検索して、取得したXMLを整形してbook[]というハッシュにして、dataという配列にするものを作ってみました。

まず、Faradyをつかったgetを用意します。

```ruby:sample1.rb
require "faraday"

def ndl_get(path, pram)
  con = Faraday.new(:url => 'http://iss.ndl.go.jp') do |f|
    f.request  :url_encoded
    f.response :logger
    f.adapter  Faraday.default_adapter
  end
  con.get path, pram
end
```
これを使って、Nokogiriでとりあえず取得したXMLから書籍情報だけを取り出せるようにします。

```ruby:sample2.rb
require 'nokogiri'

def get_book_info(title, creator = nil)
    query = {
      :mediatype => 1,
      :cnt => 10
    }
    query[:title] = title
    query[:creator] = creator if creator
    response = ndl_get('/api/opensearch', query)
    xml = Nokogiri::XML(response.body)
    xml.remove_namespaces!
    items = xml.xpath('/rss/channel/item')
```
<strong>mediatype</strong> は
1：本
2：記事・論文
3：新聞
4：児童書
5：レファレンス情報
6：デジタル資料
7：その他
8：障害者向け資料（障害者向け検索対象資料）
9：立法情報
です。ここでは「本」を指定。
<strong>cnt</strong> は取得する件数の指定です。

itmesに検索結果の複数の書籍データが入りますので each で個々に取り出します。

###typeアトリビュートの取り出し

基本的には上の続きで

```ruby:sample3.rb
items.each do |item|
  item.children.each do |c|
    key = c.name
    val = "#{c.content}"
```
のようにして、key と val が取り出せるので、単純にそれでハッシュbook[]を作ればいいのですが、少し追加した処理について以下に説明します。

XMLを読むと、
<strong>identifierタグ</strong>にはtypeアトリビュートとして <strong>ISBN, NDLBibID, JPNO, TOHANMARCNO</strong> などの重要な書籍コードがあり、
<strong>subjectタグ</strong>にはtypeアトリビュートとして <strong>NDC10, NDC9</strong> などの図書分類コードが、
発行日を示す<strong>issuedタグ</strong>にはtypeアトリビュートとして（なぜかW3Cの日付フォーマット名である）<strong>W3CDTF</strong>があります。


```xml:
<identifier type="dcndl:ISBN">9784041055977</identifier>
<identifier type="dcndl:NDLBibID">028826826</identifier>
<identifier type="dcndl:JPNO">23028965</identifier>
<identifier type="dcndl:TOHANMARCNO">33728988</identifier>
<subject type="dcndl:NDLC">KS152</subject>
<subject type="dcndl:NDC10">933.7</subject>
<subject type="dcndl:NDC9">933.7</subject>
<issued type="dcterms:W3CDTF">2018</issued>
```

これらもハッシュとして展開するために以下のように typeアトリビュートがあるときにハッシュに展開するようにします。

```ruby:sample4.rb
items.each do |item|
  book = {}
  item.children.each do |c|
    key = c.name
    val = "#{c.content}"
    label = c.attribute("type")
    if label
      label = "#{label}".gsub(/^dcndl:|^dcterms:/,'')
      book[label] ||= []
      book[label] << val unless book[label].include?(val)
      val = "#{label}:#{val}"
    end
    book[key] ||= []
    book[key] << val unless book[key].include?(val)
  end
```
これでtypeアトリビュートの各ハッシュができるとともに、各タグについては、まとめて配列でもつようにできます（後述で文字列に変換）。上記のXMLは以下のようになります。

```
book["identifier"] -> "ISBN:9784041055779,NDLBibID:028826825,JPNO:23028964,TOHANMARCNO:33728987"
book["ISBN"] -> "9784041055779"
book["NDLBibID"] -> "028826825"
book["JPNO"] -> "23028964"
book["TOHANMARCNO"] -> "33728987"

book["subject"] -> "NDLC:KS152,NDC10:933.7,NDC9:933.7"
book["NDLC"] -> "KS152"
book["NDC10"] -> "933.7"
book["NDC9"] -> "933.7"

book["issued"] -> "W3CDTF:2018"
book["W3CDTF"] -> "2018"
```
さて、以上でほぼ処理は終わりなのですが、以上のままだと、同じタグ名をもつ複数のデータは1つのハッシュに配列で格納されていますので、配列を文字列に一括で変換します。

```ruby:sample4.rb
book = book.map {|key,val| [key, val.join(',')]}.to_h
```

僕がやった OpenSearch を使って、書籍情報をハッシュにする処理は以上です。

##iss.ndl.go.jp/api/SRUについて少し
実は最初、<strong>SRU</strong>を使うつもりでした。
なぜなら、<strong>openSearch</strong>では、特定の1個の書籍情報を取り出すには <strong>ISBN</strong>を使うしかなかったためです。

<strong>SRU</strong>であれば、<strong>ISBN</strong>を使わなくてもほかに <strong>itemno（国立国会図書館サーチ内部での書誌のアイテム番号）</strong>というものがあるので、<strong>itemno</strong>の一覧さえ予め持っていれば、特定の1冊の書籍データを決め打ちで取得できます。<strong>openSearch</strong>では<strong>itemno</strong>での取得はできません。
 
なぜ、<strong>ISBN</strong>ではまずいかというのは個人的事情によるのですが、それは美術展の図録の書籍データが欲しかったのに、通常、図録というものは流通しないので<strong>ISBN</strong>がないためなのです。まあ、そもそもなぜ特定の1冊だけ決め打ちで取得したいのかということもあるのですが、それも個人的事情ですが割愛します。

とにかく、そのようなわけで<strong>SRU</strong>の利用についても試したので、少し以下に書いておきます。それが<strong>OpenSearch</strong>の柔軟さの説明にもなるからです。

###最低限のパラメータ
最低限ですが以下のようなことがまずあります。

・<strong>operation=searchRetrieve</strong> を必ず指定しなければなりません。
・<strong>maximumRecords=10</strong> のように<strong>openSearch</strong>の<strong>cnt</strong>にあたるものを指定（無指定だと200になる）
・<strong>recordSchema=dcndl_simple</strong> のようにスキーマを指定。dcndl_simpleが使いやすい。（無指定だとdc）
・<strong>recordPacking=xml</strong> のように指定してXMLを取得。（無指定だとstringになる）
・<strong>query</strong> 以下に検索条件を指定する（次項）
つまり、SRUでXMLのデータを10件分取得するために最低限以下のようなパラメータ指定が必要です。

```
http://iss.ndl.go.jp/api/sru?operation=searchRetrieve&maximumRecords=10&recordSchema=dcndl_simple&recordPacking=xml&query=<次項で説明>
```

###queryはひとまとめにandで接続

上記パラメータに続いて検索条件のqueryを記述するのですが、ここではURLのパラメータの"＆"は使えないので、" and "でつないで記述して1つの文字列にします。

なお、上記で説明したmediatypeもこのqueryの中で指定します。

この時、気がついたのですが、OpenSearch では、titleなどでタイトルに含まれる文字をいくつかスペース(+)で区切って指定するなど曖昧な指定ができるのですが、SRUではそれはできないようです。SRUでは複数のqueryパラメータをandでつないで列記します。

例えば、『縁起もの : 版画と絵画で楽しむ吉祥図』というタイトルを検索する場合、
OpenSearchでは 

```
/opensearch?mediatype=1&cnt=10&title=縁起もの+吉祥図
```
という指定で検索できますが、SRUではtitleを二回書きます。

```
/sru?..<既述>..&query=mediatype=1+and+title="縁起もの"+and+title="吉祥図"
```
※もちろんこれらはURLエンコードします（Faradayに任せることができます）

前に書いたget_book_info(title, creator) の例で言えば、引数のtitleはあいまいにタイトルに含む語の部分をスペースで区切ったもので指定することを許容しているので、以下のコードのように分解してand結合し、先述の最低限のパラメータと一緒に渡せばいいです。

```ruby:sample5.rb
def get_book_info(title, creator = nil)
    data = []
    query = "mediatype=1"
    title_array = title.split(' ')
    title_array.each do |t|
      query << " and title=\"#{t}\""
    end
    query << " and creator=\"#{creator}\"" if creator
    parm = {
      operation: "searchRetrieve",
      maximumRecords: 10,
      recordSchema: 'dcndl_simple',
      recordPacking: 'xml',
      query: query
    }
    response = ndl_get('api/sru', parm)
```

ついでに、書籍情報のNokogiriでの取り出しパスについては以下のようになります。

```ruby:sample6.rb
response = ndl_get('api/sru', parm)
xml = Nokogiri::XML(response.body)
xml.remove_namespaces!
records = xml.xpath('/searchRetrieveResponse/records/record/recordData/dc')
records.each do |record|
  book = {}
  record.children.each do |item|
    key = item.name
    val = "#{item.content}"
```
以上です。
![ndl_screen1.png](https://qiita-image-store.s3.amazonaws.com/0/80250/546b3018-5d6f-e834-8414-26aed3bd79e0.png)