キーワードを自動補完して入力できるweb検索ダイアログ【Python】【Gtk】

<a href="http://yuzugosho.blog.fc2.com/img/out-1_13-100_20140921211605e7d.gif/" target="_blank"><img src="http://blog-imgs-64.fc2.com/y/u/z/yuzugosho/out-1_13-100_20140921211605e7d.gif" alt="out-1_13-100_20140921211605e7d.gif" border="0" width="330" height="248" /></a>



以前に，シェルスクリプトでZenityを使って，firefoxで検索を行う簡単なアプリケーションを作りました（<a title="便利な常用シェルスクリプト - web検索ダイアログfirefox-search.sh" target="_blank" href="http://yuzugosho.blog.fc2.com/blog-entry-5.html">便利な常用シェルスクリプト - web検索ダイアログfirefox-search.sh</a>）が，今回はそれをPythonに移植したものを紹介します．Gtkのお勉強を兼ねて作りました．

全体的なコンセプトはシェルスクリプトで作った時と変わりませんが，今回は履歴を活用して，打った文字の自動補完をするように改良しました．


#Gtk.EntryCompletion

gi.repositoryのGtk.EntryCompletionを用いると簡単に実装できます．

<a href="http://www.pygtk.org/pygtk2reference/class-gtkentrycompletion.html" target="_blank" title="gtk.EntryCompletion:">gtk.EntryCompletion:</a>
<a href="http://www.pygtk.org/pygtk2tutorial/sec-EntryCompletion.html" target="_blank" title="16.4. EntryCompletion Objects:">16.4. EntryCompletion Objects:</a>

ただし，これはスペースごとに区切られた単語それぞれに対して変換を行うことができないため，Githubで見つけた

<a title="https://gist.github.com/evoL/1650115" target="_blank" href="https://gist.github.com/evoL/1650115">https://gist.github.com/evoL/1650115</a>

をもとにして，スペースで区切られた単語ごとに変換を行うように，ルールを変更してやりました．evoLさんありがとうございます．ちなみに，上のサイトの50行目の

<pre class="brush: py first-line:49"> # add the matching word
current_text = "%s %s" % (current_text, model[iter][0])</pre>
は削除する必要があります．これがあると，途中まで打って補完したキーワードが二重に入力されることになります．

（"ubu"まで打って，"ubuntu"を選択すると"ubuntu ubuntu"と入力されてしまう）

#構成等

また，今回はよりアプリケーションらしくするために，ファイルを分けて，設定は別ファイルで管理できるようにしてみました．全体の構成としては

- websearch.py
- websearch
    - \_\_init\_\_.py
    - config.py
    - prediction.py

のようになっていて，ここのconfig.pyを書き換えることでさまざまなブラウザで使うことができ，またオプションで指定するサイトの変更や追加も簡単にできます．\_\_init\_\_.pyはモジュールとして扱うために必要なだけのファイルで，今回は中身は空です．バージョンチェックなんかはここで処理した方が良いようですね．

また，最近Gitの簡単な使い方を勉強したので，GitHubにコードをあげてみました．

<a href="http://github.com/ssh0/web_search.git" target="_blank" title="http://github.com/ssh0/web_search">http://github.com/ssh0/web_search</a>
clone，forkして頂いてもOKです．

#使用感

websearch.pyを実行すると，

<a href="http://yuzugosho.blog.fc2.com/img/Selection_005_20140921203310ded.png/" target="_blank"><img src="http://blog-imgs-64.fc2.com/y/u/z/yuzugosho/Selection_005_20140921203310ded.png" alt="Selection_005_20140921203310ded.png" border="0" width="359" height="240" /></a>

のようなシンプルなエントリーボックスがでて，そして文字を入力していくと記事冒頭のような挙動をします．入力が完了したらEnterキーを押せば，config.pyで指定したブラウザで，新しいタブに検索結果が表示されます．

#課題

今後は，一般的なwebの検索バーのように，単語の関連度から関連する単語を予測して表示する機能をつけたいです．それからタイプミスが激しいので（今回こんなのを作ったのもそれが一つの理由），ミスしたものは削除するとかの機能もほしいですね．Google先生の検索エンジンをそのまま借りる方法もありますが．それから，シェルの機能とかは簡単に実装できそうです．使い方としてはファイル検索とかでしょうか？Ubuntuのデスクトップ環境であるUnityのレンズでも同じことができますが，重くなってしまうので今は機能を切っているんですよね．locateを実行するだけの簡単なものだったら，こっちに組み込んだほうが軽くて快適かもしれません．

ともかく，今まで役に立っていなかった自分の検索履歴が，役に立つ日がやってきたというわけです．

それに，Gtkでも何か作れそうだなぁという感覚もついてきました．今後もいろいろ触ってみたいですね．

#コード全体

最後にコードを晒します．解説は，コメントを見ればだいたい何やってるか解ると思います．それから上にあげた公式のreferenceなんかを参考にしてください．アドバイス，ツッコミ等あればおねがいします．


```python:websearch.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, September 2014
#
# NOTE: bug: This code doesn't run correctly when the browser hasn't run.

from gi.repository import Gtk
import os
import commands
import sys

import websearch.config as config
import websearch.prediction as prediction
logfile = config.logfile


class Window(Gtk.Window):

    def __init__(self):
        self.window = Gtk.Window.__init__(self, title="search")

        # list data for completion from logfile
        lists = []
        with open(logfile) as f:
            for s in f.readlines():
                lists += unicode(s, 'utf-8').split()[1:]
        lists = set(lists)
        liststore = Gtk.ListStore(str)
        for match in lists:
            liststore.append([match])

        self.entry = prediction.EntryMultiCompletion()
        self.entry.completion.set_model(liststore)
        self.entry.completion.set_text_column(0)
        self.entry.completion.set_popup_completion(popup_completion=True)
        self.entry.connect("activate", self.enter_callback)
        self.add(self.entry)

    def enter_callback(self, event):
        # get text from entry widget
        search_term = self.entry.get_text().split()

        # if text is None, do nothing
        if len(search_term) == 0:
            return 0

        # in config.py, site must be dictionary that
        # key is option argument and value is website's address
        site = config.site

        # find option
        option= search_term[0]
        if option in site:
            goto = site[option]
            del search_term[0]
            if len(search_term) == 0:
                return 0
        # if there is no option, go to default site
        else:
            goto = site['default-search']

        # search term are joined mostly with '+'
        if len(search_term) > 1:
            t = ' '.join(search_term)
        else:
            t = search_term[0]

        # save the log to logfile
        date = commands.getoutput('date +%F_%T')
        log =  date + ' ' + t + '\n'
        with open(logfile, 'a') as l:
            l.writelines(log)

        # go to website
        base = config.browser['default']
        goto = goto % t
        os.system(base + '"' + goto + '"')
        sys.exit()


def main():
    win = Window()
    win.connect("delete-event", Gtk.main_quit)
    win.show_all()
    Gtk.main()

if __name__ == '__main__':
    main()
```


```python:config.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, September 2014

logfile = "/home/ssh0/Dropbox/log.txt"

# choose the default browser by comment out
# or you can edit to any browser
browser = {"default":
           "firefox -new-tab "
#           "chromium-browser -new-tab "
#           "google-chrome -new-tab "
#           "opera -newtab "
            ,
           }


site = {
        # default: Google検索
        "default-search": r"https://www.google.co.jp/#q=%s",

        # "w": Wikipedia
        "-w": r"https:ja.wikipedia.org/wiki/%s",

        # "n": niconico動画
        "-n": r"http://www.nicovideo.jp/search/%s",

        # "p": Google画像検索
        #"-p": r"https://www.google.com/search?q=%s&um=1&ie=UTF-8&hl=ja&tbm=isch&source=og&sa=N&tab=wi",

        # "y": Youtubeで検索
        "-y": r"http://www.youtube.com/results?search_query=%s&sm=3",

        # "rt" Yahooリアルタイム検索
        "-rt": r"http://realtime.search.yahoo.co.jp/search?p=%s&ei=UTF-8",

        # "sc" Google Scholar検索
        "-sc": r"http://scholar.google.co.jp/scholar?q=%s&hl=ja&as_sdt=0,5",

        # "-t": 翻訳
        "-t": r"http://ejje.weblio.jp/content/%s"
        }
```


```python:prediction.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# written by ssh0, September 2014
# 参考: https://gist.github.com/evoL/1650115

from gi.repository import Gtk
import config
logfile = config.logfile


class EntryMultiCompletion(Gtk.Entry):
    def __init__(self):
        Gtk.Entry.__init__(self)
        self.completion = Gtk.EntryCompletion()

        # customize the matching function to match multiple space
        # separated words
        self.completion.set_match_func(self.match_func, None)

        # handle the match-selected signal, raised when a completion
        # is selected from popup
        self.completion.connect('match-selected', self.on_completion_match)
        self.set_completion(self.completion)

    def match_func(self, completion, key_string, iter, data):
        model = self.completion.get_model()
        modelstr = model[iter][0]

        # check if the user has typed in a space char,
        # get the last word and check if it matches something
        if ' ' in key_string:
            last_word = key_string.split()[-1]
            return modelstr.startswith(last_word)

        # we have only one word typed
        return modelstr.startswith(key_string)

    def on_completion_match(self, completion, model, iter):
        current_text = self.get_text()

        # if more than a word has been typed, we throw away the
        # last one because we want to replace it with the matching word
        # note: the user may have typed only a part of the entire word
        #       and so this step is necessary
        if ' ' in current_text:
            current_text = ' '.join(current_text.split()[:-1])
            print current_text
            current_text = '%s %s' % (current_text, model[iter][0])
            print current_text
        else:
            current_text = model[iter][0]
            print current_text

        # set back the whole text
        self.set_text(current_text)
        # move the cursor at the end
        self.set_position(-1)

        # stop the event propagation
        return True

```

#追記

2014/09/24:
websearch.pyで，","などを含むとちゃんと動作しないため，アドレスの両端にダブルクオーテーションをつけて渡すようにしました．これでブラウザ側で必要な文字変換を行ってくれるようになっています．