2018年05月13日

言語処理100本ノックでPython入門 #42 - 係り元と係り先の文節 & 高階関数

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

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

42. 係り元と係り先の文節の表示
係り元の文節と係り先の文節のテキストをタブ区切り形式ですべて抽出せよ.ただし,句読点などの記号は出力しないようにせよ.

■ Pythonのコード

まずは、最終的なPythonのコードを示します。

import re
import functools

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('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


## sentence内の係り元と係り先の対をタブ区切りで表示する
def printPairs(wr, sentence):
    for chunk in sentence:
        if chunk.dst == -1:
            continue
        s = chunk.concatMorphs()
        if s != '':
            t = sentence[chunk.dst].concatMorphs()
            wr.write("{s}\t{t}\n".format(s=s, t=t))

def main():
    article = analyze()
    with open('result42.txt', 'w', encoding='utf8') as w:
        for sentence in article:
            printPairs(w, sentence)

if __name__ == '__main__':
    main()

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

■ コードの解説

Morphクラス、Chunkクラス、analyzeは前回と同じまま。

新たにprintPairs関数を定義。この関数は1文の中の係り元の文節と係り先の文節のテキストをタブ区切りで表示するもの。

それと、Chunkクラスに concatMorphsメソッドを定義。これは、morphsの中のsurfaceを連結するメソッドです。 久しぶりに、formatメソッド使ってみました。
wr.write("{}\t{}\n".format(s, t))
なんと、{}の中を省略すると、引数の順番と一致するんですね。 知らなかった。 で

も、C#の挿入文字列に慣れている身としては、やはり違和感があるなー。 以下のようにC#みたいに書ければ良いんだけど。
wr.write($"{s}\t{t}")

それと、concatMorphsは、最初は以下のように書きました。ただ、ちょっと冗長です。
def concatMorphs(self):
    s = ''
    for m in self.morphs:
        if m.pos == '記号':
            continue
        s += m.surface
    return s
別の書き方がないのかなと思って調べたら、Pythonには高階関数というのがあるらしいので、使ってみました。(これが採用したコード)
import functools
    ……
    def concatMorphs(self):
        seq = filter(lambda x: x.pos != '記号', self.morphs)
        return functools.reduce(lambda x, y: x + y.surface, seq, '')

reduce関数は、C#のAggregateと同じものですね。
reduce使うには、functoolsというのをimportしなくてはいけないみたいです。はっきりいって、あまりうれしくないです。

reduce使うのやめて、以下のように書きかたも検討しましたが... せっかっくなので、reduce版を採用。
def concatMorphs(self):
    s = ''
    for m in filter(lambda x: x.pos != '記号', self.morphs):
        s += m.surface
    return s

以下のようなアロー式使えればもう少し書きやすくなるのになー。
for m in filter(x => x.pos != '記号', self.morphs):
    s += m.surface

■ 結果

出力ファイルの先頭部分だけをここに掲載
吾輩は    猫である
名前は    無い
まだ    無い
どこで    生れたか
生れたか    つかぬ
とんと    つかぬ
見当が    つかぬ
何でも    薄暗い
薄暗い    所で
じめじめした    所で
所で    泣いて
ニャーニャー    泣いて
泣いて    記憶している
いた事だけは    記憶している
吾輩は    見た
ここで    始めて
始めて    人間という
人間という    ものを
ものを    見た
しかも    種族であったそうだ
あとで    聞くと
聞くと    種族であったそうだ
それは    種族であったそうだ
書生という    人間中で
人間中で    種族であったそうだ
一番    獰悪な
獰悪な    種族であったそうだ