2018年06月21日

言語処理100本ノックでPython入門 #53 - Stanford Core NLPの出力から単語抽出


今日は、言語処理100本ノック 2015の第6章・問題53です。

■ 問題 
53. Tokenization
Stanford Core NLPを用い,入力テキストの解析結果をXML形式で得よ.また,このXMLファイルを読み込み,入力テキストを1行1単語の形式で出力せよ.

■ Stanford Core NLPをダウンロード


以下のURLからStanford Core NLPをダウンロードします。

https://stanfordnlp.github.io/CoreNLP/


これを、corenlpというフォルダに配置します。

■ Stanford Core NLPを実行

以下のようなコマンドを投入し、Stanford Core NLPを起動します。
java -cp "/Users/xxxxxxx/corenlp/*" -Xmx3g edu.stanford.nlp.pipeline.StanfordCoreNLP -annotators tokenize,ssplit,pos,lemma,ner -file nlp.txt

これで、nlp.txt.xmlファイルが作成されます。

ちなみに、Javaのバージョンは、java version "1.8.0_101" です。

でも、作成したnlp.txt.xml見ると文の判断が正しくないので、入力ファイルを手で修正。
入力ファイルは、基本1行1文なのだけれど、Stanford Core NLPは、ピリオドが行の単位と認識するようなので、タイトル行の最後にもピリオドを付加しています。4か所くらいあったかな。

得られたXMLファイルの先頭部分を載せておきます。
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="CoreNLP-to-HTML.xsl" type="text/xsl"?>
<root>
  <document>
    <docId>nlp.txt</docId>
    <sentences>
      <sentence id="1">
        <tokens>
          <token id="1">
            <word>Natural</word>
            <lemma>natural</lemma>
            <CharacterOffsetBegin>0</CharacterOffsetBegin>
            <CharacterOffsetEnd>7</CharacterOffsetEnd>
            <POS>JJ</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>
          <token id="2">
            <word>language</word>
            <lemma>language</lemma>
            <CharacterOffsetBegin>8</CharacterOffsetBegin>
            <CharacterOffsetEnd>16</CharacterOffsetEnd>
            <POS>NN</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>
          <token id="3">
            <word>processing</word>
            <lemma>processing</lemma>
            <CharacterOffsetBegin>17</CharacterOffsetBegin>
            <CharacterOffsetEnd>27</CharacterOffsetEnd>
            <POS>NN</POS>
            <NER>O</NER>
            <Speaker>PER0</Speaker>
          </token>

■ XMLファイルを操作する

XMLファイルを読み込むには、xml.etreeライブラリを使います。
from xml.etree import ElementTree
次に、入力ファイルを指定し、nlp.txt.xmlをパースします。
xdoc = ElementTree.parse('nlp.txt.xml')
これで、読み込んだ結果がtree構造として返されます。

続いて、ルートを取得します。
root = xdoc.getroot()
このrootを使い、必要な要素を取り出していきます。
sentences = root.find('document/sentences')
で、rootの直下から、パスを指定してXML elementを取得。

さらに、
for e in sentences.findall('sentence/tokens/token/word'):
        yield e
で、word要素をすべて取り出します。 実際のテキストは、
e.text
で取得できます。


■ Pythonのコード

作成したPythonのコードです。
from xml.etree import ElementTree

def getWords():
    xdoc = ElementTree.parse('nlp.txt.xml')
    root = xdoc.getroot()
    sentences = root.find('document/sentences')
    for e in sentences.findall('sentence/tokens/token/word'):
        yield e


def main():
    with open('result53.txt', 'w', encoding='utf8') as w:
        for word in getWords():
            w.write(f'{word.text}\n')
       
if __name__ == '__main__':
    main()


ソースはGitHubでも公開しています。

■ 結果

結果の先頭部分を載せます。
ピリオドやカンマも抽出しています。本来は、これらは除外すべきかもしれませんが良しとします。
Natural
language
processing
.
From
Wikipedia
,
the
free
encyclopedia
.
Natural
language
processing
-LRB-
NLP
-RRB-
is
a
field
of
computer
science
,
artificial
intelligence
,
and
linguistics
concerned
  

Posted by gushwell at 07:40Comments(0)Python

2018年06月17日

言語処理100本ノックでPython入門 #52 - nltkでステミング


今日は、言語処理100本ノック 2015の第6章・問題52を解きます。

■ 問題
52. ステミング
51の出力を入力として受け取り,Porterのステミングアルゴリズムを適用し,単語と語幹をタブ区切り形式で出力せよ. Pythonでは,Porterのステミングアルゴリズムの実装としてstemmingモジュールを利用するとよい.

■ nltkライブラリのstemモジュールを使う


ステミングとは、単語の変化形の変化した部分を取り除く処理のことらしいです。

問題に、「stemmingモジュールを利用するとよい」とあるので、以下のようなコマンドを投入。
conda install stemming
でも、以下のエラーが出て見つかりません。
Solving environment: failed

PackagesNotFoundError: The following packages are not available from current channels:

  - stemming
そもそもこのstemmingライブラリは、python3.xに非対応らしいです。

python3.xで利用できるステミングモジュールはないかな、って調べたら、nltk というパッケージでstemmingができるらしいです。

僕が利用しているpythonのanacondaは、デフォルトでいろんなライブラリが入っているので、まずは、anacondaにあるかどうか調べます。
conda list
とやったら、
nltk                      3.2.2                    py36_0
と出てきました。これを使おうとおもいます。

from nltk import stem
で stem モジュールをimportします。

でも、pylintが以下のようなエラーを吐きます。
F0002:<class 'AttributeError'>: 'TreeRebuilder3k' object has no attribute 'visit_joinedstr'
どうも、pylintが古いみたいです。

無視してもいいのですが、せっかくなので、pylintを新しくします。
conda install -c anaconda pylint
無事、エラーが消えました。


では、stemを使って、Porterのステミングアルゴリズムで語幹を取り出します。
stemmer = stem.PorterStemmer()
stm = stemmer.stem(word)
とすれば、語幹が得られます。stemとは語幹という意味です。

ちなみに、
stemmer = stem.LancasterStemmer()
とすれば、LancasterStemmerという別のアルゴリズムも使えるみたいです。


■ Pythonのコード


以下、出来上がったPythonのコードを載せます。
from nltk import stem

def enumStem():
    stemmer = stem.PorterStemmer()
    #stemmer = stem.LancasterStemmer()
    with open('result51.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            word = line.rstrip('\n')
            if word != '':
                yield word, stemmer.stem(word)

def main():
    with open('result52.txt', 'w', encoding='utf8') as w:
        for word, stm in enumStem():
            w.write(f'{word}\t{stm}\n')

if __name__ == '__main__':
    main()


ソースはGitHubでも公開しています。


そうだ、書き忘れていたけど、Pythonでも、C#の逐語的リテラル文字列のような書き方ができるようになったんですね。

f'{word}\t{stm}\n' 

便利になりました。


■ 結果
Natural	natur
language	languag
processing	process
From	from
Wikipedia	wikipedia
the	the
free	free
encyclopedia	encyclopedia
Natural	natur
language	languag
processing	process
NLP	nlp
is	is
a	a
field	field
of	of
computer	comput
science	scienc
artificial	artifici
intelligence	intellig
and	and
linguistics	linguist
concerned	concern
with	with
the	the
interactions	interact
between	between
computers	comput
and	and
human	human
... 以下省略
  
Posted by gushwell at 22:30Comments(0)Python

2018年06月12日

言語処理100本ノックでPython入門 #51 - 英文テキスト 単語の切り出し



今日は、言語処理100本ノック 2015の第6章・問題51を解きます。

■ 問題

51. 単語の切り出し
空白を単語の区切りとみなし,50の出力を入力として受け取り,1行1単語の形式で出力せよ.ただし,文の終端では空行を出力せよ.


■ Pythonのコード
import re

def enumWords():
    with open('result50.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'[\s\.",:;()]+', line)
            for w in words:
                if re.match(r'^[a-zA-Z]', w):
                    yield w

def main():
    with open('result51.txt', 'w', encoding='utf8') as w:
        w.writelines([x + '\n' for x in enumWords()])

if __name__ == '__main__':
    main()


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


■ ちょっと説明

単純に空白を区切り文字としてプログラム書いて実行して結果をみると、出力ファイルの中に以下のような内容がありました。
In
1950,
Alan
Turing
published
an
article
titled
"Computing
Machinery
and
Intelligence"
which
...

なんか、常識的に考えておかしいです。
「空白を単語の区切り」とあるけど、, " ( ) ? などの記号をどうしたらよいかは、問題読んでもよくわからないんですよね。

ということで、単語の区切りとなりそうな空白以外の記号も区切り記号として書き直したのが上のコードです。

それと、今回初めて、writelinesメソッド使ってみました。これで一気に複数行をファイルに出力できます。 でも、引数の配列の各行の最後に改行入れないといけないのが面倒です。
with open('result51.txt', 'w', encoding='utf8') as w:
    w.writelines([x + '\n' for x in enumWords()])

■ 結果

先頭の30語だけ掲載します。
Natural
language
processing
From
Wikipedia
the
free
encyclopedia
Natural
language
processing
NLP
is
a
field
of
computer
science
artificial
intelligence
and
linguistics
concerned
with
the
interactions
between
computers
and
human
  
Posted by gushwell at 08:00Comments(0)Python

2018年06月10日

言語処理100本ノックでPython入門 #50 - 英文テキスト 分区切り



本日から、言語処理100本ノック 2015の第6章に入ります。

まずは、問題50を解きます。

■ 問題
第6章: 英語テキストの処理
英語のテキスト(nlp.txt)に対して,以下の処理を実行せよ.
50. 文区切り
(. or ; or : or ? or !) → 空白文字 → 英大文字というパターンを文の区切りと見なし,入力された文書を1行1文の形式で出力せよ.

■ Pythonのコード
import re

def enumSentence():
    with open('nlp.txt', 'r', encoding='utf8') as fin:
        for line in fin:
            nl = re.sub(r'(\.|;|:|\?|!)(\s+)([A-Z])', r'\1\n\3', line)
            ss = re.split(r'\n', nl)
            for s in filter(lambda w: len(w) > 0, ss):
                yield s

def main():
    with open('result501.txt', 'w', encoding='utf8') as w:
        for sentence in enumSentence():
            w.write(sentence + '\n')

if __name__ == '__main__':
    main()

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

■ ちょっと説明

matchで1文を抜き出そうとしたのですが、文の途中に.がある場合や、引用符の中に、(. or ; or : or ? or !) → 空白文字 → 英大文字 という パターンがある場合に正規表現がうまく書けませんでした。

それなので、考えるのやめて安易な方法でやることにしました。

sub関数で文の区切りの空白を\nに置き換えて、\nでsplitして、1行を取り出すという戦術。

でも、この時、subじゃなくて、reaplceと書いてうまくいかずに、なぜ?って思ってしまいました。 substitute の略だと思うけど、どうも忘れてしまいます。 グループ化した文字列を参照するときは、\1, \2, \3 など使います。

それと、正規表現書く時は、rで始める文字列にしたほうが使いやすいですね。 


それと久しぶりにfilter使ってみました。たまに使わないと忘れてしまいそうです。
for s in filter(lambda w: len(w) > 0, ss):
    yield s
ちなみに、入力ファイルの先頭行の
Natural language processing
は、(. or ; or : or ? or !)で終わってないけど、これも文として扱うことにします。


■結果

先頭部分だけ載せます。画面だと途中で改行されてますが、ご容赦を。

Natural language processing
From Wikipedia, the free encyclopedia
Natural language processing (NLP) is a field of computer science, artificial intelligence, and linguistics concerned with the interactions between computers and human (natural) languages.
As such, NLP is related to the area of humani-computer interaction.
Many challenges in NLP involve natural language understanding, that is, enabling computers to derive meaning from human or natural language input, and others involve natural language generation.
History
The history of NLP generally starts in the 1950s, although work can be found from earlier periods.
In 1950, Alan Turing published an article titled "Computing Machinery and Intelligence" which proposed what is now called the Turing test as a criterion of intelligence.
The Georgetown experiment in 1954 involved fully automatic translation of more than sixty Russian sentences into English.
The authors claimed that within three or five years, machine translation would be a solved problem.
...
  
Posted by gushwell at 22:30Comments(0)Python

2018年06月07日

言語処理100本ノックでPython入門 #49 - 名詞間の係り受けパスの抽出



今日は、言語処理100本ノック 2015の問題49です。
「第5章:係り受け解析」最後の問題になりました。

まだ、Pythonの文法で知らないことも結構あると思いますが、それなりのコードは書けるようになったかなと思います。やりたいアルゴリズムを実装する上で、困ることはほとんどなくなったように思います。あとはいろんなライブラリの使い方を覚えて行ければ...

■ 問題
49. 名詞間の係り受けパスの抽出
文中のすべての名詞句のペアを結ぶ最短係り受けパスを抽出せよ.ただし,名詞句ペアの文節番号が iとj(i<j)のとき,係り受けパスは以下の仕様を満たすものとする.
  • 問題48と同様に,パスは開始文節から終了文節に至るまでの各文節の表現(表層形の形態素列)を"->"で連結して表現する 文節
  • iとjに含まれる名詞句はそれぞれ,XとYに置換する
また,係り受けパスの形状は,以下の2通りが考えられる.
  • 文節iから構文木の根に至る経路上に文節jが存在する場合: 文節iiから文節jjのパスを表示
  • 上記以外で,文節iiと文節jjから構文木の根に至る経路上で共通の文節kkで交わる場合: 文節iiから文節kkに至る直前のパスと文節jjから文節kに至る直前までのパス,文kkの内容を"|"で連結して表示
例えば,「吾輩はここで始めて人間というものを見た。」という文(neko.txt.cabochaの8文目)から,次のような出力が得られるはずである.
Xは | Yで -> 始めて -> 人間という -> ものを | 見た
Xは | Yという -> ものを | 見た
Xは | Yを | 見た
Xで -> 始めて -> Y
Xで -> 始めて -> 人間という -> Y
Xという -> Y

■ 新たに得たPythonの知識

Python はメソッドのオーバーロードが使えない。

intの最大値
import sys
sys.maxsize

深いコピーができる
import copy

copy.deepcopy(x)

■ 簡単な説明

Chunkクラスには、hasNounメソッドを追加。

analyze関数は今回は変更。 EOSを明確に示すために、sentenseの最後には、Chunk(sys.maxsize, -1) を追加しました。これがEOSを意味します。
 sentence.append(Chunk(sys.maxsize, -1))
これに伴い、これまでは、dst:-1の時は、次のChunkはないことを示していましたが、最後のChunkのdstは、EOSを表すChunkへの番号をセットするようにしました。
    if destNo == -1:
        destNo = num + 1
各関数の動作については、コードにコメントを付けたのでそれを読んでください。
なお、主要な変数は以下の通りです。
  • 変数ciは、文節i(Chunkクラス) を表す
  • 変数cjは、文節j(Chunkクラス) を表す
  • 変数ckは、文節k(Chunkクラス) を表す
  • 変数kは、文節kのインデックスを表す (sentenceのインデックス) 

ただ、あまり効率の良いコードとは言えないです。改良の余地hじゃありますが、まあ良しとします。


■ Pythonのコード
import re
import sys
import copy
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 hasNoun(self):
        for m in self.morphs:
            if m.pos == '名詞':
                return True
        return False

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

# 文章をsentenceに分割する。ひとつのsentenceは、Chunk(文節)の配列からなる
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'))
                if destNo == -1:
                    destNo = num + 1
                chunk = Chunk(num, destNo)
                sentence.append(chunk)
            elif words[0] == 'EOS':
                if sentence:
                    sentence.append(Chunk(sys.maxsize, -1))
                    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)のペアを列挙する
def enumPairs(sentence):
    count = len(sentence)
    for i in range(count):
        for j in range(i+1, count):
            if sentence[i].hasNoun() and sentence[j].hasNoun():
                yield sentence[i], sentence[j]

# ciからのパスの中にcjが含まれているかを調べる
def containsOther(ci, cj, sentence):
    curr = ci
    while curr.dst >= 0:
        if curr == cj:
            return True
        curr = sentence[curr.dst]
    return False

# ciからのパスとcjからのパスが出会う番号を返す
def connectedPoint(ci, cj, sentence):
    k = ci.dst
    while k >= 0:
        curr = cj
        while curr.dst >= 0:
            if curr.number == k:
                return k
            curr = sentence[curr.dst]
        k = sentence[k].dst
    return -1

# chunkの名詞の部分を to で指定した値に変更(オリジナルは変更しない)
def replaceTo(to, chunk):
    dup = copy.deepcopy(chunk)
    for m in dup.morphs:
        if m.pos == '名詞':
            m.surface = to
            break
    return dup

# 'Xで -> 始めて -> 人間という -> Y' というパス文字列を生成
def makePath1(ci, cj, sentence):
    path = []
    curr = replaceTo('X', ci)
    while curr.number < cj.number:
        path.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    path.append('Y')
    return '{}'.format(' -> '.join(path))

# 'Xは | Yという -> ものを | 見た' という形式のパス文字列を生成
def makePath2(ci, cj, ck, sentence):
    ci = replaceTo('X', ci)
    cj = replaceTo('Y', cj)
    p1 = ci.concatMorphs()
    list1 = []
    curr = cj
    while curr.number < ck.number:
        list1.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    p2 = '{}'.format(' -> '.join(list1))
    list2 = []
    curr = ck
    while curr.dst > 0:
        list2.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    p3 = '{}'.format(' -> '.join(list2))
    return '{} | {} | {}'.format(p1, p2, p3)

# 1センテンスから、パスを列挙する
def extractPaths(sentence):
    for ci, cj in enumPairs(sentence):
        if containsOther(ci, cj, sentence):
            yield makePath1(ci, cj, sentence)
        else:
            k = connectedPoint(ci, cj, sentence)
            if k > 0:
                yield makePath2(ci, cj, sentence[k], sentence)

# ファイルを読み込み、1センテンス毎に、extractPathsを呼び出し、Path(複数)を取り出し、ファイルに出力
def main():
    article = analyze()
    with open('result49.txt', 'w', encoding='utf8') as w:
        for sentence in article:
            for path in extractPaths(sentence):
                w.write(path + '\n')

if __name__ == '__main__':
    main()

ソースはGitHubでも公開しています。

■ 結果

先頭の10センテンスの結果を掲載します。
Xは -> Y
Xで | Yが | つかぬ
Xでも -> 薄暗い -> Y
Xでも | Y | 泣いて -> 記憶している
Xでも | Yだけは | 記憶している
Xでも -> 薄暗い -> 所で -> 泣いて -> Y
Xで | Y | 泣いて -> 記憶している
Xで | Yだけは | 記憶している
Xで -> 泣いて -> Y
X | Yだけは | 記憶している
X -> 泣いて -> Y
Xだけは -> Y
Xは | Yで -> 始めて -> 人間という -> ものを | 見た
Xは | Yという -> ものを | 見た
Xは | Yを | 見た
Xで -> 始めて -> Y
Xで -> 始めて -> 人間という -> Y
Xという -> Y
Xで | Yは | 種族であったそうだ
Xで | Yという -> 人間中で | 種族であったそうだ
Xで | Y中で | 種族であったそうだ
Xで | Y -> 獰悪な | 種族であったそうだ
Xで | Yな | 種族であったそうだ
Xで -> 聞くと -> Y
Xは | Yという -> 人間中で | 種族であったそうだ
Xは | Y中で | 種族であったそうだ
Xは | Y -> 獰悪な | 種族であったそうだ
Xは | Yな | 種族であったそうだ
Xは -> Y
Xという -> Y
Xという | Y -> 獰悪な | 種族であったそうだ
Xという | Yな | 種族であったそうだ
Xという -> 人間中で -> Y
X中で | Y -> 獰悪な | 種族であったそうだ
X中で | Yな | 種族であったそうだ
X中で -> Y
X -> Y
X -> 獰悪な -> Y
Xな -> Y
Xというのは | Yを -> 捕えて -> 煮て -> 食うという | 話である
Xというのは -> Y
Xを -> 捕えて -> 煮て -> 食うという -> Y
Xは | Yという -> 考も | なかったから -> 思わなかった
Xは | Yも | なかったから -> 思わなかった
Xという -> Y
Xの -> Y
Xの | Yと | 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
Xの -> 掌に -> 載せられて -> 持ち上げられた -> Y
Xの -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> Y
Xに | Yと | 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
Xに -> 載せられて -> 持ち上げられた -> Y
Xに -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> Y
Xと -> 持ち上げられた -> Y
Xと -> 持ち上げられた -> 時 -> フワフワした -> Y
X -> フワフワした -> Y
  
Posted by gushwell at 08:00Comments(0)Python

2018年06月04日

言語処理100本ノックでPython入門 #48 - 名詞から根へのパスの抽出



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

■ 問題
48. 名詞から根へのパスの抽出
文中のすべての名詞を含む文節に対し,その文節から構文木の根に至るパスを抽出せよ. ただし,構文木上のパスは以下の仕様を満たすものとする.
  • 各文節は(表層形の)形態素列で表現する 
  • パスの開始文節から終了文節に至るまで,各文節の表現を"->"で連結する 
「吾輩はここで始めて人間というものを見た」という文(neko.txt.cabochaの8文目)から,次のような出力が得られるはずである.
吾輩は -> 見た
ここで -> 始めて -> 人間という -> ものを -> 見た
人間という -> ものを -> 見た
ものを -> 見た

■ 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


def findNouns(sentence):
    for chunk in sentence:
        for m in chunk.morphs:
            if m.pos == '名詞':
                yield chunk
                break

def makePath(sentence, chunk):
    curr = sentence[chunk.number]
    path = []
    while curr.dst >= 0:
        path.append(curr.concatMorphs())
        curr = sentence[curr.dst]
    path.append(curr.concatMorphs())
    return path

def enumPath(article):
    for sentence in article:
        for chunk in findNouns(sentence):
            path = makePath(sentence, chunk)
            if len(path) >= 2:
                yield path

def main():
    article = analyze()
    with open('result48.txt', 'w', encoding='utf8') as w:
        for path in enumPath(article):
            w.write('{}\n'.format(' -> '.join(path)))

if __name__ == '__main__':
    main()

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

■ コードの簡単な説明


Morphクラス、Chunkクラス、analyze関数は変更ありません。
問題47は、結構複雑なコードで書いているうちに自分が何をやりたかったのか判らなくなることがありましたが、 この問題48は、問題47に比べて複雑さもそれほどではなかったです。

enumPath関数
analyze関数で得たarticleから、名詞を含む文節に対し,その文節から構文木の根に至るパスを抽出する関数

findNouns関数
enumPathの下請け関数
1つの文から、名詞を含む文節(Chunk)を列挙する。 makePath関数

enumPathの下請け関数
findNounsで得た文節から終了文節に至るまでのパスを作成するする関数

main関数
enumPathで得たパスを-> で連結しファイルに出力する


■ ひとり言

そういえば、初めて while 文を使ったかも。
makePathの中で使っています。while文は他の言語にもあるので、何も難しいことはないですね。

各文節を"->"で連結するのに、joinを使っていますが、いまだに、以下の書き方に慣れないです。
' -> '.join(path)

■ 結果
吾輩は -> 猫である
名前は -> 無い
どこで -> 生れたか -> つかぬ
見当が -> つかぬ
何でも -> 薄暗い -> 所で -> 泣いて -> 記憶している
所で -> 泣いて -> 記憶している
ニャーニャー -> 泣いて -> 記憶している
いた事だけは -> 記憶している
吾輩は -> 見た
ここで -> 始めて -> 人間という -> ものを -> 見た
人間という -> ものを -> 見た
ものを -> 見た
あとで -> 聞くと -> 種族であったそうだ
それは -> 種族であったそうだ
書生という -> 人間中で -> 種族であったそうだ
人間中で -> 種族であったそうだ
一番 -> 獰悪な -> 種族であったそうだ
獰悪な -> 種族であったそうだ
書生というのは -> 話である
我々を -> 捕えて -> 煮て -> 食うという -> 話である
当時は -> なかったから -> 思わなかった
何という -> 考も -> なかったから -> 思わなかった
考も -> なかったから -> 思わなかった
彼の -> 掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
掌に -> 載せられて -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
スーと -> 持ち上げられた -> 時 -> フワフワした -> 感じが -> あったばかりである
時 -> フワフワした -> 感じが -> あったばかりである
感じが -> あったばかりである
掌の -> 上で -> 落ちついて -> 見たのが -> 人間という -> ものの -> 見始であろう
  
Posted by gushwell at 07:30Comments(0)Python