日本語テキストから特定単語を含む「主語・動詞（述語）・目的語」を抽出する関数


##作ったもの

係り受け解析器を用いて、テキストデータから、SVO（SPO)のTriple set（[主語, 動詞（述語）, 目的語]）を返す処理を行う実装コードを探してみたところ、なかなか見当たりませんでした。方々を探してみたところ、見つけた以下のコードがうまく動きました。

・ __[東邦大学 「2014-01-29 CaboChaによる係り受け解析の利用 ～ 文の主節の骨組を取り出す」](https://pepper.is.sci.toho-u.ac.jp/pepper/index.php?%A5%CE%A1%BC%A5%C8%2F%A5%C6%A5%AD%A5%B9%A5%C8%A5%DE%A5%A4%A5%CB%A5%F3%A5%B0%2FCaboCha%A4%C7%BC%E7%C0%E1%A4%CE%B9%FC%C1%C8%A4%F2%BC%E8%A4%EA%BD%D0%A4%B9)__

この記事では、__東邦大学の研究室が公開中の上記のコードを、表題の目的を達成する関数（メソッド）に書き換え__てみました。

書き換えたコードの全体は、__この記事の後半に掲載__しました。

___

##使い方

__主語、述語、目的語のいずれに該当する単語も、検索対象ワードに指定することができます。__

__サンプルの和文テキストは、NHK NewsWebの国際面に、2020/12/1に掲載されていた[記事](https://www3.nhk.or.jp/news/html/20201201/k10012741251000.html?utm_int=news_contents_news-main_001)__から、１文を拝借しました。

```Python:ipython対話型シェル
In [1]: from get_svo_triple_including_target_word import *

In [2]: word = 'ミサイル'

In [3]: word2 = 'テレビ局'

In [4]: sentence = "このミサイルシステムは中国国境に近いロシア極東のユダヤ自治州に配備されていたものを移送したということで、軍のテレビ局は、北方領土の択捉島でミ
   ...: サイルシステムを稼働させる映像を流しました。"

In [5]: (result1, result2) = return_svo_triple(sentence, word)

In [6]: result1
Out[6]: {'主語': 'ミサイルシステムは'}

In [7]: result2
Out[7]: ['ミサイルシステムは->流しました。']

In [8]: (result3, result4) = return_svo_triple(sentence, word2)

In [9]: result3
Out[9]: {'主語': 'テレビ局は、'}

In [10]: result4
Out[10]: ['テレビ局は、->流しました。']

In [11]: word3 = '択捉島'

In [12]: (result5, result6) = return_svo_triple(sentence, word3)

In [13]: result5
Out[13]: {}

In [14]: result6
Out[14]: []

In [15]:
```

####（ 注意点 ）

__空文字("")は、常に*True*になってしまう。__


```Python:ipython対話型シェル
In [19]: "" in "bkj"
Out[19]: True
```

```Python:ipython対話型シェル
In [15]: word_ = ""

In [16]: (result_1, result_2) = return_svo_triple(sentence, word_)

In [17]: result_1
Out[17]: {'主語': 'テレビ局は、', '目的語': '映像を', '述語': '流しました。'}

In [18]: result_2
Out[18]: ['ミサイルシステムは->流しました。', 'ことで、->流しました。', 'テレビ局は、->流しました。', '映像を->流しました。']
```

__空文字を受け取った場合、空の配列（List）を返すように、コードを書き直す必要がある。__

###（ 文が複数件、格納された配列を渡す場合 ）

__配列の中に空文字が含まれないように、事前にクレンジングを行う必要がある。__

```Python:ipython対話型シェル
In [1]: In [1]: document = """ロシア軍は、北方領土で地対空ミサイルシステム「S300」の訓練を初めて行ったと発表しました。ロシアとしては北方領土で軍備を強化している姿勢を強調するねらいがあるものとみら
   ...: れます。
   ...:    ...: 
   ...:    ...: ロシア軍の東部軍管区は1日、地対空ミサイルシステム「S300」の訓練を北方領土を含む島々で初めて行ったと発表しました。
   ...:    ...: 
   ...:    ...: このミサイルシステムは中国国境に近いロシア極東のユダヤ自治州に配備されていたものを移送したということで、軍のテレビ局は、北方領土の択捉島でミサイルシステムを稼働させる映像を流しまし
   ...: た。
   ...:    ...: 
   ...:    ...: このミサイルシステムは、射程がおよそ400キロ、戦闘機やミサイルなどを撃ち落とす対空防衛を目的としていて、島にある演習場で訓練することが目的だとしています。
   ...:    ...: 
   ...:    ...: ロシアは、択捉島と国後島には地対艦ミサイルシステムを配備していますが、「S300」の訓練を北方領土で行ったのは初めてで、ロシアとしては、北方領土で軍備を強化している姿勢を強調するねらい
   ...: があるも
   ...:    ...: のとみられます。
   ...:    ...: 
   ...:    ...: 日本政府は、ロシア側による北方領土での軍備の強化について「北方領土に関する日本の立場と相いれず受け入れられない」として繰り返し、抗議しています。"""

In [2]: document = document.replace(" ", "").replace("　", "").replace("\n", "")

In [3]: print(document)
ロシア軍は、北方領土で地対空ミサイルシステム「S300」の訓練を初めて行ったと発表しました。ロシアとしては北方領土で軍備を強化している姿勢を強調するねらいがあるものとみられます。ロシア軍の東部軍管区は1日、地対空ミサイルシステム「S300」の訓練を北方領土を含む島々で初めて行ったと発表しました。このミサイルシステムは中国国境に近いロシア極東のユダヤ自治州に配備されていたものを移送したということで、軍のテレビ局は、北方領土の択捉島でミサイルシステムを稼働させる映像を流しました。このミサイルシステムは、射程がおよそ400キロ、戦闘機やミサイルなどを撃ち落とす対空防衛を目的としていて、島にある演習場で訓練することが目的だとしています。ロシアは、択捉島と国後島には地対艦ミサイルシステムを配備していますが、「S300」の訓練を北方領土で行ったのは初めてで、ロシアとしては、北方領土で軍備を強化している姿勢を強調するねらいがあるものとみられます。日本政府は、ロシア側による北方領土での軍備の強化について「北方領土に関する日本の立場と相いれず受け入れられない」として繰り返し、抗議しています。

In [4]: sentence_list = document.split("。")

In [5]: from pprint import pprint

In [6]: pprint(len(sentence_list))
8

In [7]: pprint(sentence_list)
['ロシア軍は、北方領土で地対空ミサイルシステム「S300」の訓練を初めて行ったと発表しました',
 'ロシアとしては北方領土で軍備を強化している姿勢を強調するねらいがあるものとみられます',
 'ロシア軍の東部軍管区は1日、地対空ミサイルシステム「S300」の訓練を北方領土を含む島々で初めて行ったと発表しました',
 'このミサイルシステムは中国国境に近いロシア極東のユダヤ自治州に配備されていたものを移送したということで、軍のテレビ局は、北方領土の択捉島でミサイルシステムを稼働させる映像を流しました',
 'このミサイルシステムは、射程がおよそ400キロ、戦闘機やミサイルなどを撃ち落とす対空防衛を目的としていて、島にある演習場で訓練することが目的だとしています',
 'ロシアは、択捉島と国後島には地対艦ミサイルシステムを配備していますが、「S300」の訓練を北方領土で行ったのは初めてで、ロシアとしては、北方領土で軍備を強化している姿勢を強調するねらいがあるものとみられます',
 '日本政府は、ロシア側による北方領土での軍備の強化について「北方領土に関する日本の立場と相いれず受け入れられない」として繰り返し、抗議しています',
 '']

In [8]: word = 'ロシア'

In [9]: sentence_list = [sentence for sentence in sentence_list if not(sentence == "")]

In [10]: result_list = [return_svo_triple(sentence, word) for sentence in sentence_list]

In [11]: pprint(len(result_list))
7

In [12]: pprint(result_list)
[({'主語': 'ロシア軍は、'}, ['ロシア軍は、->発表しました']),
 ({'主語': 'ロシアとしては', '目的語': 'ロシアとしては'}, ['ロシアとしては->みられます']),
 ({}, []),
 ({}, []),
 ({}, []),
 ({'主語': 'ロシアは、'}, ['ロシアは、->みられます']),
 ({}, [])]

In [13]: word2 = "ミサイル"

In [14]: result_list2 = [return_svo_triple(sentence, word2) for sentence in sentence_list]

In [15]: pprint(result_list2)
[({}, []),
 ({}, []),
 ({}, []),
 ({'主語': 'ミサイルシステムは'}, ['ミサイルシステムは->流しました']),
 ({'主語': 'ミサイルシステムは、'}, ['ミサイルシステムは、->しています']),
 ({}, []),
 ({}, [])]

In [16]: word3 = "日本"

In [17]: result_list3 = [return_svo_triple(sentence, word3) for sentence in sentence_list]

In [18]: pprint(result_list3)
[({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({'主語': '日本政府は、'}, ['日本政府は、->抗議しています'])]

In [19]: word4 = "中国"

In [20]: result_list4 = [return_svo_triple(sentence, word4) for sentence in sentence_list]

In [21]: pprint(result_list4)
[({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, [])]

In [22]: word5 = "北方領土"

In [23]: result_list5 = [return_svo_triple(sentence, word5) for sentence in sentence_list]

In [24]: pprint(result_list5)
[({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, [])]

In [25]: result_list5 = [return_svo_triple(sentence, word5) for sentence in sentence_list]

In [26]: pprint(result_list5)
[({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, [])]

In [27]: word6 = "軍"

In [28]: result_list6 = [return_svo_triple(sentence, word6) for sentence in sentence_list]

In [29]: pprint(result_list6)
[({'主語': 'ロシア軍は、'}, ['ロシア軍は、->発表しました']),
 ({}, []),
 ({'主語': '東部軍管区は'}, ['東部軍管区は->発表しました']),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, [])]

In [30]: word7 = "政府"

In [31]: result_list7 = [return_svo_triple(sentence, word7) for sentence in sentence_list]

In [32]: pprint(result_list7)
[({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({'主語': '日本政府は、'}, ['日本政府は、->抗議しています'])]

In [33]: word8 = "射程"

In [34]: result_list8 = [return_svo_triple(sentence, word8) for sentence in sentence_list]

In [35]: pprint(result_list8)
[({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, []), ({}, [])]

In [36]: word9 = "映像"

In [37]: result_list9 = [return_svo_triple(sentence, word9) for sentence in sentence_list]

In [38]: pprint(result_list9)
[({}, []),
 ({}, []),
 ({}, []),
 ({'目的語': '映像を'}, ['映像を->流しました']),
 ({}, []),
 ({}, []),
 ({}, [])]

In [39]: word10 = "初めて"

In [40]: result_list10 = [return_svo_triple(sentence, word10) for sentence in sentence_list]

In [41]: pprint(result_list10)
[({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, ['初めてで、->みられます']),
 ({}, [])]

In [41]: word11 = "発表"

In [43]: result_list11 = [return_svo_triple(sentence, word11) for sentence in sentence_list]

In [44]: pprint(result_list11)
[({'述語': '発表しました'}, ['ロシア軍は、->発表しました', '行ったと->発表しました']),
 ({}, []),
 ({'述語': '発表しました'}, ['東部軍管区は->発表しました', '1日、->発表しました', '行ったと->発表しました']),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, [])]

In [45]: word12 = "配備"

In [46]: result_list12 = [return_svo_triple(sentence, word12) for sentence in sentence_list]

In [47]: pprint(result_list12)
[({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, []),
 ({}, ['配備していますが、->みられます']),
 ({}, [])]

In [48]: 
```

####__主語、述語、目的語のいずれに該当する単語も、検索対象ワードに指定することができることが、確認できました。__


__*any()*__に__空のlist__もしくは__空のdict__を渡すと、__False__が返ってくる。

```Python:ipython対話型シェル
In [62]: any({})
Out[62]: False

In [63]: any([])
Out[63]: False

In [64]: any(["a"])
Out[64]: True

In [65]: any({"a" : 1})
Out[65]: True
```

そのため、以下のようにすると、返り値の配列listに格納されている個々の__Tuple(dict, list)__型オブジェクトのうち、__dict部分__も__list部分__の__両方が空__のものを、__含めない__ようにできます。

```Python:ipython対話型シェル
In [48]: result_list12 = [return_svo_triple(sentence, word12) for sentence in sentence_list]

In [49]:   [result for result in result_list12 if (any(result[0]) or any(result[1]))]
Out[149]: [({}, ['配備していますが、->みられます'])]

In [50]:   [result for result in result_list7 if (any(result[0]) or any(result[1]))]
Out[50]: [({'主語': '日本政府は、'}, ['日本政府は、->抗議しています'])]
```

以下で、__ターゲット単語の出現文脈を、ターゲット単語の格（主格、目的格）を「主語」、「述語」、「目的語」で指定__できる。

その結果、__ターゲット単語が指定した格で出現する部分に絞り込んだ結果__を抽出できる。

```Python:ipython対話型シェル
In [51]: case_type_of_search_word = '主語'

In [52]:   [result for result in result_list7 if list(result[0].keys())==[case_type_of_search_word]]
Out[52]: [({'主語': '日本政府は、'}, ['日本政府は、->抗議しています'])]

In [53]: case_type_of_search_word = '述語'

In [54]:   [result for result in result_list11 if list(result[0].keys())==[case_type_of_search_word]]
Out[54]: 
[({'述語': '発表しました'}, ['ロシア軍は、->発表しました', '行ったと->発表しました']),
 ({'述語': '発表しました'}, ['東部軍管区は->発表しました', '1日、->発表しました', '行ったと->発表しました'])]

In [55]: case_type_of_search_word = '目的語'

In [56]:   [result for result in result_list9 if list(result[0].keys())==[case_type_of_search_word]]
Out[56]: [({'目的語': '映像を'}, ['映像を->流しました'])]
```

以上、__（1）探索ワード・フィルター__と、__（2）探索ワードの格フィルター__の__2つのフィルター__を実装できた。

```Python:ipython対話型シェル
In [57]: case_type_of_search_word = '目的語'
In [58]:   [dict_obj for (dict_obj, list_obj) in result_list9]
Out[58]: [{}, {}, {}, {'目的語': '映像を'}, {}, {}, {}]

In [59]:   [dict_obj for (dict_obj, list_obj) in result_list9 if list(dict_obj.keys())==[case_type_of_search_word]]
Out[59]: [{'目的語': '映像を'}]
```
___

##実装コード

```Python:get_svo_triple_including_target_word.py
import CaboCha
import sys
import codecs
# https://docs.python.org/ja/3.6/library/typing.html
from typing import Dict, Tuple, List

def return_svo_triple(sentence:str, target_word:str) -> Tuple[Dict, List]:
    c = CaboCha.Parser()
    tree = c.parse(sentence)
    size = tree.size()
    myid = 0
    ku_list = []
    ku = ''
    ku_id = 0
    ku_link = 0
    kakari_joshi = 0
    kaku_joshi = 0
    
    for i in range(0, size):
        token = tree.token(i)
        if token.chunk:
            if (ku!=''):
                ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  #前 の句をリストに追加

            kakari_joshi = 0
            kaku_joshi = 0
            ku = token.normalized_surface
            ku_id = myid
            ku_link = token.chunk.link
            myid=myid+1
        else:
            ku = ku + token.normalized_surface
        
        m = (token.feature).split(',')
        if (m[1] == u'係助詞'):
            kakari_joshi = 1
        if (m[1] == u'格助詞'):
            kaku_joshi = 1
    
    ku_list.append((ku, ku_id, ku_link, kakari_joshi, kaku_joshi))  # 最後にも前の句をリストに追加
    for k in ku_list:
        if (k[2]==-1):  # link==-1?      # 述語である
            jutsugo_id = ku_id  # この時のidを覚えておく
    #述語句
    predicate_word = [k[0] for k in ku_list if (k[1]==jutsugo_id)]
    #for k in ku_list:
    #	if (k[1]==jutsugo_id):  # jutsugo_idと同じidを持つ句を探す
    #		print(k[1], k[0], k[2], k[3], k[4])
    #述語句に係る句
    # jutsugo_idと同じidをリンク先に持つ句を探す
    word_to_predicate_list = [k[0] for k in ku_list if k[2]==jutsugo_id]
    #　述語句に係る句 -> 述語句
    svo_arrow_text_list = [str(word_to_predicate) + "->" + str(predicate_word[0]) for word_to_predicate in word_to_predicate_list]
    #print(svo_arrow_text_list)
    
    desired_svo_arrow_text = [arrow_pair_str for arrow_pair_str in svo_arrow_text_list if target_word in arrow_pair_str]
    
    svo_dict = {}
    for num, k in enumerate(ku_list):
        if (k[2]==jutsugo_id):  # jutsugo_idと同じidをリンク先に持つ句を探す
            if (k[3] == 1):
                subject_word = k[0]
                if target_word in subject_word:
                    svo_dict["主語"] = subject_word
                    #print(subject_word)
            if (k[4] == 1):
                object_word = k[0]
                if target_word in object_word:
                    svo_dict["目的語"] = object_word
                    #print(object_word)
        if (k[1] == jutsugo_id):
                predicate_word = k[0]
                if target_word in predicate_word:
                    svo_dict["述語"] = predicate_word
                #print(predicate_word)
        
    return (svo_dict, desired_svo_arrow_text)
        
```