記事の最頻出単語を調べてみよう
mecabをpythonで使う より、pythonで形態素解析ができる環境が出来たので、mecabとpythonを用いて記事や文章ファイルに出てくる単語をカウントするプログラムを作ってみました。この記事では作成したプログラムの簡単な解説です。
用途?結果を出したらへぇと思えたらそれで良いのです。
大まかな流れ
記事データから余分な記号を取り除く→mecabで形態素解析→単語の集計→グラフとかで結果を表示
こんな感じでやっていきます。
デモコード
gitにあげていますのでこちらを参照してください。
この記事では上記のコードを解説していきたいと思います。コードの訂正やツッコミは随時お待ちしております。
デモコードの解説
分割しながら解説していきたいと思います。
mecabのバグ対策
def __init__(self):
self.s = 0
self.e = 200000
self.stops = 2000000
self.tagger = MeCab.Tagger("-d /usr/local/lib/mecab/dic/mecab-ipadic-neologd")
バグについてはmecabのバグ?参照。
いい案が思いつかなかったので単純に文字数を200万字で区切ってmecabに投げるようにしています。
URLやHTMLタグを取り除く処理
def re_def(self,filepass):
with codecs.open(filepass, 'r', encoding='utf-8', errors='ignore')as f:
#with open(filepass, 'r')as f:
l = ""
re_half = re.compile(r'[!-~]') # 半角記号,数字,英字
re_full = re.compile(r'[︰-@]') # 全角記号
re_full2 = re.compile(r'[、・’〜:<>_|「」{}【】『』〈〉“”○〔〕…――――◇]') # 全角で取り除けなかったやつ
re_comma = re.compile(r'[。]') # 読点のみ
re_url = re.compile(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+')
re_tag = re.compile(r"<[^>]*?>") #HTMLタグ
re_n = re.compile(r'\n') # 改行文字
re_space = re.compile(r'[\s+]') #1以上の空白文字
re_num = re.compile(r"[0-9]")
start_time = time.time()
for line in f:
if re_num.match(line):
line = mojimoji.han_to_zen(line, ascii=False)
line = re_half.sub("", line)
line = re_full.sub("", line)
line = re_url.sub("", line)
line = re_tag.sub("",line)
line = re_n.sub("", line)
line = re_space.sub("", line)
line = re_full2.sub(" ", line)
line = re_comma.sub("\n",line) #読点で改行しておく
l += line
end_time = time.time() - start_time
print("無駄処理時間",end_time)
return l
形態素解析前の処理として、URLやHTMLタグ等を取り除きます。
まず、ファイルを開く際にバイトコードが入っていたりして文句を言われないようにcodecs
を用いてエラーを無視するようにしています。
次にタグ等の処理です。処理には正規表現を扱うre
を使用します。
複数回呼び出すことになるので事前にコンパイルしておきました。
取り除く内容としては
- HTMLタグ
- URL
- 半角文字全般
- 全角記号
- 余分な空白・改行
です。
半角文字を全て取り除くので英単語等をこの時点で除外してしまうことになりますが・・・目をつぶりました
またうまく取り除けなかった全角記号はアナログに打ち込んでいます。いい感じの正規表現ありましたらご教授くださいm(_ _)m
ただ、数字だけ多少気に食わなかったのでmojimoji
を使って半角から全角に直して対象から外しています。
time
モジュールについてはどれくらい時間かかってんのかなぁくらいで入れてるだけですハイ
ストップワードの取得
自然言語処理の前処理として、ストップワードを取り除く作業があります。
今後色々使いそうなので書いてみました。
ストップワード集としてSlothlibを使用します。逐一開いてDLしてくる作業がめんどくさいのでスクレイピングしてくるようししています。
def sloth_words(self): #slothwordのlist化
if os.path.exists("sloth_words.txt"):
text = ""
with open("sloth_words.txt",'r') as f:
for l in f:
text += l
soup = json.loads(text,encoding='utf-8')
return soup
###sloth_words###
sloth = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
slothl_file = urllib.request.urlopen(sloth)
soup = BeautifulSoup(slothl_file, 'html.parser')
soup = str(soup).split()
###sloth_singleword###
sloth_1 = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/OneLetterJp.txt'
slothl_file2 = urllib.request.urlopen(sloth_1)
soup2 = BeautifulSoup(slothl_file2, 'html.parser')
soup2 = str(soup2).split()
soup.extend(soup2) #1つにまとめる
###毎回呼ぶの面倒だからファイル作る
with open("sloth_words.txt","w") as f:
text_dic = json.dumps(soup,ensure_ascii=False, indent=2 )
f.write(text_dic)
return soup
urllib.request
で取ってきてBeautifulSoup
でタグを消してます。
一応、一文字のみのOneLetterJpも同時に取得しています。また、次回以降このスクレイピング処理を省きたいのでファイルを作成して、呼び出せるようにしています。
形態素解析用の関数
def morphologial(self, all_words):
wakati_data = []
while True:
w = all_words[self.s:self.e]
wakati_data.extend(self.tagger.parse(w).split("\n"))
if self.e > self.stops or self.e > len(all_words):
break
self.s = self.e
self.e += 200000
return wakati_data
mecabのバグより、200万字ごとに分けて形態素解析しています。この関数を、カウントする関数内で繰り返し呼び出していきます。yieldで返しても良かったのですが作成時の知識不足で使ってません。気が向いたら作り直しますハイ
単語の形態素解析処理
長いので区切りながら見ていきます。
def counting(self,all_words):
dicts = {} # 単語をカウントする辞書
print("総文字数:{0}\t({1}万字)".format(len(all_words),len(all_words)/10000))
mem = 0 #一定単語以上か判別
sloths = self.sloth_words() #slothのlist
if len(all_words) > 2000000:
mem = 1
総文字数をこの時点で出力して確認できるようにしてます。もし200万字を超えるなら変数を1にして繰り返し処理するようにします。
while True:
word_list = []
wakati = self.morphologial(all_words) #分かち書きアンド形態素解析
for addlist in wakati:
addlist = re.split('[\t,]', addlist) # 空白と","で分割
if addlist == [] or addlist[0] == 'EOS' or addlist[0] == '' or addlist[0] == 'ー' or addlist[0] == '*':
pass
elif addlist[1] == '名詞': #名詞のみカウント
if addlist[2] == '一般' or addlist[2] == '固有名詞' :#and not addlist[3] == '人名':
word_list.append(addlist) #listごとに区切るのでappendで。extendだとつながる
else:
pass
変数wakati
で形態素解析の関数を呼び出して解析させます。
mecab
には追加辞書を適用し、オプションなしで解析していますので
ここのスクショ
こんな感じで分かち書き+品詞+読みのように出力されます。これをsplit
でlist化して扱いやすくしています。
for文内のif・else文ですが、ここで取得する単語の品詞を分けています。このプログラムだと名詞に限定しています。さらにそこから名詞の一般形と固有名詞形に絞って変数word_list
に格納します。
単語のカウント
for count in word_list:
if count[0] not in dicts:
dicts.setdefault(count[0], 1)
else:
dicts[count[0]] += 1
単語を形態素解析し、かつ欲しい品詞に分類するとこまで終わったのであとはカウントするだけです。
dict型を使用してカウントしています。dict内に存在しなければ新しく追加し、既存であればカウントを+1しています。
文字数が200万を超えた場合の処理
###文字数オーバー時###
if mem:
if len(all_words) < self.stops:
del wakati, addlist, word_list
break
else:
del addlist
print("{}万字まで終わったよ".format(self.stops/10000))
self.stops += 2000000
self.s = self.e
self.e += 200000
else:
break
コンストラクタの文字数の始点・終点を入れ替えて終わるまで今までの処理を繰り返します。そもそも文字数が200万以下の場合は繰り返し処理は行いません。
ストップワードの除去
for key in list(dicts): #ストップワード除去
if key in sloths:
del dicts[key]
別の関数で取得したストップワードを最後に取り除きます。
dict型のキーのみでfor文を回す際はdict.keys()
よりもlist化する方が良いみたいです。
カウントした単語をグラフで表示させる
def plot(self,countedwords):
counts = {}
total = sum(countedwords.values())
c = 1
show = 20 #何件表示する?
for k, v in sorted(countedwords.items(), key=lambda x: x[1], reverse=True): # 辞書を降順に入れる
counts.update( {str(k):int(v)} )
c += 1
if c > show:
break
plt.figure(figsize=(15, 5)) #これでラベルがかぶらないくらい大きく
plt.title('頻繁に発言したワードベスト{0} 総単語数{1} 単語の種類数{2}'.format(show,total,len(countedwords)), size=16)
plt.bar(range(len(counts)), list(counts.values()), align='center')
plt.xticks(range(len(counts)), list(counts.keys()))
# 棒グラフ内に数値を書く
for x, y in zip(range(len(counts)), counts.values()):
plt.text(x, y, y, ha='center', va='bottom') #出現回数
plt.text(x, y/2, "{0}%".format(round((y/total*100),3)),ha='center',va='bottom') #パーセンテージ
plt.tick_params(width=2, length=10) #ラベル大きさ
plt.tight_layout() #整える
plt.show()
matplotlib
のplotを用いて、最頻出単語をグラフで表示させてます。
plotは全く触ってなかったのでggっていいとこ取りしてるだけなのでカバカバなコードになってますがご容赦ください。
よく使う箇所としてはこれ
sorted(countedwords.items(), key=lambda x: x[1], reverse=True)
lambda式を用いてdictを降順にしています。
カウントした単語の検索
def Search(self, countedwords,search):
results = {}
total = {"単語の種類数":sum(countedwords.values()),"単語の総数":len(countedwords)}
for k, v in countedwords.items():
if search == k:
results.update({str(k): int(v)})
return results , total
グラフで表示させるのとは別に、検索もできます。
実際にカウントしてみる
安倍総理が今年一番多く発言した単語を調べてみた
Wikipediaの記事の単語をカウントしてみた
安倍総理の発言を各年毎に集計してグラフにしてみた