STAGの備忘録

みんなブログを書いている、書いていないのは俺だけ

英語コーパスをAzure Database for PostgreSQLに保管して、pythonからクエリを投げて例文検索するGUIを作る

動機

最近諸事情で英語の文章を書かなければいけず、いろいろ四苦八苦している。英語の文章を書くうえで、不自然でない表現を心がけなくてはいけないが、これが難しい。辞書を引くだけでは基本的に意味しか載っていないので使い方がわからない。そこで役に立つのがその単語が使われている例文を、たくさんあたってみることだ。そして、これまた諸事情により自分のPCになぜか12GBほどの英語のコーパスがあったので、このコーパスから指定した単語を含む文を抽出するプログラムを作ろうとしたのがきっかけである。

 

コーパス処理

自分が持っていた英語コーパスはtxtファイルに無造作に英語の文章が大量に入っているだけなのでまずはこれをセンテンス単位に分割した。

英文のセンテンス単位の分割だが、基本的にはピリオドで区切れば良いのだが、Mr.とかMs.はピリオドで終わってるがセンテンスは終わらないし、 他にも He quoted as saying "I have hoge.".などとピリオドで区切ればいいと言えない場面も多々ある。Natural Language Toolkitを使えばこの辺の細々した条件を(ある程度)よしなにやってくれる。  

from nltk.tokenize import sent_tokenize #文章をセンテンス単位で分割しそのリストを返す関数

さてこれでコーパスを処理すれば良いが、残念ながら自分の16GBメモリには載りそうもないので、なんとかする。色々方法はあると思うが自分はデータを分割して読み込むことにした。データ分割はLinuxコマンドの

split -l 100000 corpus_utf.txt split_data/splt_

などで適当にやる。次のコードを叩いてsentence.txtというセンテンス単位で改行されたtxtファイルを作る。

import glob
split_files=glob.glob("./split_data/*")
for file in split_files:
    with open(file) as f:
        s = f.read()
        sent_list = sent_tokenize(s)
        with open('sentence.txt', 'a') as f:
            for sent in sent_list:
                f.write("%s\n" % sent)

これで前処理は完了である。ちなみにsentence.txtは1億行ぐらいありました。

Azure Database for PostgreSQLにあげる

sentence.txtに対して直接処理するのははメモリ的にも実行時間的にも厳しいと感じた。ここでちょうどクラウドサービスとSQLに慣れておきたいと思っていたので、このファイルをAzure Database for PostgreSQLにアップロードしてSQLを叩いて結果を返してもらうことを思いつく。Azureは幸い無料試用期間があるので無料でサーバーを建てさせてもらった。Pythonからデータベースにアクセスするにはpsycopg2を使った。pycopg2ではconnectオベジェクトでデータベースにつないで、cursorオブジエクトでSQLクエリを投げることができる。使い終わったらちゃんとclose()を呼び出さないといけないなど、そのままではやや使い勝手が悪いと思ったので(connectionとかcursorとかをcloseとかを気にせず)SQLクエリを文字列として入力して結果を返す関数exec_queryを自前で作っておく。

import psycopg2
def exec_query(query):
    with psycopg2.connect(db_info) as connection:
        with connection.cursor() as cur:
            cur.execute(query)
            try:
                res=cur.fetchall()
            except :
                return None
    return res

なお接続にはAzureに設定したユーザー名やサーバー名やパスワードが要求される。上のコードでdb_infoに当たるものがそうである。実際にはdb_infoにはAzure Database for PostgreSQLの接続文字列が入っている。

exec_query("create table sentences (sentence text)")

でまずテーブルを作る。今回は各レコードにセンテンスがひとつ入ったテーブルを作るので上のようなSQLクエリを投げる。ここで型をtext型にしたのはvarchar型ではvarchar型の最大文字数を超えるセンテンスがあったためである。 ではテーブルsentencesを作ってデータをインポートしよう。しかしSQLのCOPYクエリがAzureのセキュリティの関係で使えないので(ここはかなり詰まった)pycopg2のcopy_from関数を使ってimportしていく。

exec_query("truncate table sentences")
with psycopg2.connect(db_info) as connection:
    with connection.cursor() as cur:
        f=open("sentence.txt")
        cur.copy_from(f,"sentences",sep="#")
        f.close()

これでサーバーにセンテンスがレコードとして保存されたテーブルが作成されたので後は指定したワードでSQLクエリを作ってサーバーに投げて結果を取得すればokである。 一連の処理を関数化しておく。

def search_sent(word):
    rslt=exec_query("select sentence from sentences where sentence like '% {} %'".format(word))
    print(rslt)

tkinterGUIを作る

これで例文検索はできるようになったのだが、少し寂しいのでGUIで使えるようにもうちょっといじる。目標としては f:id:QDSN:20200831223439p:plain のようにテキストボックスに入れた単語を含む例文を検索ボタンを押すことで右の大きなテキストボックスに該当単語をハイライトして例文を表示する機能を実装したい。 見栄えはどうでもいいので標準ライブラリのtkinterを使う。

import Tkinter as tk

まず検索ワードを入れる小さなテキストボックスは

txt = tk.Entry(width=20)

で作る。 右の大きなテキストボックスはネットで調べてみると次のようにtkinterのFrameクラスを継承し定義するとうまくいくという。

class SbTextFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)
        text = tk.Text(self, wrap='none', undo=True)
        x_sb = tk.Scrollbar(self, orient='horizontal')
        y_sb = tk.Scrollbar(self, orient='vertical')
        x_sb.config(command=text.xview)
        y_sb.config(command=text.yview)
        text.config(xscrollcommand=x_sb.set, yscrollcommand=y_sb.set)
        text.grid(column=0, row=0, sticky='nsew')
        x_sb.grid(column=0, row=1, sticky='ew')
        y_sb.grid(column=1, row=0, sticky='ns')
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        self.text = text
        self.x_sb = x_sb
        self.y_sb = y_sb

次にボタンを押した時の挙動を関数として記述する。search_sentの結果をtextframe.textにinsertメソッドで入力している。ハイライトをつける機能はtag機能を使って少しゴリ押しで書いた。

def get_word_and_search():
    word = txt.get()
    sents = search_sent(word)
    textframe.text.delete("1.0", "end")
    textframe.text.insert("1.0", "\n\n".join(sents))
    # 該当単語にハイライトを作る部分
    for i, sent in enumerate(sents):
        ind = 2*i+1
        s = sent.find(word)
        t = s+len(word)
        textframe.text.tag_add("highlight", str(
            ind)+"."+str(s), str(ind)+"."+str(t))
        textframe.text.tag_config("highlight", foreground="red")

後は適当にタイトルをつけたりボタンなどを配置し関数を紐付ける。

# Tkクラス生成
root = tk.Tk()
# 画面サイズ
root.geometry('1400x600')
# 画面タイトル
root.title('例文検索')
# 表示
# テキストボックス
lbl = tk.Label(text='検索ワード')
lbl.pack(padx=20, side='left')
txt.pack(padx=20, side='left')

# 検索ボタン設置
btn = tk.Button(root, text='検索', command=get_word_and_search)
btn.pack(padx=20, side='left')

# 結果出力
textframe = SbTextFrame(root)
textframe.pack(side="top", expand=True)

root.mainloop()

これで一応完成。いろんな単語を入れてみたが大体1秒以内には結果が返ってくるのでとりあえず満足。weblioの例文検索使えばよくない?