概要
文章を形態素解析し、助詞などは無視して、自立語だけを抽出したくなることがよくあるので実装をまとめました。
以下などでも書いたコードのまとめ直しです。
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
方針
自立後の定義は、正式なものとは少し違いますが、ここでは非自立でない名詞、非自立でない動詞、形容詞、副詞とします。
またカタカナ1文字、ひらがな1文字、数字のみの形態素は、形態素解析のミスの可能性が高くノイズになりがちなので、抽出しないようにします。
またキーワードマッチング等での利用を考慮し、なるべく原型を抽出するようにします。原型を取得できない単語は表層型で代用します。
実装
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-90-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
#カナやアルファベット1文字や数字出ないかの判定
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を宣言するようにしています。
またカナやアルファベットの1文字や数字のみの形態素を除外するための正規表現パターンをコンパイルして用意しておきます。
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-90-9]+")
MeCabの行のオブジェクト化
筆者の環境で標準辞書としているNEologdによるMeCabの出力は形態素ごとに
来る\t動詞,自立,*,*,カ変・来ル,基本形,来る,クル,クル
のようになっていて少し扱いにくいので、以下のように辞書型に変換します。
{'surface_form': '来る',
'pos': '動詞',
'pos_detail_1': '自立',
'pos_detail_2': '*',
'pos_detail_3': '*',
'conjugated_type': 'カ変・来ル',
'conjugated_form': '基本形',
'basic_form': '来る',
'reading': 'クル',
'pronunciation': 'クル'}
辞書型のキーは関数をまたいで使用するので、参照しやすいように、インナークラスの静的プラパティとして定義しておきます。
用いる辞書(unidic-liteなど)によっては行の出力形式が変わってきますので、環境に合わせてmecabLineToDictの関数を編集してください。
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
カナやアルファベットの1文字や数字かどうかの判定
カナやアルファベットの1文字や数字のみの形態素は形態素解析の失敗の可能性が比較的高く、自立語として扱わないほうが都合がいいことが多い気がするので、正規表現のfullmatchでそのような形態素を判定する関数を作っておきます。
#カナやアルファベット1文字や数字出ないかの判定
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