PHPでワードクラウドをつくる

(1/1)
ワードクラウド
ワードクラウド(word cloud)とは、与えられたコンテンツを解析し、左図のように出現頻度が高い単語ほど大きく表示する仕組みである。
今回は、与えられた URL のワードクラウドを表示するプログラムを PHP で作ってみることにする。

(2021 年 10 月 2 日)PHP8 対応,リファラ・チェック改良,https 対応

目次

サンプル・プログラムの実行例

ワードクラウド

サンプル・プログラム

圧縮ファイルの内容
WordCloud.phpサンプル・プログラム本体。

必要な機能

このプログラムでは、大きく 3 つの機能が要求される。
  1. URL からコンテンツを取り出す。
  2. コンテンツから単語を切り出し、出現頻度を数える。
  3. 出現頻度の高い単語ほど大きく表示する。
1.は、後述する my_get_contents 関数によって実現する。2.は、「PHP で形態素解析を行う」で使った Yahoo!JAPAN の WebAPI「日本語形態素解析」がそのまま利用できる。
3.は、後述する 関数によって実現する。

解説:準備

0040: //Yahoo! JAPAN Webサービス アプリケーションID【各自で設定】
0041: //取得方法:https://www.pahoo.org/e-soul/webtech/php06/php06-01-02.shtm#Yahoo
0042: define('APPLICATION_ID', '***************************************');

Yahoo!JAPAN の WebAPI「日本語形態素解析」を利用するため、Yahoo! JAPAN Web サービス アプリケーション ID を取得する必要がある。その入手方法は「Yahoo!JAPAN デベロッパーネットワーク - WebAPI の登録方法」を参照されたい。
取得した ID は定数 APPLICATION_ID に格納する。

解説:日本語形態素解析

0271: /**
0272:  * 「Yahoo!JAPAN 日本語形態素解析」を用いてテキストを解析する
0273:  * @param   string $sentence 解析するテキスト
0274:  * @param   array  $items 解析結果を格納する配列
0275:  * @return  bool TRUE:解析成功/FALSE:失敗
0276: */
0277: function getParse($sentence, &$items) {
0278:     //WebAPIにパラメータをPOST渡しする
0279:     $url = REQ_URL;
0280:     $sentence = urlencode($sentence);
0281:     $post = array(
0282:         'appid'       => APPLICATION_ID,
0283:         'results'     => 'ma,uniq',
0284:         'uniq_filter' => '9|10',
0285:         'sentence'    => $sentence
0286:     );
0287: 
0288:     //リクエスト+エラーチェック
0289:     $res = http($url, 'POST', '', $post);
0290:     if ($res == NULL || $res == FALSE || $res == '|| preg_match('/Requested\s*Was\s*Not\s*Found/', $res) > 0) {
0291:         return FALSE;
0292:     }
0293: 
0294: //PHP4用; DOM XML利用
0295:     if (isphp5over() == FALSE) {
0296:         $dom = domxml_open_mem($res);
0297:         //形態素解析の結果
0298:         $ma_result = $dom->get_elements_by_tagname('ma_result');
0299:         if (($word_list = $ma_result[0]->get_elements_by_tagname('word_list')) == NULLreturn FALSE;
0300:         if (($word = $word_list[0]->get_elements_by_tagname('word')) == NULL)   return FALSE;
0301:         foreach ($word as $val) {
0302:             $node = $val->get_elements_by_tagname('surface');
0303:             $surface = (string)$node[0]->get_content();
0304:             $node = $val->get_elements_by_tagname('reading');
0305:             $items[$surface]['reading'] = $node[0]->get_content();
0306:             $node = $val->get_elements_by_tagname('pos');
0307:             $items[$surface]['pos'] = $node[0]->get_content();
0308:             $items[$surface]['count'] = 1;
0309:         }
0310:         //出現頻度の結果
0311:         $ma_result = $dom->get_elements_by_tagname('uniq_result');
0312:         if (($word_list = $ma_result[0]->get_elements_by_tagname('word_list')) == NULLreturn FALSE;
0313:         if (($word = $word_list[0]->get_elements_by_tagname('word')) == NULL)   return FALSE;
0314:         foreach ($word as $val) {
0315:             $node = $val->get_elements_by_tagname('surface');
0316:             $surface = (string)$node[0]->get_content();
0317:             $node = $val->get_elements_by_tagname('count');
0318:             $items[$surface]['count'] = (int)$node[0]->get_content();
0319:         }
0320: 
0321: //PHP5用; SimpleXML利用
0322:     } else {
0323:         $ResultSet = simplexml_load_string($res);
0324:         //形態素解析の結果
0325:         foreach ($ResultSet->ma_result->word_list->word as $val) {
0326:             $surface = (string)$val->surface;
0327:             $items[$surface]['reading'] = $val->reading;
0328:             $items[$surface]['pos']     = $val->pos;
0329:             $items[$surface]['count']   = 1;
0330:         }
0331:         //出現頻度の結果
0332:         foreach ($ResultSet->uniq_result->word_list->word as $val) {
0333:             $surface = (string)$val->surface;
0334:             $items[$surface]['count']   = (int)$val->count;
0335:         }
0336:     }
0337: 
0338:     return TRUE;
0339: }

「URL で与えられたコンテンツから単語を切り出し、出現頻度を数える」処理は、「PHP で形態素解析を行う」で作成したユーザー関数 getParse をほぼそのままの形で使う。

ただし今回、検索対象とするのは名詞だけにすることを考えているので、filter を指定できるように、引数に $filter を追加している。

解説:ワードクラウドの作成

0352: /**
0353:  * ワードクラウドを作成する
0354:  * @param   array  $items 情報を格納した配列
0355:  * @return  string ワードクラウド(HTML)
0356: */
0357: function getWordCloud($items) {
0358:     $max_fsize = 250;            //最大サイズ(%)
0359:     $outstr = '';
0360:     $n = 0;
0361: 
0362:     //出現頻度の最大値・最小値を調べる
0363:     $count_max = 0;
0364:     $count_min = 0;
0365:     foreach ($items as $surface=>$val) {
0366:         if (mb_strlen($surface) > 1) {       //1文字は無視する
0367:             if ($val['count'] > $count_max)     $count_max = $val['count'];
0368:             if ($val['count'] < $count_min)     $count_min = $val['count'];
0369:         }
0370:     }
0371:     $k = 30 / ($count_max - $count_min);
0372: 
0373:     foreach ($items as $surface=>$val) {
0374:         if (mb_strlen($surface) > 1) {       //1文字は無視する
0375:             $n++;
0376:             $count = $val['count'];
0377:             $fsize = $k * $val['count'] * $val['count'] + 80;
0378:             if ($fsize > $max_fsize)    $fsize = $max_fsize;
0379:             $link = get_link($surface);
0380:             if ($n > 1)     $outstr .= ",\n";
0381: $outstr .=<<< EOT
0382: <span style="font-size: {$fsize}%;">
0383: <a href={$link}>{$surface}</a>
0384: </span>
0385: 
0386: EOT;
0387:         }
0388:     }
0389:     return $outstr;
0390: }

ユーザー関数 getWordCloudワードクラウドを作成する。

単語の最多出現頻度を C1、最小出現頻度を C2、ある単語の出現頻度を N、表示する文字フォントサイズ(%)を S とすると、  mimetex  で計算することにした。
これは、ある単語の出現回数 N が増えると、回数の二乗に比例して文字サイズが大きくなるというものである。当初、単純比例で大きくしようとしたのだが、それでは頻度による大きさの違いがそれほど目立たなかったので、二乗に比例するようにした。
また、あまりにも大きくなりすぎるのも困るので、変数 $max_fsize で制限を設けるようにした。

0341: /**
0342:  * ワードクラウド表示時における個々の単語のリンク先URLを取得する
0343:  * @param   string $surface 見出し語
0344: */
0345: function get_link($surface) {
0346:     $url  = 'https://www.google.com/search?hl=ja&lr=lang_ja&q=';    //Google検索
0347:     $site = SEARCH_DOMAIN;       //検索絞り込みドメイン名
0348: 
0349:     return $url . urlencode($surface) . '+site%3A' . $site;
0350: }

ただ文字サイズを変更して並べるだけでは面白くないので、個々の単語にリンクを張ることにした。
ここでは、その単語を Google 検索に投げ、その際に site で絞り込み検索を指定するようにした。$site には自サイトの URL を入れておけばいいだろう。

解説:コンテンツの取り出し

0248: /**
0249:  * コンテンツの必要な部分を読み込む
0250:  * @param   string $url コンテンツのURL
0251:  * @return  string 読み込んだコンテンツ/FALSE=失敗
0252: */
0253: function my_get_contents($url) {
0254:     $instr = @file_get_contents($url);
0255:     $instr = mb_convert_encoding($instrINTERNAL_ENCODING, 'auto');
0256:     if ($instr == FALSE || $instr == '')   return FALSE;
0257: 
0258:     //scriptを除く
0259:     $arr = array();
0260:     $ss = preg_match('/\<body.*?\>(.*?)<\/body\>/imsu', $instr$arr);
0261:     $ss = preg_replace('/\<script.*?\>.*?<\/script\>/imsu', '', $arr[1]);
0262:     $ss = preg_replace('/\<style.*?\>.*?<\/style\>/imsu', '', $ss);
0263:     $ss = preg_replace('/\&nbsp\;/imsu', '', $ss);     //&nbsp;を除く
0264:     $ss = strip_tags($ss);                               //タグを除く
0265:     $ss = html_entity_decode($ss);           //特殊文字を通常文字に変換
0266:     $outstr = preg_replace('/\n|\r|\r|\t/', '', $ss);  //改行、タブを除く
0267: 
0268:     return $outstr;
0269: }

ユーザー関数 getParse に渡すコンテンツを URL から取り出す必要がある。これには少し工夫が必要だ。

まず、URL の全体を getParse に渡すのは非合理的だ。ヘッダやフッタなど、コンテンツとして分析する必要がない部分も含まれてしまう。

そこで用意したのがユーザー関数 my_get_contents である。

まず、関数  file_get_contents  を使ってコンテンツ全体を変数 $instr に取り込む。
次に、 preg_match  を使って、<body> タグ内だけ切り出す。
続いて、<script> タグや<style> タグ内は省く。特殊文字 &nbsp; 無視する。
最後に、関数  strip_tags 、 html_entity_decode 、 preg_replace  を使い、HTML や PHP のタグ、特殊文字、改行・タブなどを消去しておく。

ユーザー関数 my_get_contents については、解析したサイトの状況に合わせて変更してほしい。

参考サイト

(この項おわり)
header