2018年05月16日

言語処理100本ノックでPython入門 #43 - 分節の処理 & yield, any, all, slice

  
今日は、言語処理100本ノック 2015の問題43です。

■ 問題
夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をCaboChaを使って係り受け解析し,その結果をneko.txt.cabochaというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ.

43. 名詞を含む文節が動詞を含む文節に係るものを抽出
名詞を含む文節が,動詞を含む文節に係るとき,これらをタブ区切り形式で抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

■ Pythonのコード
import re
import functools
#import itertools

class Morph:
    def __init__(self, surface, base, pos, pos1):
        self.surface = surface
        self.base = base
        self.pos = pos
        self.pos1 = pos1

    def toList(self):
        return [self.surface, self.base, self.pos, self.pos1]

class Chunk:
    def __init__(self, number, dst):
        self.number = number
        self.morphs = []
        self.dst = dst
        self.srcs = []

    def print(self):
        print(self.number)
        print([x.toList() for x in self.morphs])
        print(self.dst, self.srcs)
        print()

    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')

def analyze():
    article = []
    sentence = []
    chunk = None
    with open('chap05/neko.txt.cabocha', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n| ', line)
            if line[0] == '*':
                num = int(words[1])
                destNo = int(words[2].rstrip('D'))
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    for index, c in enumerate(sentence, 0):
                        sentence[c.dst].srcs.append(index)
                    article.append(sentence)
                sentence = []
            else:
                chunk.morphs.append(Morph(
                    words[0],
                    words[7],
                    words[1],
                    words[2],
                ))
    return article


# chunkの中に、posで指定した品詞が含まれているかを確かめる
def contains(chunk, pos):
    return any(m.pos == pos for m in chunk.morphs)

## 名詞を含んだ文節が、動詞を含んだ文節に係るものを抜き出す。
def extract(article):
    for sentence in article:
        for chunk in sentence:
            if chunk.dst >= 0 and contains(chunk, '名詞'):
                target = sentence[chunk.dst]
                if contains(target, '動詞'):
                    yield chunk, target

## nounとverbをタブ区切りで表示する
def printPairs(wr, noun, verb):
    s = noun.concatMorphs()
    if s != '':
        t = verb.concatMorphs()
        wr.write("{}\t{}\n".format(s, t))

def main():
    article = analyze()
    with open('result43.txt', 'w', encoding='utf8') as w:
        for noun, verb in extract(article):
            printPairs(w, noun, verb)

if __name__ == '__main__':
    main()

ソースコードは、GitHubで公開しています。

■ yieldとfor文

Morphクラス、Chunkクラスは変更なし、analyze関数も変更なし。

extract関数は、yieldで値を列挙しています。 pythonではジェネレータというらしいです。

extract関数とprintPair関数を定義した後、まず確認の為に先頭の20個だけを表示させようとして以下のように書きました。
for noun, verb in extract(article)[0:20]:
    printPairs(noun, verb)

でも、実行させたら、エラーになりました(T T)
TypeError: 'generator' object is not subscriptable

いったん、リストにしないとダメみたいです。(最終的には全部を表示しているのでlist()は使ってないです)
for noun, verb in list(extract(article))[0:20]:
    printPairs(noun, verb)

それと、C#のLINQみたいに、take使えればいいのになーと思いますね。

■ any関数

extract関数の中では、contains関数を呼び出しています。このcontains関数は初めは以下のように書きました。chunkの中に指定した品詞があるかどうかを調べる関数です。
def contains(chunk, pos):
    for m in chunk.morphs:
        if m.pos == pos:
            return True
    return False

C#やっている僕としてはちょっとこのコードは泥臭く感じます。C#ならば、Any使うところです。
Pythonにはany無いのかなーって調べたら、Pythonにもanyがありました。
def contains(chunk, pos):
    return any(m.pos == pos for m in chunk.morphs)

以下のようにも書けるけど、こちらはいったんリストに変換されるのであまり良くないと思います。確信は無いですが...
return any([m.pos == pos for m in chunk.morphs])

allもあるみたいです
return all(m.pos == pos for m in chunk.morphs)

■ タプルとfor文

これ書いていて、ちょっと気になったので、次のようなコードを書いてみました。
for elem in (1, 2, 3):
    print(elem)

ちゃんと繰り返されています。 あれ、よくわからなくなってきた、(1, 2, 3) ってタプルなのかな? それともシーケンス?
for elem in 1, 2, 3:
    print(elem)
上のコードでも結果は同じです。どう違うのかな?と思い、
a = (1, 2, 3)
b = 1, 2, 3
をデバッグで見てみたら、2つとも同じ構造みたいです。ということは、両方ともタプルなんんですね。

今日は、いろいろと新しいことを知ることができました。

■ islice関数

話がそれましたので、元に戻します。 anyやallがあるんなら、takeもあるんじゃね、ということで調べたら、 itertools.isliceというのがありました。
for noun, verb in itertools.islice(extract(article), 20):
と書けば、リストにしないでも、20個取り出すことができます。たぶん、こちらのほうが効率よいはずです。 でも、以下のように書けないのがとってもストレスです。
for noun, verb in extract(article).islice(20):

もちろん、最終的には、以下のように変更したので、isliceは使ってません。
for noun, verb in extract(article):
ところで、islice の 先頭のiって何?、i-sliceという意味なのかな?


itertools には、count() repeat() takewhile() dropwhile() groupby()... なんてのがあるみたいです。 必要になった時にちゃんと調べようと思います。


■ 結果

結果の先頭部分だけを示します。
どこで    生れたか
見当が    つかぬ
所で    泣いて
ニャーニャー    泣いて
いた事だけは    記憶している
吾輩は    見た
ここで    始めて
ものを    見た
あとで    聞くと
我々を    捕えて
掌に    載せられて
スーと    持ち上げられた
時    フワフワした
感じが    あったばかりである
上で    落ちついて
顔を    見たのが
ものの    見始であろう