まったりいんふぉまてぃくすめもらんだむ

主にプログラミング関係のメモに使うかもしれないしそうでないかもしれない

NewYorkTimes APIを使ってみる

ボスからNYTのAPIを使ってちょっと遊んでみ、という課題を仰せつかったのでTimes Developer Network - APIsのクッソ長いドキュメント見ながらがんばって使ってみた。
ということで以下APIの使い方などに関するメモ。必要なとこだけ流し読みしながらなのでいろいろ不備があるかもしれないのはいつものことです。

The Article Search API

NYT APIは色々種類があるけど、今回使用したのは記事検索を行うコイツ。URI送るとレスポンスがJSONで返ってくる。

アクセス制限はAPIのドキュメントには1日5000リクエスト、APIキーもらうための登録ページには10000リクエスト(1秒10リクエスト)とある。どっちだよ。

URIの構造はこんな感じ。

http://api.nytimes.com/svc/search/v1/article?query=(field:)keywords 
(facet:[value])(&params)&api-key=your_API_key

クエリには普通のキーワードに加え、ファセットというNYT固有のデータを使うこともできる。説明の英語がよく分からなかったんだけど、記事についてるタグのようなもの?今回は必要なさそうなのでこれ以上は触れない。
キーワードはtitle,byline,bodyのような検索するフィールドを指定することもできる。

API-keyはもちろん事前にもらっておく。

paramsでは検索する年月日の範囲を指定したり返すフィールドを制限したりできる。

このへんでドキュメント読むの飽きたので、あとは適宜調べながら実際に使っていった。

使ってみてわかった注意点。

  • 検索結果は一度に10件しか返ってこない。

クエリにマッチした記事が例えば100件あったとしても、一回のリクエストで返ってくるのは最初の10件のみ。
そして検索結果全体のうちどこを返すかを制御してるのが、offsetというパラメータ。offsetは検索結果のページ番号のようなもので、結果が100件ある場合offsetは0〜9の値をとる。
この値をリクエストURIのオプション部分で指定してやれば、好きな部分の結果が得られるという仕組み(デフォルト値は0なので指定しなければ常に最初の10件だけが返される)。
後から知ったけどこういう仕組みは検索結果が大量になるAPIでは普通らしいね。

ということですべての検索結果を取得したければヒット数を調べてからoffsetの数だけループを回す必要があるんだなぁ。楽勝かと思われたアクセス制限数にこんな罠が。

  • 記事全文は取得できない。

レスポンスのbodyには記事の始めの部分しか入っていない。
ボスから課されたタスクは「記事中でクエリ語の前後n語以内に現れる語をすべて集めてきてそれぞれの出現数をカウントし、クエリ語と共起することが多い語を取得する」というものだったので、これを知った時点でこのタスクを遂げることは不可能になってしまいました。
取得はできないけど記事本文に対する検索は全文から行ってるよーということらしい。

まぁ何も残りませんでしたーではアレなので、プログラム例。
クエリと検索年を指定して呼び出すとまず0ページ目の検索結果を表示して、そのあとは対話環境で残りのページの内容を見るか選べる感じ。
UIはわかりやすくしたつもりだけど内部の実装はわりと汚い。

nyt.py

# -*- coding: utf-8 -*-

import sys, urllib2, json
from math import ceil

API_KEY = "Your API key"


def search(query, year):
    """[year]年の記事に対しクエリ語で検索

    """
    # フレーズ検索はスペースを+に置き換える必要がある
    url_base = "http://api.nytimes.com/svc/search/v1/article?query=body:" + \
        "{0}&begin_date={1}0101&end_date={1}1231"\
        .format(query.replace(" ", "+"), year)
    offset = 0

    def request_and_output(n):
        """オフセットを渡すとリクエストを送信し
        指定したオフセットの検索結果を取得し表示する
        返り値はページ数
        """
        url = "{0}&offset={1}&api-key={2}".format(url_base, n, API_KEY)
        page = urllib2.urlopen(url)
        j_obj = page.read()
        # JSONデータ(文字列)をPythonの辞書形式に変換する
        j_data = json.loads(j_obj.decode("utf-8"))

        # ページ数を求める(10で割って切り上げ)
        # 一回だけやればいいんだけどね懼懼
        pagenum = int(ceil(j_data["total"] / 10.0))

        # 検索結果の表示
        print "---- query \"{0}\" PAGE {1}/{2} Hits:{3} ----\n"\
            .format(j_data["tokens"][0], n+1, pagenum, j_data["total"])
        for result in j_data["results"]:
            print "--{0} : {1}".format(result["date"], result["title"])
            print "{0}\n".format(result["body"].encode("utf-8"))

        return pagenum


    while True:
        # とりあえず0ページ目を表示
        pnum = request_and_output(offset)

        # 他のページの検索結果を対話的に表示していく
        print "Current page is {0}.\nTo see next page, enter 'n'.\n"\
            .format(offset+1) + \
            "To set page number, enter integer from 1 to {0}.\n"\
            .format(pnum) + \
            "To quit search, enter 'q'."
        while True:  # 入力ループ開始
            sys.stdout.write('>')
            s = sys.stdin.readline()
            if s.strip() == "n":  # nならオフセットを漸増し入力終了
                offset += 1
                if offset >= pnum:  # オフセットが範囲外なら終了
                    return None
                else:
                    break
            elif s.strip().isdigit():  # 整数なら正しい入力かチェック
                p = int(s.strip())
                if 1 <= p <= pnum:  # オフセットの範囲内ならセットし入力終了
                    offset = p - 1
                    break
                else:  # 範囲外ならループ
                    sys.stderr.write("offset outbound.\n")
            elif s.strip() == "q":  # qなら終了
                return None
            else:  #  それ以外の入力ならループ
                sys.stderr.write("invalid input.\n")


if __name__ == "__main__":
    try:
        q = sys.argv[1]
        y = sys.argv[2]
        if not 1981 <= int(y) <= 2010:
            raise Exception
    except:
        sys.stderr.write("usage: python nyt.py [query] [year(1981-2010)]\n")
    else:
        search(q, y)

出力例

$ python nyt.py Hyogo 2000
---- query "body:Hyogo" PAGE 1/1 Hits:3 ----

--20000723 : To See Japan, Try Rail Pass And Ryokan
THE most triumphant moment of the whole trip to Japan occurred on the grounds of the stupendous 
16th century ''white heron'' castle of Himeji. There, with the castle towering above us, we 
encountered three men dressed as samurai, and my 4-year-old son, Anatol, knew his Japanese dreams 
had come true. Using gestures, the three samurai invited the

--20000610 : WORLD BUSINESS BRIEFING: ASIA; JAPANESE BANK DEAL
Japan's fifth-largest lender, Sakura Bank Ltd., offered to buy a controlling stake in a small 
rival, Minato Bank Ltd., to strengthen its position in the country's second-largest metropolitan 
area. Sakura said it would buy as many as 142 million Minato shares for 240 yen each, or 34 
billion yen ($325 million). The acquisition of the Kobe-based

--20000414 : Lawmakers In Japan Hear Grim Sex Case
In a jolt to the entertainment industry, a lawmaker peppered government authorities today with 
questions about how they had dealt with accusations that Japan's leading talent agent had sexually 
abused teenage boys he was grooming for stardom. In a riveting question and answer session in 
Parliament, Yoshihide Sakaue of the governing Liberal

Current page is 1.
To see next page, enter 'n'.
To set page number, enter integer from 1 to 1.
To quit search, enter 'q'.
>

まぁAPIの使い方について勉強するという点ではよかったです。

追記

APIから直接記事全文を取得することはできないけど各記事のURLが取得できるので、それを使えば改めて記事のページから全文を取得できるんじゃね、というアドバイスを頂いた。そうだね。