このブログで何度かワードクラウドを作って来ましたが、ちょっとした興味からそれを画像ではなくHTML出力したいと思いやってみました。
そして、単にHTML出力をするだけでなく、ワードクラウド中の単語をクリックしたら単語ごとに指定したURLにジャンプできるようにします。
この記事のサンプルではその単語でのGoogle検索結果へアクセスできるようにします。ワードクラウド自体はいつものライブドアニュースコーパスのデータで作ります。
参考: livedoorニュースコーパスのファイルをデータフレームにまとめる
実はいつも使っているワードクラウドのライブラリ(wordcloud)には、to_html()というメソッドが形だけあって、中身が実装されていないという状態でした。僕はこれが実装されるのを待っていたのですが、残念ながら最近の更新でこのメソッド自体が消されてしまいました。
参考: remove empty to_html in favor of to_svg (#607) · amueller/word_cloud@be9bb5e · GitHub
HTML出力機能はもう実装されないんだと思い、wordcloudのオブジェクトが持っているlayout_プロパティ(単語や配置場所、色などがまとまった配列)を読み解いて、自分でHTMLを作るコードを書くしかないと思ってコーディングに着手していました。
しかし改めてライブラリのコードをよく見るとto_svg()なるメソッドを持っているではないですか。これを使えばSVGで書き出せるので、簡単にHTMLに埋め込めます。
参考: wordcloud.WordCloud — wordcloud 1.8.1 documentation
SVGで書き出せたらあとはリンクを貼るだけなので簡単です。textタグたちをaタグで囲むだけです。ただ、今回はJavaScriptの addEventListener で、click したら指定のURLを開く関数をセットしました。
前置きが長くなって来たので、ここからやってみます。まずは普通にワードクラウドを作ります。
import re
import subprocess
import unicodedata
import pandas as pd
import MeCab
from wordcloud import WordCloud
# データの読み込み
df = pd.read_csv("./livedoor_news_corpus.csv")
# ユニコード正規化とアルファベットの小文字統一
df.text = df.text.str.normalize("NFKC").str.lower()
# 改行コードを取り除く
df.text = df.text.str.replace("\n", " ")
# エラーになる文字があるので取り除く (ライブドアニュースコーパス使う場合だけの処理。普通は不要)
df.text = df.text.str.replace("\u2028", "")
# 辞書のパス取得
dicdir = subprocess.run(["mecab-config", "--dicdir"], capture_output=True, text=True).stdout.strip()
tagger = MeCab.Tagger(f"-d {dicdir}/ipadic")
# ひらがなのみの文字列にマッチする正規表現
kana_re = re.compile("^[ぁ-ゖ]+$")
# 分かち書きして、ひらがな以外を含む 名詞/動詞/形容詞 を返す関数
def mecab_tokenizer(text):
# 分かち書き
parsed_lines = tagger.parse(text).splitlines()[:-1]
surfaces = [l.split('\t')[0] for l in parsed_lines]
features = [l.split('\t')[1] for l in parsed_lines]
# 原型を取得
bases = [f.split(',')[6] for f in features]
# 品詞を取得
pos = [f.split(',')[0] for f in features]
# 各単語を原型に変換する
token_list = [b if b != '*' else s for s, b in zip(surfaces, bases)]
# 名詞/動詞/形容詞に絞り込み
target_pos = ["名詞", "動詞", "形容詞"]
token_list = [t for t, p in zip(token_list, pos) if (p in target_pos)]
# ひらがなのみの単語を除く
token_list = [t for t in token_list if not kana_re.match(t)]
# アルファベットを小文字に統一
token_list = [t.lower() for t in token_list]
# 半角スペースを挟んで結合する。
result = " ".join(token_list)
# 念のためもう一度ユニコード正規化
result = unicodedata.normalize("NFKC", result)
return result
# 形態素解析
df["tokens"] = df.text.apply(mecab_tokenizer)
# wordcloud入力データ用に連結する
text_data = " ".join(df["tokens"])
# ワードクラウドのオブジェクト生成
wc = WordCloud(
font_path="/Library/Fonts/ipaexg.ttf", # 日本語フォントファイル
width=600, # 幅
height=400, # 高さ
prefer_horizontal=0.9, # 横書きで配置することを試す確率 (デフォルト0.9)
background_color='white', # 背景色
include_numbers=False, # 数値だけの単語を含まない
colormap='tab20', # 文字色のカラーマップ指定
regexp=r"\w{2,}", # 2文字以上の単語のみ含む
relative_scaling=1, # 頻度のみで文字サイズを決める
collocations=False, # bi-gramを考慮しない
max_font_size=60, # 最大フォントサイズ
random_state=42, # 乱数の初期値
).generate(text_data)
# この時点で作成できたwordcloudを確認する場合は以下の関数を実行。
wc.to_image()
# 出力省略
さて、これでワードクラウドができました。これを to_svg() で SVGに出力するとどうなるか見てみましょう。
# to_svg() で svgの文字列を生成できる
print(wc.to_svg())
"""
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="400">
<style>text{font-family:'IPAexGothic';font-weight:normal;font-style:normal;}</style>
<rect width="100%" height="100%" style="fill:white"></rect>
<text transform="translate(342,63)" font-size="60" style="fill:rgb(127, 127, 127)">思う</text>
<text transform="translate(391,134)" font-size="44" style="fill:rgb(127, 127, 127)">映画</text>
<text transform="translate(239,359)" font-size="41" style="fill:rgb(174, 199, 232)">自分</text>
<text transform="translate(458,55)" font-size="39" style="fill:rgb(174, 199, 232)">記事</text>
# 中略
<text transform="translate(411,86)" font-size="12" style="fill:rgb(158, 218, 229)">一番</text>
<text transform="translate(408,50)" font-size="12" style="fill:rgb(23, 190, 207)">転職</text>
<text transform="translate(58,277)" font-size="12" style="fill:rgb(255, 152, 150)">得る</text>
</svg>
"""
引数にファイル名の指定などはできず、SVGタグで囲まれた文字列を返してくれるようです。
これを .svg の拡張子で保存すると、ワードクラウドのSVGファイルが出来上がります。
先頭に、”<!DOCTYPE HTML>”をくっつけて、.htmlファイルで保存してもよいでしょう。
文字列にリンクを設定するなどの要件が無いのであれば、これをファイルに保存して完成です。
ここからリンクを設定していきます。リンク先は今回はお試しなので、Google検索結果にします。URLは、 https://www.google.com/search?q={検索キーワード} です。
前の方でも書きましたが、リンクっていう観点だとaタグで囲むのがセオリーですし、テキストを加工するスクリプトを描くのもそんなに難しくありません。ただ、文字列の加工の総量を少なめにしたいのと、イベントリスナーを使う方法の方がページ遷移させる以外の用途への応用も多いと思うので、こちらを採用しました。
Google検索結果のURLの検索キーワードはパーセントエンコーディングする必要があるので、JavaScriptのencodeURI関数を使います。僕はあまりJavaScript得意ではないので自信がないですが、次のようなコードでいかがでしょうか。
(無理矢理PythonでJavaScriptを書き出してますが、一旦SVGを書き出してテキストエディタでスクリプトを書き込んでも良いと思います。)
# textタグたちに、クリック時にGoogle検索結果を開くイベントリスナーを追加するJavaScript
link_script = """
<script>
svg = document.getElementsByTagName("svg")[0];
text_tags = svg.getElementsByTagName("text")
for(var i=0; i<text_tags.length; i++){
text_tags[i].addEventListener(
"click",
function(){
word = this.textContent;
word_uri = encodeURI(word);
url = "https://www.google.com/search?q=" + word_uri;
window.open(url, "_bkank");
}
)
}
</script>"""
# HTMLファイルに書き出し
with open("word_cloud.html", "w") as f:
f.write("<!DOCTYPE HTML>\n")
f.write(wc.to_svg())
f.write(link_script)
出力されるhtmlファイルが次です。
ブログに公開できるデータということで、無難なデータと無難なリンク先で作っていますが、これはデータと使い方によっては結構面白いものを作れる可能性を感じませんか?