銀月の符号

Python 使い見習いの日記・雑記

連想検索エンジン reflexa の API を Python から使ってみる

先日、 http://labs.preferred.jp/reflexa/ を知りまして。なかなかに面白い連想結果が得られるので楽しんでいるところです。

API が公開されていたのでさっそく Python から叩けるようにしてみました。 json を解析するため demjson モジュールに依存しています。 xml の解析には BeautifulSoup モジュールを使用していますが、こちらは無くても parse_xml 関数のみが使えないだけで他は動作します。ちなみにどちらも easy_install でインストール可能でかつ Pure Python モジュールです。作成はWindows XP 上で行いましたが、 OS 問わず動くのではないかと思います。

ソースコード

コードは今のところこうなっています。ver 0.1 は検索結果の JSON の解析に PyYAML をつかっていました。 JSONYAML のサブセットと聞いていたので。しかし 「Python」 の検索結果のひとつ 「Tcl/Tk」 のスラッシュが読めず転び、1日ですぐお蔵入りになりました。実はすごく似ているだけ、だったと。

ちなみに YAML の新版である YAML 1.2 は JSON との親和性が増して、 JSONYAML 1.2 準拠のパーサで読めるようになるそうな。でもこのとき使用した PyYAML 3.05 は当然ながら YAML 1.1 準拠なのでした。

# coding: utf-8
u"""『連想検索エンジン reflexa』(http://labs.preferred.jp/reflexa/) 
を使用する
"""

from urllib import urlencode
import urllib2
from string import Template
import demjson
try:
    import BeautifulSoup
except ImportError:
    pass

__all__ = ['reflexa_search', 'reflexa_search_raw', 'reflexa_api']
__version__ = '0.2'
__date__ = '2008-09-06'

reflexa_api = Template(
        u'http://labs.preferred.jp/reflexa/api.php?${query}'
        )

def parse_json(response):
    u"""
    Reflexa より取得した json 型データの内容を解析し1つずつ返す
    """
    # Reflexa より得られるデータは UTF-8 でエンコードされている。
    # また、 demjson は encoding 引数で文字エンコーディングを指定可能。
    for s in demjson.decode(response, encoding='utf-8'):
        yield unicode(s)

def parse_xml(response):
    u"""
    Reflexa より取得した XML 型データの内容を解析し1つずつ返す
    """
    xml = BeautifulSoup.BeautifulStoneSoup(response)
    for s in xml.words.findAll(u'word'):
        # s.string は Unicode 文字列ライクオブジェクト。
        # このままでもほぼ支障は無いが、
        # 念のため Unicode 文字列に変換しておく。
        # たとえば、そのままだと pickle することができない。
        yield unicode(s.string) 

if not globals().has_key('BeautifulSoup'):
    def _parse_xml_dummy(response):
        raise ImportError('BeautifulSoup module is not imported')
    parse_xml = _parse_xml_dummy

def _create_query(word, format=u'json'):
    u"""
    検索文字列 word からクエリー文字列を作成する。
    """
    # TODO: 『reflexa Web APIについて
    #       (http://labs.preferred.jp/reflexa/about_api.html)』
    #       にて「空白は %20 とする」とある。
    #       しかし、『連想検索エンジン reflexa について
    #       (http://labs.preferred.jp/reflexa/about.html)』
    #       では「apple フルーツ」の例にて空白が + になっている。
    #       (q=apple+%E3%83%95%E3%83%AB%E3%83%BC%E3%83%84)
    #       空白を %20 にする必要は無いのではないか?
    #       api.php と search.php とは別物なので試してみないと
    #       わからないが。
    #       追記:
    #       「apple フルーツ」で試してみたところ、どちらでも動く。
    #       要再調査。
    if not format in (u'xml', u'json'):
        raise ValueError
    q = dict(q=word.encode('utf-8'), format=format.encode('utf-8'))
    qs = urlencode(q)
    #qs = qs.replace('+', '%20')
    return qs

def _create_request(qs):
    u"""
    reflexa API テンプレートにクエリー文字列を埋め込んで
    問合せ先 URL を作成する。
    """
    url = reflexa_api.substitute(query=qs)
    request = urllib2.Request(url)
    return request

def reflexa_search_raw(word, opener=None, format=u'json'):
    u"""
    検索文字列 word を 『連想検索エンジン reflexa』 で検索する。
    戻り値は reflexa より得られた生のデータで、
    json もしくは xml フォーマットのテキスト。

    word: 検索文字列、もしくは検索文字列を含むシーケンス
    opener: urllib2.OpenerDirector オブジェクト。
            またはその類似オブジェクト。
            もしくは None 。 None の場合 urllib2.build_opener() で
            オブジェクトを作成する。
    format: 受信フォーマットの指定 ['json' or 'xml']
    """

    if not opener:
        opener = urllib2.build_opener()

    if not format in (u'json', u'xml'):
        raise ValueError

    qs = _create_query(word, format)
    request = _create_request(qs)

    reader = opener.open(request)
    response = reader.read()
    reader.close()
    return response

def reflexa_search(word, opener=None):
    u"""
    検索文字列 word を 『連想検索エンジン reflexa』 で検索し、
    連想された語をひとつずつ返す。

    word が文字列でないならば、文字列を含むシーケンスであることを
    期待して u' '.join で連結を試みる。

    word: 検索文字列、もしくは検索文字列を含むシーケンス
    opener: urllib2.OpenerDirector オブジェクト。
            またはその類似オブジェクト。
            もしくは None 。 None の場合 urllib2.build_opener() で
            オブジェクトを作成する。
    """
    if not opener:
        opener = urllib2.build_opener()

    if isinstance(word, basestring):
        pass
    else:
        word = u' '.join(word)

    response = reflexa_search_raw(word, opener, u'json')
    for s in parse_json(response):
        yield s

def main():
    import sys
    words = sys.argv[1:]

    coding = sys.getdefaultencoding()
    opener = urllib2.build_opener()
    for word in words:
        word = unicode(word, coding)
        if len(words) > 1:
            print (u'--- %s ---' % word).encode(coding, 'backslashreplace')
        for s in reflexa_search(word, opener=opener):
            # 'strict' はまずい。
            # 'u\017d'(Zにハーチェクを付した文字)
            # とか届いたことが。
            print s.encode(coding, 'backslashreplace')

if __name__ == '__main__':
    main()