ブクマ記事のタイトルを使ってワード・クラウドを作成する
はじめに
こんにちは!はやいもので、もう12月ですね!
これは、SLP KBITアドベントカレンダーの4日目の記事です。他の部員の記事は以下からご覧ください!
adventar.org
今回は、タイトルにもある通り、はてなブックマークで、自身がブクマしている記事のタイトルからキーワードを抽出し、ワード・クラウドの画像データを作成します。
ワード・クラウドから、自分はこういうキーワードに興味を持っているというものを見ることが出来れば、面白いなと思っています!
ワード・クラウドとは
ワード・クラウドとは、タグ・クラウドの応用形で、文章ベースのコンテンツを視覚化して魅力的な文字空間を構成する、情報可視化の手法の一つです*1。
ワード・クラウドを構成する語句のサイズは、出現頻度から決まります。
開発環境
- Windows10 Pro 20H2
- Docker 19.03.13
コンテナのイメージ
使用する主なライブラリ
- feedparser
- MeCab
- Matplotlib
- WordCloud
今回使用したプログラムは、GitHubで公開しています。
実行方法などは、READMEを参考にしてください。
github.com
私はDocker上でアプリケーションを動かしましたが、Python3を動かせる環境があれば、以下に記述するプログラムは動作すると思います。
コンテナを使わない方は、プログラムに記述してあるファイルパスを適宜変更するようにしてください。
tree
. ├── Dockerfile ├── images │ └── image-CNaan.png // ワードクラウド画像 ├── requirements.txt ├── src │ ├── bookmark.py │ └── main.py └── userdic └── myDic.csv
ブクマ記事のタイトルを取得する
ブクマ記事のタイトルを取得するために、RSSを利用します。
RSSの詳しい仕様はこちらを参照してください。↓
はてなブックマークフィード仕様 - Hatena Developer Center
RSSは、https://b.hatena.ne.jp/${ユーザ名}/bookmark.rss
から確認できます。
私のユーザ名はCNaanですので、次の通りです。
https://b.hatena.ne.jp/CNaan/bookmark.rss
上記のRSSからタイトルを取得するために、Pythonのライブラリであるfeedparserを使います。
Pythonでプログラム(bookmark.py)を作成しました。
import feedparser import re import sys from typing import List class Bookmark: hatena_id = "" def __init__(self, hatena_id: str) : self.hatena_id = hatena_id # 公開しているブックマークの数を求める def count_bookmark(self) -> int: d = feedparser.parse('https://b.hatena.ne.jp/{}/rss'.format(self.hatena_id)) content = d['feed']['subtitle'] # 'Userのはてなブックマーク (num)' match = re.search(r"(はてなブックマーク \()(.*?)\)", content) num = match.group(2).replace(',', '') # 公開しているブックマーク数 if not num.isdecimal(): print('Error: num is string', file=sys.stderr) return 0 return int(num) def get_title(self) -> List[str]: # 1ページに20件のデータがある。ページ数を求める bookmark_num = self.count_bookmark() max_page = (bookmark_num//20) + int((bookmark_num%20) > 0) titles = [] for i in range(max_page): d = feedparser.parse('https://b.hatena.ne.jp/{}/rss?page={}'.format(self.hatena_id, i+1)) entries = d['entries'] for entry in entries: titles.append(entry['title']) return titles bookmark = Bookmark("CNaan") titles = bookmark.get_title() print(titles)
インスタンス生成時に、はてなIDの"CNaan"を引数に入れ、初期化しました。
関数count_bookmarkでは、公開しているブックマークの数を求めています。(軽くRSSを見て、取得方法がわからなかったので、正規表現で文字列から取得しました。)
正規表現で取得しましたが、' , '区切りの数となるので、注意が必要です(コンマを取り除かないと、1000を超える人のブックマーク数が取得できません)。
関数get_titleは、タイトルを取得し、リストを作成します。
実行すると、タイトルの一覧のリストが出力されると思います。
ワード・クラウドの作成
日本語フォントの用意
日本語を出力するためには日本語フォントが必要です。
今回は、予めホストで持っているフォントファイルをコンテナ内にCOPYすることで対応しました。
今回はGoogle Noto Fontsを利用しました。
How to install fonts – Google Noto Fonts
ダウンロードし、.fontsというディレクトリの中にNotoSansCJKjp-Regular.otfを用意しました。
プログラム
from bookmark import Bookmark import MeCab import os from wordcloud import WordCloud #ワードクラウドの作成 def create_wordcloud(titles: str): fontpath = '/work/.fonts/NotoSansCJKjp-Regular.otf' tagger = MeCab.Tagger( '-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd' ) tagger.parse('') word_list = [] for title in titles: node = tagger.parseToNode(title) while node: word_type = node.feature.split(',')[0] word_surf = node.surface.split(',')[0] if word_type == '名詞': if len(set(["副詞可能", "数", "非自立", "代名詞", "接尾"]) \ & set(node.feature.split(",")[1:4])) == 0: word_list.append(node.surface) node = node.next word_chain = ' '.join(word_list) wordcloud = WordCloud(background_color=None, mode="RGBA", font_path=fontpath, width=900, height=500, relative_scaling=0.5 # フォントサイズの相対的な単語頻度の重要性 ).generate(word_chain) #ファイルの作成 wordcloud.to_file("/work/images/image-" + os.environ['HATENAID'] + ".png") def main(): hatena_id = os.environ['HATENAID'] bookmark = Bookmark(hatena_id) titles = bookmark.get_title() create_wordcloud(titles) if __name__ == "__main__": main()
システム辞書として、mecab-ipadic-neologdを使用しました。
GitHub - neologd/mecab-ipadic-neologd: Neologism dictionary based on the language resources on the Web for mecab-ipadic
mecab-ipadic-NEologdは、新語や固有表現に強い辞書です。週に2回以上更新されています(すごい)。
このプログラムでは、キーワードとして名詞を抽出するようにしています。なお、名詞の中の("副詞可能", "数", "非自立", "代名詞", "接尾")については除外するようにしています。
WordCloudのパラメータの、background_color=Noneと、mode="RGBA"を組み合わせることで、透過画像となります。また、relative_scalingの値(0から1)によって、文字の出現頻度に対する文字サイズの比率を設定することができます。
その他のWordCloudのパラメータについては以下を参照ください。
wordcloud.WordCloud — wordcloud 1.8.1 documentation
出力してみる
画像を作成してみると、次のような感じになりました。
Qiitaが大きいですね。
今回は、自分の趣向が見たいなーと思っているので、Qiitaの記事のタイトルは必要ですが、サービス名である、"Qiita"という単語自体は、今回はそこまで重要ではありません。よって、ここからQiitaを除外したいと思います。
また、noteやSpeaker Deckも除外します。
stop wordsの追加
除外する単語を格納したリスト、stop_wordsを作成します。
from bookmark import Bookmark import MeCab import os from wordcloud import WordCloud #ワードクラウドの作成 def create_wordcloud(titles: str): fontpath = '/work/.fonts/NotoSansCJKjp-Regular.otf' stop_words = ['Qiita', 'note', 'Speaker Deck'] tagger = MeCab.Tagger( '-d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd' ) tagger.parse('') word_list = [] for title in titles: node = tagger.parseToNode(title) while node: word_type = node.feature.split(',')[0] word_surf = node.surface.split(',')[0] if word_type == '名詞': if (node.surface not in stop_words) and \ len(set(["副詞可能", "数", "非自立", "代名詞", "接尾"]) \ & set(node.feature.split(",")[1:4])) == 0: word_list.append(node.surface) node = node.next word_chain = ' '.join(word_list) wordcloud = WordCloud(background_color=None, mode="RGBA", font_path=fontpath, width=900, height=500, relative_scaling=0.5 # フォントサイズの相対的な単語頻度の重要性 ).generate(word_chain) #ファイルの作成 wordcloud.to_file("/work/images/image-" + os.environ['HATENAID'] + ".png")
ユーザ辞書の作成
ストップワードとしてSpeaker Deckを記述したにも関わらず、画像にはSpeaker Deckが表示されていることがわかります。これは、下のように、Speaker と Deck が別の単語として認識されているためです。
Speaker 名詞,固有名詞,組織,*,*,*,* Deck 名詞,一般,*,*,*,*,*
また、Speaker と Deckは、辞書に登録されていない単語で、MeCabが品詞を推定しています。
Speaker Deckを、一つの単語として認識させるために、ユーザ辞書を作成します。CSVファイル(userdic/myDic.csv)を作成しました。
ユーザ辞書の追加は、こちらを参考にしました。
blog.apar.jp
このとき、下の例のように、システムの品詞体系に沿わない辞書登録を行うと、エラーとなります。
Speaker Deck,,,1,名詞,固有名詞,*,*,*,*,Speaker Deck,,,
$ /usr/lib/mecab/mecab-dict-index -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -u /work/userdic/myDic.dic -f utf-8 -t utf-8 /work/userdic/myDic.csv reading /work/userdic/myDic.csv ... context_id.cpp(96) [it != left_.end()] cannot find LEFT-ID for 名詞,固有名詞,*,*,*,*,*
固有名詞を、さらに分ける必要があります。
MeCabユーザ辞書作成時の陥りがち?なミス(エラー: cannot find LEFT-ID) - Qiita
Speaker Deck,,,1,名詞,固有名詞,組織,*,*,*,Speaker Deck,,,
"組織" を追加してみると、以下のように成功します。
$ /usr/lib/mecab/mecab-dict-index -d /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd -u /work/userdic/myDic.dic -f utf-8 -t utf-8 /work/userdic/myDic.csv reading /work/userdic/myDic.csv ... 1 emitting double-array: 100% |###########################################| done!
また、mecabrcに作成したユーザ辞書の情報( userdic = /work/userdic/myDic.dic )を追加します。
echo userdic = /work/userdic/myDic.dic >> /usr/local/etc/mecabrc
出力結果を確認すると、ユーザ辞書が適用されていることがわかります。
>>> import MeCab >>> tagger = MeCab.Tagger() >>> print(tagger.parse('Speaker Deck')) Speaker Deck 名詞,固有名詞,組織,*,*,*,Speaker Deck,,,
結果
docker runをする際、環境変数ではてなIDを設定し、また、画像出力用のディレクトリをコンテナ内のディレクトリとマウントします。
$ docker build -t myword-cloud:1.0 . $ docker run -it --rm --name myword-cloud -e HATENAID=CNaan -v $(pwd)/images:/work/images myword-cloud:1.0