【Python】自立語の原型の抽出【MeCab】

#概要
文章を形態素解析し、助詞などは無視して、自立語だけを抽出したくなることがよくあるので実装をまとめました。

以下などでも書いたコードのまとめ直しです。
https://qiita.com/shimajiroxyz/items/3922d6f7dc8e4b156692

実装コードは以下にまとめています。
https://github.com/JiroShimaya/PythonModule/tree/main/IndependentWord

#環境
```
% sw_vers
ProductName:	macOS
ProductVersion:	11.4
BuildVersion:	(略)
% python -V
3.8.6
```
#方針
自立後の定義は、正式なものとは少し違いますが、ここでは非自立でない名詞、非自立でない動詞、形容詞、副詞とします。
またカタカナ１文字、ひらがな１文字、数字のみの形態素は、形態素解析のミスの可能性が高くノイズになりがちなので、抽出しないようにします。
またキーワードマッチング等での利用を考慮し、なるべく原型を抽出するようにします。原型を取得できない単語は表層型で代用します。

#実装

```Python:IndependendWord.py
import MeCab
import regex

class IndependentWord:
  def __init__(self, dictpath=None):
    if dictpath:
      self.m = MeCab.Tagger("-d "+dictpath)
    else:
      self.m = MeCab.Tagger()   
    self.kanaalpha = regex.compile(r'[\p{Script=Hiragana}\p{Script=Katakana}ーA-Za-z]')
    self.number = regex.compile("[0-9０-９]+")
  
  class Constant:
    BASIC = "basic_form" #原型
    SURFACE = "surface_form" #表層型
    POS = "pos" #品詞
    POS_DETAIL_1 = "pos_detail_1" #品詞詳細1
    POS_DETAIL_2 = "pos_detail_2" #品詞詳細2
    POS_DETAIL_3 = "pos_detail_3" #品詞詳細3
    PRONUNCIATION = "pronunciation" #発音
    READING = "reading" #読み
    CONJUGATED_TYPE = "conjugated_type" #活用
    CONJUGATED_FORM = "conjugated_form" #活用形
    
  #mecabの出力行をobjectに変換
  #mecabの出力フォーマットに応じて適宜修正する
  def mecabLineToDict(self, line):
    surface, tmp = line.split("\t")
    others = tmp.split(",")
    
    Const = self.Constant
    
    return {
      Const.SURFACE: surface,
      Const.POS: others[0],
      Const.POS_DETAIL_1: others[1],
      Const.POS_DETAIL_2: others[2],
      Const.POS_DETAIL_3: others[3],
      Const.CONJUGATED_TYPE: others[4],
      Const.CONJUGATED_FORM: others[5],
      Const.BASIC: others[6],
      Const.READING: others[7],
      Const.PRONUNCIATION: others[8]
      }
  #自立語かどうかの判定
  def isIndependentWord(self, token):
    pos = token[self.Constant.POS]
    pos_detail_1 = token[self.Constant.POS_DETAIL_1]
    if pos == "名詞" and pos_detail_1 in ['一般','固有名詞','サ変接続','形容動詞語幹']: #用途によっては「副詞可能」を足しても良いかもしれません
      return True
    elif pos == '形容詞' and pos_detail_1 == '自立':
      return True
    elif pos == "副詞" and pos_detail_1 == "一般":
      return True
    elif pos == "動詞" and pos_detail_1 == "自立":
      return True
    else:
      return False
  #カナやアルファベット１文字や数字出ないかの判定
  def isReliableWord(self, token):
    surface = token[self.Constant.SURFACE]
    if self.number.fullmatch(surface):
      return False
    elif self.kanaalpha.fullmatch(surface):
      return False
    else:
      return True
    
  #自立語の原型を抽出
  def extract(self,text):
    lines = self.m.parse(text).splitlines()[:-1]
    tokens = [self.mecabLineToDict(line) for line in lines]
    
    independent_words = []
    for token in tokens:
      if self.isIndependentWord(token) and self.isReliableWord(token):
        surface = token[self.Constant.SURFACE]
        basic = token[self.Constant.BASIC]
        if basic == "*":
          independent_words.append(surface)
        else:
          independent_words.append(basic)
    return independent_words    

if __name__ == "__main__":
  idptwd = IndependentWord()
  result = idptwd.extract("今日は来てくれてありがとう")
  print(result)
```

#解説
##コンストラクタ
mecabのTaggerオブジェクトを宣言しています。NEologd辞書などを使いたい場合を考慮して、引数に空でない文字列が指定された場合は、それを辞書としてTaggerを宣言するようにしています。
またカナやアルファベットの１文字や数字のみの形態素を除外するための正規表現パターンをコンパイルして用意しておきます。

```Python:IndependentWord.py
  def __init__(self, dictpath=None):
    if dictpath:
      self.m = MeCab.Tagger("-d "+dictpath)
    else:
      self.m = MeCab.Tagger()   
    self.kanaalpha = regex.compile(r'[\p{Script=Hiragana}\p{Script=Katakana}ーA-Za-z]+')
    self.number = regex.compile("[0-9０-９]+")
```

##MeCabの行のオブジェクト化
筆者の環境で標準辞書としているNEologdによるMeCabの出力は形態素ごとに

```
来る\t動詞,自立,*,*,カ変・来ル,基本形,来る,クル,クル
```

のようになっていて少し扱いにくいので、以下のように辞書型に変換します。

```Python
{'surface_form': '来る',
 'pos': '動詞',
 'pos_detail_1': '自立',
 'pos_detail_2': '*',
 'pos_detail_3': '*',
 'conjugated_type': 'カ変・来ル',
 'conjugated_form': '基本形',
 'basic_form': '来る',
 'reading': 'クル',
 'pronunciation': 'クル'}
```
辞書型のキーは関数をまたいで使用するので、参照しやすいように、インナークラスの静的プラパティとして定義しておきます。
用いる辞書（unidic-liteなど）によっては行の出力形式が変わってきますので、環境に合わせて`mecabLineToDict`の関数を編集してください。

```Python:IndependentWord.py
  class Constant:
    BASIC = "basic_form" #原型
    SURFACE = "surface_form" #表層型
    POS = "pos" #品詞
    POS_DETAIL_1 = "pos_detail_1" #品詞詳細1
    POS_DETAIL_2 = "pos_detail_2" #品詞詳細2
    POS_DETAIL_3 = "pos_detail_3" #品詞詳細3
    PRONUNCIATION = "pronunciation" #発音
    READING = "reading" #読み
    CONJUGATED_TYPE = "conjugated_type" #活用
    CONJUGATED_FORM = "conjugated_form" #活用形
    
  #mecabの出力行をobjectに変換
  #mecabの出力フォーマットに応じて適宜修正する
  def mecabLineToDict(self, line):
    surface, tmp = line.split("\t")
    others = tmp.split(",")
    
    Const = self.Constant
    
    return {
      Const.SURFACE: surface,
      Const.POS: others[0],
      Const.POS_DETAIL_1: others[1],
      Const.POS_DETAIL_2: others[2],
      Const.POS_DETAIL_3: others[3],
      Const.CONJUGATED_TYPE: others[4],
      Const.CONJUGATED_FORM: others[5],
      Const.BASIC: others[6],
      Const.READING: others[7],
      Const.PRONUNCIATION: others[8]
      }
```

##自立語の判定
入力された形態素が自立語であるかどうかを判定します。
この判定基準が最適とは限らないので、利用目的に合わせて判定方法は適宜編集したほうがいいかもしれません。
例えば「名詞」では「副詞可能」も自立語として扱っても良いかもしれません（「今日」「明日」「前」などが副詞可能の名詞の例です）

```Python:IndependentWord.py
  def isIndependentWord(self, token):
    pos = token[self.Constant.POS]
    pos_detail_1 = token[self.Constant.POS_DETAIL_1]
    if pos == "名詞" and pos_detail_1 in ['一般','固有名詞','サ変接続','形容動詞語幹']: #用途によっては「副詞可能」を足しても良いかもしれません
      return True
    elif pos == '形容詞' and pos_detail_1 == '自立':
      return True
    elif pos == "副詞" and pos_detail_1 == "一般":
      return True
    elif pos == "動詞" and pos_detail_1 == "自立":
      return True
    else:
      return False
```

##カナやアルファベットの１文字や数字かどうかの判定
カナやアルファベットの１文字や数字のみの形態素は形態素解析の失敗の可能性が比較的高く、自立語として扱わないほうが都合がいいことが多い気がするので、正規表現のfullmatchでそのような形態素を判定する関数を作っておきます。

```Python:IndependentWord.py
  #カナやアルファベット１文字や数字出ないかの判定
  def isReliableWord(self, token):
    surface = token[self.Constant.SURFACE]
    if self.number.fullmatch(surface):
      return False
    elif self.kanaalpha.fullmatch(surface):
      return False
    else:
      return True
```

##自立語の抽出
上記の関数を組み合わせて、日本語文から自立語のリストを返す関数を作ります。
キーワードマッチングに利用することを今回は想定して、自立語の原型を返せるときは返す処理をしています。原型が検出できないときは表層型を返しています。

```Python:IndependentWord.py
 def extract(self,text):
    lines = self.m.parse(text).splitlines()[:-1]
    tokens = [self.mecabLineToDict(line) for line in lines]
    
    independent_words = []
    for token in tokens:
      if self.isIndependentWord(token) and self.isReliableWord(token):
        surface = token[self.Constant.SURFACE]
        basic = token[self.Constant.BASIC]
        if basic == "*":
          independent_words.append(surface)
        else:
          independent_words.append(basic)
    return independent_words  
```