Pythonで現代の季語を調べてみた

初めまして!! Aidemy研修生のカピバラさんです。 表題の通り、今回僕はPythonで現代の季語を調べてみました。 俳句の授業などで季語に関して習うと思いますが、何でこの言葉が季語なんだろうって思うことありませんでしたか? 僕は、今と昔で感じ方が違うのかなーと思いながら頑張って暗記してました。 では、現代の人々が季語だと感じているのはどんな言葉なんでしょう。 今でいう昔の俳句のようなものと言えば、CDなどで世に出回っている曲かなと思います。そこで、春夏秋冬、それぞれのイメージを持たれている曲の歌詞から現代の季語を調べていきます。

環境

以下、今回の開発環境です。

  • windows10
  • Python3.6.5
  • Jupyter4.4.0

また、インポートしたライブラリは以下のようになっています。

import requests
from bs4 import BeautifulSoup
import re
import neologdn
from janome.tokenizer import Tokenizer
import collections

webスクレイピング

まずは、webスクレイピングによって季節ごとの曲の歌詞を収集します。 今回は、以下のサイトからスクレイピングをさせて頂きました。

今回のwebスクレイピングは大きく分けて2段階に分かれます。

  1. 人気曲投票ランキングから季節の曲の曲名と歌手名を取得する
  2. 歌詞検索サービスによって指定の曲を検索し、歌詞を取得する

まず、1からやっていきましょう。 それぞれの季節の曲の人気ランキングから曲名と歌手名を取得していきます。 今回参照したサイトでは「曲名(歌手名)」のように曲名と歌手名が連なって表記されているため、取得のついでに曲名と歌手名を分けてリストに入れます。

しかし、表記方法がぶれていたり、そもそも歌手の情報がなかったりと上手く情報が取得できない曲もあったため、そういった曲はリストから削除してしまいました。 ソースコードはこんな感じです。

#urlを登録
url_spring = 'http://www.rankingbook.com/category/music/spring/favorite/'
url_summer = 'http://www.rankingbook.com/category/music/summer/favorite/'
url_autumn = 'http://www.rankingbook.com/category/music/autumn/favorite/'
url_winter = 'http://www.rankingbook.com/category/music/winter/favorite/'

#曲名と歌手を取得
def getmusic(url):
    r = requests.get(url)
    soup = BeautifulSoup(r.content, "html.parser")
    music = soup.find_all("a", attrs={"target": "_top"})
    music.extend(soup.find_all("td", attrs={"class": "b","align":"LEFT"}))
 #取得した情報を曲名と歌手名に分割
    for i in range(len(music)):
        music[i] = list(music[i].strings)[0][:-1]
        music[i] = re.split("[((/<…『</]",music[i])
    return music

#歌手情報が無い曲などを削除
def delerror(music):
    delindex = []
    for i in range(len(music)):
        if(len(music[i])!=2):
            delindex.append(i)
    delindex.reverse()
    for i in delindex:
        del music[i]
    
    
music_spring = getmusic(url_spring)
music_summer= getmusic(url_summer)
music_autumn = getmusic(url_autumn)
music_winter = getmusic(url_winter)

delerror(music_spring)
delerror(music_summer)
delerror(music_autumn)
delerror(music_winter)

続いて、2に取り掛かります。 先ほど取得した曲名と歌手名のリストを使って歌詞を検索し、取得します。 検索機能があるwebページでは、ユーザーが入力した情報をurlの中に組み込んで検索結果のページに遷移することによって、検索を行っている場合があります。

そのような場合、urlに直接キーワードを入力することでも検索ができます。 今回はそれを利用しました。 試しに歌詞検索のページで「カピバラ」というキーワードにより曲名検索をしてみると以下のようなurlのページに遷移しました。

https://www.uta-net.com/search/?Aselect=2&Keyword=カピバラ&Bselect=4&x=24&y=15

この「カピバラ」の部分に検索したい曲名を入れたurlを登録することで、曲名検索を行います。 そして、検索結果の中から歌手名が一致する曲の歌詞を取得しました。 ソースコードはこんな感じです。

#歌詞を取得
def getkashi(musics):
    kashi = ""
    for music in musics:
        url = "https://www.uta-net.com/search/?Aselect=2&Keyword="+music[0]+"&Bselect=4&x=24&y=15"
        r = requests.get(url)
        soup = BeautifulSoup(r.content, "html.parser")
        #曲名検索の結果、ヒットがあるかを判定
        if(len(soup.find_all("td", attrs={"class": "side td1"})) != 0):
            
            #ヒットした曲の中に歌手名が一致するものがあるかを判定
            singers = soup.find_all("td", attrs={"class": "td2"})
            singer = -1
            for i in range(len(singers)):
                if(list(singers[i].strings)[0] == music[1]):
                    singer = i
            if(singer != -1):
                
                #歌詞を取得
                href = soup.find_all("td", attrs={"class": "side td1"})[singer].contents[0].get("href")
        
                r = requests.get("https://www.uta-net.com"+href)
                soup = BeautifulSoup(r.content, "html.parser")
            
                kashi_list = list(soup.find_all("div", attrs={"id": "kashi_area"})[0].strings)
                for kashi_ in kashi_list:
                    kashi += kashi_
    return kashi


#ファイルへ保存
#春
kashi_spring = getkashi(music_spring)
with open('spring.txt', 'w') as f:
    f.write(kashi_spring)
#夏
kashi_summer = getkashi(music_summer)
with open('summer.txt', 'w') as f:
    f.write(kashi_summer)
#秋
kashi_autumn = getkashi(music_autumn)
with open('autumn.txt', 'w') as f:
    f.write(kashi_autumn)
#冬
kashi_winter = getkashi(music_winter)
with open('winter.txt', 'w') as f:
    f.write(kashi_winter)

これで、webスクレイピングは終了です! これで、それぞれの季節の曲の歌詞をテキストファイルに保存することができました。 結果としては、春:51曲、夏:127曲、秋:39曲、冬112曲の歌詞を取得できました。 季節によってだいぶ偏りがありますが、そもそもランキングに載っている曲数がかなり偏っていたため、仕方ないかなということでこのまま進めていきます。

歌詞データの整理

次に、歌詞データを整理していきます。 今回は、日本語の季語を調べたいため、英文字は不要です。また、記号や空白、改行なども要りません。そういったもの正規表現を使って予め削除していきます。 また、文字列の正規化も行います。 複数の文書から特徴を抽出する場合、入力ルールが統一されておらず表記揺れが発生している場合があります。

そうすると、同じはずの単語を別のものとして解析してしまい、意図しない解析結果になってしまいます。 この表記ゆれを無くすために、全角を半角に統一や大文字を小文字に統一等、ルールベースで文字を変換することを正規化と言います。 正規化はライブラリの NEologd を用いると簡単に行うことができます。 ソースコードはこんな感じです。

#データの整理
def organization(text):
    # 英数字,記号,空白,改行の削除
    text = re.sub("[a-zA-Z0-9_]","",text)
    text = re.sub("[!-/:-@[-`{-~…!「」“”。?、・~♪‥‘’]","",text)
    text = re.sub("\s","",text)
    text = re.sub("\n","",text)
    #正規化
    text = neologdn.normalize(text)   
    return text

kashi_spring = organization(kashi_spring)
kashi_summer = organization(kashi_summer)
kashi_autumn = organization(kashi_autumn)
kashi_winter = organization(kashi_winter)

名詞を抽出

続いて、歌詞データの中から名詞を抽出していきます。 名詞を抽出するためにまず形態素解析を行います。 形態素解析とは、辞書を利用して形態素に分割し、さらに形態素ごとに品詞などのタグ付け(情報の付与)を行うことです。形態素は意味を持つ最小の言語単位です。 例えば、「明日は晴れるだろうか。」という文に対して形態素解析を行うと以下のような結果が得られます。

明日 名詞,副詞可能,*,*,*,*,明日,アシタ,アシタ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
晴れる 動詞,自立,*,*,一段,基本形,晴れる,ハレル,ハレル
だろ 助動詞,*,*,*,特殊・ダ,未然形,だ,ダロ,ダロ
う 助動詞,*,*,*,不変化型,基本形,う,ウ,ウ
か 助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ
。 記号,句点,*,*,*,*,。,。,。

今回はjanomeという形態素解析器を用いて、形態素解析を行い、名詞を抽出します。 ソースコードはこんな感じです。

#形態素解析を行い、名詞を取り出す
def getnoun(text):
    t = Tokenizer()
    tokens = t.tokenize(text)
    noun = []
    for token in tokens:
        partOfSpeech = token.part_of_speech.split(",")[0]
        if partOfSpeech == "名詞":
            noun.append(token.surface)
    return noun

#名詞取り出し
noun_spring = getnoun(kashi_spring)
noun_summer = getnoun(kashi_summer)
noun_autumn = getnoun(kashi_autumn)
noun_winter = getnoun(kashi_winter)

出現数をカウント

次に、単語の出現数をカウントしていきます。 出現数のカウントには、ライブラリのcollectionsを使用します。 単語の出現数をカウントし、上位30単語を抽出します。 ソースコードはこんな感じです。

#出現数カウント
count_dict_spring = collections.Counter(noun_spring)
count_dict_summer = collections.Counter(noun_summer)
count_dict_autumn = collections.Counter(noun_autumn)
count_dict_winter = collections.Counter(noun_winter)

#上位30単語をリストで取得
word_spring = count_dict_spring.most_common(30)
word_summer = count_dict_summer.most_common(30)
word_autumn = count_dict_autumn.most_common(30)
word_winter = count_dict_winter.most_common(30)

取得結果を調整

最後に取得結果を調整します。 まず、季節の単語そのまま(春、夏、秋、冬)は季語とは言えないため、ストップワードとし、削除します。 また、他の季節でも出てきている単語は季語とは言えないため、重複している単語も削除します。 ストップワード削除のソースコードはこんな感じです。

#指定した文字列をリストから削除
def dellist(text,del_text):
    del_index = []
    for i in range(len(text)):
        for delt in del_text:
            if(text[i][0]==delt):
                del_index.append(i)
    del_index.reverse()
    for deli in del_index:
        del text[deli]

#"春","夏","秋","冬"をストップワードとして登録
stopword = ["春","夏","秋","冬"]

#ストップワードをリストから削除
dellist(word_spring,stopword)
dellist(word_summer,stopword)
dellist(word_autumn,stopword)
dellist(word_winter,stopword)
重複している単語削除のソースコードはこんな感じです。

#共通する要素のインデックス番号を返す
def delcommon(text1,text2):
    del_index1 = []
    del_index2 = []
    for i in range(len(text1)):
        for j in range(len(text2)):
            if(text1[i][0]==text2[j][0]):
                del_index1.append(i)
                del_index2.append(j)
    return del_index1,del_index2

#指定したインデックス番号の要素を削除
def delindex(text,del_index):
    del_index = list(set(del_index))
    del_index.sort(reverse=True)
    for deli in del_index:
        del text[deli]

#共通する要素のインデックスを取得
del_index_spring,del_index_summer = delcommon(word_spring,word_summer)
del_index_sp1,del_index_autumn = delcommon(word_spring,word_autumn)
del_index_sp2,del_index_winter = delcommon(word_spring,word_winter)
del_index_su1,del_index_au1 = delcommon(word_summer,word_autumn)
del_index_su2,del_index_wi1 = delcommon(word_summer,word_winter)
del_index_au2,del_index_wi2 = delcommon(word_autumn,word_winter)

#季節ごとにインデックス番号を結合
del_index_spring.extend(del_index_sp1)
del_index_spring.extend(del_index_sp2)
del_index_summer.extend(del_index_su1)
del_index_summer.extend(del_index_su2)
del_index_autumn.extend(del_index_au1)
del_index_autumn.extend(del_index_au2)
del_index_winter.extend(del_index_wi1)
del_index_winter.extend(del_index_wi2)

#指定したインデックス番号の要素を削除
delindex(word_spring,del_index_spring)
delindex(word_summer,del_index_summer)
delindex(word_autumn,del_index_autumn)
delindex(word_winter,del_index_winter)

結果と考察

今回のプログラムで得られた結果はこんな感じになりました。 数字は単語の出現数です。

春
[('花', 60), ('さくら', 42), ('それ', 31), ('桜', 27), ('声', 26), ('前', 25)]
夏
[('海', 99), ('太陽', 74), ('波', 64), ('目', 52)]
秋
[('涙', 41), ('好き', 21), ('そば', 20), ('ひとつ', 19), ('どこ', 18), ('雨', 16)]
冬
[('雪', 138), ('街', 79), ('夜', 78), ('言葉', 48)]

「花」や「桜」、「海」や「雪」などイメージ通りの単語を抽出することができたかなと思います。 個人的に驚いたのは秋の「涙」や、冬の「街」ですね。潜在的にそういうイメージをもっているのでしょうか。 あとは、秋で「そば」が抽出されていますが、最初「蕎麦」を抽出できたのかと思い感動してました(蕎麦の旬は10~11月らしいです)。でもこれ多分「傍」ですね。途中で気づいてがっかりしてました。 まだ、「それ」や「どこ」など意味のない単語を抽出してしまっているため、そのような単語を削除できれば、精度は上がっていくのかなと思います。

また、「さくら」と「桜」が別の単語として出力されています。word2vecを使った類似度計算などで対策しようかとも思ったのですが、季語の中には似たような言葉もあったイメージがあって、必要以上にまとめ上げられてしまいそうな気がしたため、今回はやめておきました。こういった部分も今後の課題です。 ここまで読んでいただき、ありがとうございました!!

参考

今回、単語の出現数をカウントする際に以下のサイトを参考にさせて頂きました。 ありがとうございます。 www.lifewithpython.com
それではまた次の記事でお会いしましょう。最後までご覧くださりありがとうございました。

プログラミング未経験からでもAIスキルが身につくAidemy Premium




PythonやAIプログラミングを学ぶなら、オンライン制スクールのAidemy Premiumがおすすめです。
「機械学習・ディープラーニングに興味がある」
「AIをどのように活用するのだろう?」
「文系の私でもプログラミング学習を続けられるだろうか?」
少しでも気になることがございましたら、ぜひお気軽にAidemy Premiumの【オンライン無料相談会】にご参加いただき、お悩みをお聞かせください!