アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Python x 自然言語処理でアシアルブログの単語ランキングを算出する

はじめまして、江口です。

機械学習やらAIやらtensorflowやらで最近耳にすることが増えたと思うPythonについての記事です。
先日書店でも並んでいる本が殆どRやPythonばかりで、機械学習の盛り上がりが伝わってきました!
今回はネタですが、以下の技術を使って、アシアルブログの頻出単語ランキングを算出して円グラフ化してみました。

▼使用言語


Python 3.6

▼使用library


unicodedata -> 全角文字判定に使用

requests -> アシアルブログのHTML文字列取得に使用

BeautifulSoup -> スクレイピングに使用

MeCab -> 形態素解析に使用

CountVectorizer -> 単語頻出数算出に使用

matplotlib -> データ可視化(円グラフ)に使用

▼その他ツール


conda 4.3.16 ->パッケージ管理に使用
spyder 3.1.2 -> IDE
jupyter notebook 4.3.1 -> 対話式にコードを実行し、グラフ等も即時に可視確認できる

最初に結果です!

▼アシアルブログ頻出単語ランキング




・処理概要


下記ソースコードにも記載してあるのですが、アシアルブログのTOPページ(http://blog.asial.co.jp/)から「/[0-9]{4}」形式のリンクのみをスクレイピングで抽出し、ブログの詳細記事をクローリングして全てのブログ記事からBODYタグを抽出して、さらにそこから全角の一般名詞のみを抽出してます。

・考察


最終的にTF解析で上位10件を円グラフで頻出単語ランキングとして表示させています。
さすがOnsenUIの効果でしょうか。「温泉」が1位の143個でした!
2位の「アプリケーション」は汎用的すぎて弾いても良かったですね^^;
コンポーネントは最近は宇都宮さんのブログでvuejsの「コンポーネント」が結構目立ったのだと思います。
それ以降の単語は、汎用的と言いますか、あまり参考にならなそうなので、こちらも弾いていいかもですね!

ソースコード


・asialblog_ranking.pyで下記を保存
・TTF_PATHは人それぞれ場所が異なります。



#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pythonでアシアルブログの単語ランキングを算出する
[前提]アシアルブログの記事詳細はhttp://blog.asial.co.jp/[0-9]{4}の形式になっている
1. TOPサイトから上記形式のhrefを全て抽出する
2. 1で取得したURLを全てクローリングし、BODYタグの全ての全角文字を抽出する
3. 2で抽出した全角文字を形態素解析に掛けて、一般名詞のみを抽出する
4. 3で取得した一般名詞をTF解析で単語頻出度を算出する
5. 最終的にmatplotlibで円グラフに上位10件を表示する

Created on Wed Apr 12 11:38:42 2017

@author: eguchi
"""
import unicodedata
import requests as req
from bs4 import BeautifulSoup as bs
import MeCab as mc
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager
import pylab
import re

# 定数(と言って良いのかは微妙ですが)
BASE_URL = 'http://blog.asial.co.jp/'
EXCLUDE_STR_LIST = ['年月日', '年月', 'アシ', 'アシアル', 'アル', '情報', '株式会社', '全国']
TTF_PATH = '/Users/hogehoge/anaconda/lib/python3.6/site-packages/matplotlib/mpl-data/fonts/ttf/ipam.ttf'

# 1. TOPサイトからhttp://blog.asial.co.jp/[0-9]{4}の形式のhrefを全て抽出する
html = req.get(BASE_URL).text
# BeautifulSoup4でHTMLを解析
soup = bs(html, 'html.parser')
for script in soup(["script", "style"]):
    script.extract()

# 記事詳細のlinkのみを取得
blogs = []
for link in soup.select('a[href]'):
    if (re.match('^/[0-9]{4}$', link.attrs['href'])):
        blogs.append(BASE_URL + link.attrs['href'])

# 2. 1で取得したURLを全てクローリングし、BODYタグの全ての全角文字を抽出する
# 全てのblogを走査
contents_lo = ''
for blog in blogs:
    html = req.get(blog).text
    # BeautifulSoupでHTMLを解析
    soup = bs(html, 'html.parser')
    for script in soup(["script", "style"]):
        script.extract()
        # bodyコンテンツ内のinner textのみを抽出
    contents = soup.find('body').get_text()
    # extractで救いきれなかったので、unicodedata.east_asian_widthで全角かどうか判定
    for uniStr in contents:
        str_width = unicodedata.east_asian_width(uniStr)
        if str_width == 'W':
            contents_lo += uniStr

# 3. 2で抽出した全角文字を形態素解析に掛けて、一般名詞のみを抽出する
# 形態素解析にかけて、一般名詞だけを抽出
m = mc.Tagger('mecabrc')
mecab_result = m.parse(contents_lo)
info_of_words = mecab_result.split('\n')
words = []
for info in info_of_words:
    if '\t' in info:
        kind = info.split('\t')[1].split(',')[0]
        category = info.split('\t')[1].split(',')[1]
        if kind == '名詞' and category == '一般' and (info.split('\t')[0] not in EXCLUDE_STR_LIST):
            words.append(info.split('\t')[0])

# 4. 3で取得した一般名詞をTF解析で単語頻出度を算出する
# TF解析で単語数を取得する
vectorizer = CountVectorizer(min_df=1)
tf_idf_result = vectorizer.fit_transform([' '.join(words)])
# スコアの配列を取得
tf_idf_result_array = tf_idf_result.toarray()

# ラベルを取得
feature_names = vectorizer.get_feature_names()

# スコアとラベルが一対になるように辞書型に変換(ついでに小数点の第3位切り捨て)
dic = {}
for k, v in enumerate(feature_names):
    dic[v] = round(tf_idf_result_array[0][k], 3)

# 辞書型をvalueで降順sortしてlistにして返却
sorted_list = sorted(dic.items(), key=lambda x: x[1], reverse=True)
plot_value = np.zeros([10])
plot_label = [0 for i in range(10)]
i = 0
for rank in sorted_list:
    if i >= 10:
        break
    plot_value[i] = rank[1]
    plot_label[i] = rank[0] + '(' + str(rank[1]) + ')'
    i += 1

# 5. 最終的にmatplotlibで円グラフに上位10件を表示する
# マッププロット
prop = matplotlib.font_manager.FontProperties(fname=TTF_PATH, size=14)
pylab.clf()
patches, texts = plt.pie(plot_value, labels=plot_label, counterclock=False, startangle=90)
plt.axis('equal')
pylab.setp(texts, fontproperties=prop)


▼苦労したとこ


1. script.extract()で救いきれないscript


 今回は一般名詞だけ集計したかったので、スクレイピング結果からscriptタグに関する記載は除外したかったのですが、完全には救えず。。
 そのため、苦しい感じではありましたが、「全角の単語に絞ろう」という逃げに走りました。
 


unicodedata.east_asian_width(uniStr)


pythonの標準モジュール「unicodedata」にある「east_asian_width」関数を使って、対象の文字が全角かどうかを判定することで回避することにしました!
https://docs.python.jp/3/library/unicodedata.html

標準でも随分リッチなことが手軽にできて便利だな〜。。。

2. 除外対象単語



ソースコードに思いっきり「EXCLUDE_STR_LIST」って羅列しているのですが、
mecabで使う辞書をカスタマイズすればもっとスマートに管理できるのだろうなと思いました!
今回はネタということで。

3. matplotlibの日本語ラベル豆腐問題



これが地味に一番ハマりました。
matplotlibを標準デフォルトのまま使うと、日本語ttfファイルがlib下に存在しないため、
いわゆる豆腐問題「□」に陥りました。

日本語ttfを「matplotlib/mpl-data/fonts/ttf/」下にコピーして配置し、パスを



prop = matplotlib.font_manager.FontProperties(fname=TTF_PATH, size=14)
pylab.setp(texts, fontproperties=prop)


とすることで、plot時に日本語フォントを見るようにして解決しました。

4. pythonのversion


今回はlocalのmacでサクッとやってたのですが、当初考えなしにpythonいじってたら
macのデフォルトのpythonのversionが2系だとわかり、急遽環境を整えることにしました^^;

そこで今回チョイスしたのはanacondaです。

anacondaにはpythonの開発に必要なモジュールが一通り揃ってますので、楽チンでした。

anacondaをインストールすると、環境変数に自動で以下のように追記されるので、



# added by Anaconda3 4.3.1 installer
export PATH="/Users/hogehoge/anaconda/bin:$PATH"


source ~/.bash_profileでmacで扱うpythonをインストールしたanaconda下を見るようにしました。

anacondaを入れると、「Anaconda-Navigator」というアプリケーションも同胞されるため、
そちらを起動すると、以下のような管理画面が立ち上がるので、



「spyder」というアプリケーションを立ち上げますと、pythonIDE(統合開発環境)が起動します。
今回の円グラフもspyder上のconsole(IPython)で実行、表示させたものを貼り付けてます。

spyderでの作業風景はこんな感じです。



▼最後に・・・



そもそもの話ではありますが、ランキングって円グラフで見せるものなんですかね?^^;
バ〜っと作りきってから、ふと思ってしまいました。。。

Pythonには数値を簡単に扱うライブラリが豊富なので、「あ、これやってみたい」が驚くほど手軽にできてしまうのが魅力だなと感じました。

今回のネタも急に思いついたのですが、次回は機械学習、特にscikit-learn(サイキットラーン)やtensorflowあたりでネタを考えて展開してみようと思います!

拙い文章ではありましたが、読んで頂き本当にありがとうございました!

※クローリングする際は、対象のサイトでクローリングしても問題ないか確認した上で実行するようにしましょう!!

Webスクレイピングが捗るGoutteを使ってみる



シャワー後の水切りでヘドバンしてたら頸椎を痛めてしまいました。あれは絶対やめた方がいいです。と周囲に広めているたきゃはしです。急に本題ですが今回はPHPで簡単にできるWebスクレイピングをご紹介します。

◯ Webスクレイピングとは



Webサイトからデータを抽出するソフトウェア技術のことです。
RSSやWebAPIが公開されていないサイトからでもデータ抽出が出来るようなイメージです。

早速クローラー Goutte(グットゥ) を使って紹介していきたいと思います。
Goutte は Symfony や Twig、Pimple等の開発者として知られるFabienが手がける人気ライブラリです。

◯ インストール




php composer.phar require fabpot/goutte:~2.0

インストールが完了したら vendor/fabpot/goutte/composer.json を覗いてみます...


  ...
  "require": {
    "php": ">=5.4.0",
    "symfony/browser-kit": "~2.1",
    "symfony/css-selector": "~2.1",
    "symfony/dom-crawler": "~2.1",
    "guzzlehttp/guzzle": "4.*"
  },
  ...

BrowserKit に Guzzle とくればどんな機能があるかなんとなく想像がつきますね。
話が逸れますが、使用するライブラリの依存コンポーネントを確認しておくことは大切かなと思います。

◯ 使ってみる





<?php
// first.php
require_once './vendor/autoload.php';

$client = new Goutte\Client();
$crawler = $client->request('GET', 'http://blog.asial.co.jp/');

// 抽出
$targetSelector = 'h2.lh1_2em'; // アシアルブログの見出しのセレクター
$crawler->filter($target)->each(function ($node) {
    echo $node->text() . "\n";
});



~/Sites/prac/goutte  php first.php
  外部コンテンツをiframeサイズで拡大縮小させたり、固定幅コンテンツをウィンドウサイズでピッタリ表示させる方法
  「Monaca for Hybridcast」CEATEC JAPAN 2014(2014/10/7-11開催)にて展示
  お絵描きアプリと画像の保存処理の実装
  PC/スマホでは無いWEBアプリ開発の話 -ハイブリッドキャスト編-
  IllustratorでSVGファイルを保存してみました
  iPhone6 PlusのPSDモックアップとAppleのフォント
  Canvas Fingerprintingというトラッキング技術
  HTML5プロフェッショナル認定試験のセミナー資料を公開します
  弊社田中のコラム「エンタープライズHTML5とバックエンド─エンタープライズ×モバイルアプリ開発の最新動向」が公開されました
  HTML5+CSS3+JSでネイティブGUIアプリが作れる、node-webkitを触ってみる

試しにアシアルブログの記事タイトルを抽出してみました。実質10行も書いてないです。
コードにしれっと出てきた $client と $crawler について確認してみたいと思います。

・$client Goutte\Client



Array
(
    [0] => setClient
    [1] => getClient
    [2] => setHeader
    [3] => removeHeader
    [4] => setAuth
    [5] => resetAuth
    [6] => __construct
    [7] => followRedirects
    [8] => setMaxRedirects
    [9] => insulate
    [10] => setServerParameters
    [11] => setServerParameter
    [12] => getServerParameter
    [13] => getHistory
    [14] => getCookieJar
    [15] => getCrawler
    [16] => getInternalResponse
    [17] => getResponse
    [18] => getInternalRequest
    [19] => getRequest
    [20] => click
    [21] => submit
    [22] => request
    [23] => back
    [24] => forward
    [25] => reload
    [26] => followRedirect
    [27] => restart
)

定義されているメソッドのほとんどがsetter&getterですね。
このオブジェクトの役割はブラウザもしくはページそのものみたいな感覚でいいかなと思います。
click, submit, request, back, forward, reload, restart あたり覚えておけば良さそうです。

・$crawler Symfony\Component\DomCrawler\Crawler



Array
(
    [0] => __construct
    [1] => clear
    [2] => add
    [3] => addContent
    [4] => addHtmlContent
    [5] => addXmlContent
    [6] => addDocument
    [7] => addNodeList
    [8] => addNodes
    [9] => addNode
    [10] => eq
    [11] => each
    [12] => reduce
    [13] => first
    [14] => last
    [15] => siblings
    [16] => nextAll
    [17] => previousAll
    [18] => parents
    [19] => children
    [20] => attr
    [21] => text
    [22] => html
    [23] => extract
    [24] => filterXPath
    [25] => filter
    [26] => selectLink
    [27] => selectButton
    [28] => link
    [29] => links
    [30] => form
    [31] => setDefaultNamespacePrefix
    [32] => registerNamespace
    [33] => xpathLiteral
    [34] => getNode
    [35] => attach
    [36] => detach
    [37] => contains
    [38] => addAll
    [39] => removeAll
    [40] => removeAllExcept
    [41] => getInfo
    [42] => setInfo
    [43] => getHash
    [44] => count
    [45] => rewind
    [46] => valid
    [47] => key
    [48] => current
    [49] => next
    [50] => unserialize
    [51] => serialize
    [52] => offsetExists
    [53] => offsetSet
    [54] => offsetUnset
    [55] => offsetGet
)

見た感じ配列のユーティリティクラスを継承してるようですね。
クローラーオブジェクトの役割はデータ抽出の作業者でよさそうです。
eq, attr, text, html, extract, selectLink, selectButton, link, links, form あたり覚えておけば良さそうです。
配列操作だと each, first, last, filter, contains, count あたりはよく使いそうですね。

さて、クライアントやクローラの内容をちょこっと予習できたところで、他の操作もしてみたいと思います。

・テキストリンクをクリックする




<?php
...
// リンクのテキストをクリックする
$targetLinkText = 'バックエンド(プログラミング)';
$link = $crawler->selectLink($targetLinkText)->link();
$crawler = $client->click($link);

ページの遷移に関しては同期処理となっていてWaitを実装する必要はなさそうです。

・ボタンをクリックする




<?php
...
// ボタンのテキストをクリックする
$targetButtonText = '検索';
$button = $crawler->selectButton($targetButtonText)->form();
$crawler = $client->click($button);

なんとブログにbuttonタグがありませんでした。。。なかったのですが
inputタグでも出来ることが分かって良かったです。(type=imageのaltテキストを対象にしました。)

・フォームを送信する




<?php
...
// 指定テキストで検索する
$targetButtonText = '検索';
$form = $crawler->selectButton($targetButtonText)->form();
$searchParameters = ['words' => 'Monaca'];
$crawler = $client->submit($form, $searchParameters);

formタグのactionやmethodを使用して処理してくれます。

また$clientはリクエスト&レスポンスを持っていました。以下のメソッドが定義されています。

Symfony\Component\BrowserKit\Request



Array
(
    [0] => __construct
    [1] => getUri
    [2] => getMethod
    [3] => getParameters
    [4] => getFiles
    [5] => getCookies
    [6] => getServer
    [7] => getContent
)

Symfony\Component\BrowserKit\Response



Array
(
    [0] => __construct
    [1] => __toString
    [2] => getContent
    [3] => getStatus
    [4] => getHeaders
    [5] => getHeader
)

「なるほどぉ〜」くらいに思ってもらえればと思います。

また記事作成中に知った機能ですが、Chrome Developer Tools > Copy CSS Path(画像参照) がセレクター取得に便利でした。


◯ まとめ



Goutteでスクレイピングしてみて良かったなぁという点は...



  • めちゃくちゃ簡単にスクレイピングできること。

  • 手作業に比べて圧倒的に高速かつ正確なこと。

  • 人間が詳細な作業内容を覚える必要がないこと。

  • 作業という単位でコンポーネント化しやすいので開発がどんどん楽になりそうなこと。

  • ちょっとずれるけど、PHPUnitと連携すれば画面テストにも利用できること。


逆に課題かなぁと感じた点は...



  • プログラムがWebサイトの稼働状況やDOM構造に依存しているため安定稼働が難しいこと。

  • 自動化する場合は対象のDOM構造が変更されていないか監視する機能が必要なこと。

  • 変更があれば効率よく開発者へ通知するためのロギング機能や通知処理が必要なこと。

  • 変更があれば抽出ミスが発生するため、エラー処理やリカバリが必要なこと。


要約すると、すごい便利なんだけど作りこまないと運用大変そうだなぁという感じでした。

今更言うことではないですが、規約に反する行為および抽出データの取り扱いにはくれぐれもご注意を。
それでは皆様、健全で生産的なスクレイピングを楽しみましょう!