nikkie-ftnextの日記

イベントレポートや読書メモを発信

自分が書いたはてなブログの記事でWordCloudを作り、2019年と2018年を比較する

はじめに

頑張れば、何かがあるって、信じてる。nikkieです。
先日「エンジニアの登壇を応援する会」の忘年LT大会にて、週1でブログを書くブログ駆動開発を知りました。
今週のネタ「自分の今年のブログのWordCloudを作る」についてブログを書きます。

宣言:私のブログ駆動開発

ブログ駆動開発がよさそうに思ったのは、「本当に学びたいことをブログ記事にする」という点です。
2019年4月から業務で自然言語処理に取り組んでいますが、この分野のキャッチアップはまだまだと感じます。
そこで直近1クール(2020年3月末まで)は、自然言語処理のネタで毎週1本ブログを書くことにします。

目的は自然言語処理へのキャッチアップですので、イベントレポートや雑記はブログ駆動開発としてはカウントしないという制約を追加します。

[ここから本題] WordCloudを作ってみた

忘年LT大会で知った「自分のブログのWordCloud」に取り組みました。

WordCloudとは

文章中で出現頻度が高い単語を複数選び出し、その頻度に応じた大きさで図示する手法。ウェブページやブログなどに頻出する単語を自動的に並べることなどを指す。文字の大きさだけでなく、色、字体、向きに変化をつけることで、文章の内容をひと目で印象づけることができる。

デジタル大辞泉 より

1年のブログをWordCloudにすることで、どんなことについて、頻繁に書いた1年だったかが掴めると考えています。

今回は公開している記事のタイトルと本文を対象にしました

  • 2019年:32件
  • 2018年:78件

2019年のブログのWordCloud

f:id:nikkie-ftnext:20191229015635p:plain

出現頻度が高いものは

ですね。
「PyCon」は、PyCon JPのスタッフ活動を始めたり、海外のPyConに参加したりと、私の2019年を言い表している単語のように思います。

2018年のブログのWordCloud

f:id:nikkie-ftnext:20191229015649p:plain

出現頻度が高いものは

といったところでしょうか。
2018年はDjango Girls Tutorialの翻訳に取り組んだ年で、ブログにもコードを載せて翻訳中の気づきをアウトプットしています(postやcommentはTutorialのコードをカウントしたためと思われます)。
Dockerは2019年はめっきり登場しなくなっていますが、使わなくなったわけではありません(最近は業務の中で、コンテナ内のuserとホストマシンのuserとの重ね合わせやコンテナの/etc/hostsの書き換えでハマりました)

2018年からずっとこのブログはPythonについてアウトプットしていて、改めて「Pythonに夢中になっているんだなあ」としみじみ思いました。

どのように手を動かしたか

WordCloudは(このブログでは未アウトプットですが、)過去の #pyhack で実装したコードがあります。

今回は前処理に手を入れて使っています。

はてなブログの記事の取得は新規に実装しました。
ソースコードはこちら

今回の開発で学んだのは大きく2点です:

  • はてなブログの記事一覧(XML)のパース
  • ブログ記事から不要な部分の除去(re.sub

はてなブログの記事一覧のパース

長くなったのでQiitaにまとめました。

今回、XMLの解析とは別の、思ってもみないところでつまづきました。
それがdatetimeタイムゾーン周りで発生した以下のエラーです:

TypeError: can't compare offset-naive and offset-aware datetimes

原因ですが、

わけです。

取得した記事は別々のファイルに分かれてフォルダに格納されます。
一方、私の実装の都合により、後続のWordCloudのスクリプトにはテキストを1ファイルで渡す必要があります。
そこでシェルからcatで1ファイルにまとめます:cat 2019/*.txt > 2019_all_blog.txt

正規表現を使ってブログ記事から不要な部分の除去

これまでのコードでは、前処理が不十分なためにURLに使われる単語もカウントされていました(先のツイートのhttpsやcom)。
これを削除することに取り組みました。

また、はてなブログの記事では [awesome_link:embed]といった埋め込みや [^1] のような脚注も登場します。 この [] で囲まれた部分も削除しました。

さらに、2018年の記事を見返すと、Markdown記法ではなく見たまま記法で書いていた時期がありました。
そのためにWordCloudにdivやpxなどのHTMLタグに使われる語が登場していました。
見たまま記法で作ったHTMLからHTMLタグも削除しました。

text = fin.read()  # ファイルから読み込み
square_bracket_removed = re.sub(r'\[.+?\]', '', text)  # はてな記法の[]を削除
uri_removed = re.sub(  # リンクのURIを削除
    r'https?://[\w/:%#$&?~.=+-]+', '', square_bracket_removed)
# <div>や</blockquote>などのHTMLタグを削除
html_tags_removed = re.sub('</?.+?>', '', uri_removed)

このコードがどのように動くのか、理解したことを以下に書きます。

正規表現. は「改行以外の任意の文字」にマッチします1
.+ は「改行以外の任意の文字を 1 回以上繰り返したもの」です。
.+?最小のマッチにします。
最小とはどういうことかと言うと、[^1]: mofu [hoge:title] のような文字列を考えた時、[^1][hoge:title] にマッチするということです。
?をつけない.+の場合は、[^1]: mofu [hoge:title]全体にマッチします(続く処理により、mofu の部分が捨てられてしまいます)2

URIを表す正規表現正規表現サンプル集 を参考にしました。
https?でhttpもhttpsも表せていて、うまいなと思います(?は「直前の正規表現を 0 回か 1 回繰り返し」を表す)。
[]は「文字の集合を指定」します。
この中では「特殊文字はその特殊な意味を失」うそうです(バックスラッシュによるエスケープが不要でした)。

HTMLタグを表す正規表現</?.+?>は、URIの場合を真似ました。
<h2>日記の見出し</h2>という文字列では、<h2></h2>だけを除いてほしいので、?を使って最小マッチにしています。

こうして指定した正規表現を使い、re.subで空文字列に置き換えて除去します。
今回の開発で re.sub の使い方にはだいぶ慣れました。

不要な部分を除去した後は、kz_moritaさんから教わった参考記事を真似て

  • STOPWORDの追加
  • 品詞の絞り込み

を試しました。
その結果が先ほどお見せした2枚のWordCloudです。

通して実行した様子

# はてなブログの記事一覧を取得するスクリプトのディレクトリに移動
cd ~/hatenablog-atompub-python
source env/bin/activate
python main.py nikkie-ftnext nikkie-ftnext.hatenablog.com 2019 --output output/2019
cd output
cat 2019/*.txt > 2019_all_blog.txt
deactivate
# WordCloudのスクリプトのディレクトリに移動
cd ~/cfp_wordcloud
source env/bin/activate
python draw_cloud.py ~/hatenablog-atompub-python/output/2019_all_blog.txt  # ブログ記事をまとめたファイルを渡す

その他のリソース

手を動かす中や動かした後に見つけたリソースを挙げます。
janomeチュートリアルはブログ駆動開発(自然言語処理編)の1つのネタとしておきます

終わりに

初回のブログ駆動開発は以上です。
迫るタイムリミットで平日はプレッシャーを感じるものですね。

実装したコードにはブラッシュアップできる点がいくつか浮かぶので、Issue管理して整えていきます。
それでは、翌週のブログ駆動開発(自然言語処理編)でまたお会いしましょう。


  1. 正規表現については re --- 正規表現操作 — Python 3.8.1 ドキュメント を参照しています

  2. Pythonによるあたらしいデータ分析の教科書』5章の青空文庫のテキストの前処理で見たコードを参考にドキュメントを引きながら手を動かしました。今回ようやく意味がつかめました