銀月の符号

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

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

先日の連想検索エンジン reflexa API を Python から使ってみる モジュールを手直し。

使い方

reflexa_search に検索したい文字列を渡します。連想された語を Unicode 文字列で返してくるジェネレータが得られます。 for 文で回したり list で変換したりしてあげてください。

>>> from reflexa import reflexa_search
>>> for s in reflexa_search(u'Python'):
...   print s
Monty
Python
ニシキヘビ
(中略)
factorial
WideStudio
PythonLabs

スクリプトとしても使用可能です。手抜き実装ですが・・・(汗

C:\>python -m reflexa "初音ミク"
VOCALOID
クリプトン・フューチャー・メディア
(後略)

reflexa.py

ダウンロード

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

fgshun
『銀月の符号』 http://d.hatena.ne.jp/fgshun/
"""

import sys
import urllib2
from urllib import urlencode
from string import Template
from xml.parsers.expat import ParserCreate
if sys.version_info[:2] >= (2, 6):
    import json
else:
    import simplejson as json

__all__ = ['reflexa_search', 'ReflexaError']
__version__ = u'0.3.2'
__date__ = u'2009-09-19'

class ReflexaError(StandardError):
    pass

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

def reflexa_parse_json(response):
    u"""Reflexa より取得した JSON データの内容を解析し1つずつ返す"""
    # Reflexa より得られるデータは UTF-8 でエンコードされている。
    for s in json.loads(response, encoding='utf-8'):
        yield unicode(s)

def reflexa_parse_xml(response):
    u"""Reflexa より取得した XML データの内容を解析し1つずつ返す"""
    words = []
    elements = []

    def char_data(data):
        if elements == ['result', 'words', 'word']:
            words.append(data)

    def start_element(name, attrs):
        elements.append(name)

    def end_element(name):
        elements.pop()

    parser = ParserCreate()
    parser.CharacterDataHandler = char_data
    parser.StartElementHandler = start_element
    parser.EndElementHandler = end_element

    parser.Parse(response, True)
    for word in words:
        yield word

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"""問合せ先 URL を作成する

    reflexa API テンプレートにクエリー文字列を埋め込むことで行っている。
    """
    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 フォーマットのテキスト。

    Reflexa へのアクセス失敗時には ReflexaError が送出される。

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

    try:
        reader = opener.open(request)
        response = reader.read()
    except urllib2.URLError, e:
        raise ReflexaError(e)
    return response

def reflexa_search(word, opener=None):
    u"""検索文字列 word を 『連想検索エンジン reflexa』 で検索する

    連想された語をひとつずつ返す。

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

    Reflexa へのアクセス失敗時には ReflexaError が送出される。

    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 reflexa_parse_json(response):
        yield s

def main():
    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')
        try:
            for s in reflexa_search(word, opener=opener):
                # 'strict' はまずい。
                # 'u\017d'(Zにハーチェクを付した文字)
                # とか届いたことが。
                print s.encode(coding, 'backslashreplace')
        except ReflexaError, e:
            m = u'Reflexa にアクセスできませんでした'.encode(coding)
            print >> sys.stderr, m
            if __debug__:
                print >> sys.stderr, e.message

if __name__ == '__main__':
    main()

変更点

  • json を解析するためにつかうモジュールを demjson から simplejson に変更しました。このモジュールは easy_install で取ってこれるのですが、 C のコンパイル環境の無い Windows ではインストールに失敗してしまいます。この場合はソースコードをダウンロードしてきて python setup.py --without-speedups build とします。C モジュール部分を欠くと本来の速度はでませんが動作に問題は無いそうです。また Python 2.6 より json として標準モジュール入りするので将来は準備不要となります。モジュールインポートするときの if sys.version_info[:2] >= (2, 6): のイディオムはuasiの日記風より。ありがとうございます。
  • xml の解析には BeautifulSoup を使用していましたが、標準モジュール xml.sax.expat に変更しました。解析対象が簡単な XML なので BeautifulSoup の力を借りなくても無事、読めました。
  • reflexa_search, reflexa_search_raw 関数が reflexa へのアクセスを失敗した際に ReflexaError を送出するようにしました。