NaaN日記

やったこと、覚えたことを発信する場

ブクマ記事のタイトルを使ってワード・クラウドを作成する

はじめに

こんにちは!はやいもので、もう12月ですね!

これは、SLP KBITアドベントカレンダーの4日目の記事です。他の部員の記事は以下からご覧ください!
adventar.org

今回は、タイトルにもある通り、はてなブックマークで、自身がブクマしている記事のタイトルからキーワードを抽出し、ワード・クラウドの画像データを作成します。

ワード・クラウドから、自分はこういうキーワードに興味を持っているというものを見ることが出来れば、面白いなと思っています!

ワード・クラウドとは

ワード・クラウドとは、タグ・クラウドの応用形で、文章ベースのコンテンツを視覚化して魅力的な文字空間を構成する、情報可視化の手法の一つです*1
ワード・クラウドを構成する語句のサイズは、出現頻度から決まります。

f:id:CNaan:20201204231213p:plain

開発環境

  • 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も除外します。

f:id:CNaan:20201203214534p:plain

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")

f:id:CNaan:20201204032927p:plain

ユーザ辞書の作成

ストップワードとして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
f:id:CNaan:20201204230008p:plain
CNaanのブックマークから作成したワード・クラウド

おわりに

今回は、CNaanがはてなブックマークしている記事のタイトルを使って、ワードクラウドを作成しました。
最後の画像を見てみると、GoやWeb、TypeScriptやCSSなどに興味を持っていそうですね*2

ワードクラウドは、見た目が色鮮やかで、見ていて楽しい気持ちになるので、
タイトル以外の情報に対しても、ワードクラウドを作ってみたいなあと思いました!

*1:Bella Martin, Bruce Hanington(2013) 『Research & Design Method Index -リサーチデザイン、新・100の法則』 郷司 陽子 (訳) 株式会社ビー・エヌ・エヌ新社

*2:blogはstop_wordsに入れるか悩ましいところでした。今回は結局入れませんでしたが