PHPでNHK政治意識月例調査をグラフ表示

(1/1)
NHK 政治意識月例調査に、内閣支持率や政党支持率が公開されている。PHP を用いて、これらの数値データを取り出し、グラフに表示してみる。

サンプル・プログラム

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

NHK政治意識月例調査をグラフ表示

解説:準備

0021: //jqPlotのあるフォルダ
0022: define('JQPLOT', '../../../../common/jqplot/');
0023: 
0024: //NHK政治意識月齢調査URL
0025: define('URL_NHK', 'http://www.nhk.or.jp/bunken/yoron/political/');
0026: 
0027: //描画期間(年)
0028: define('PERIOD', 7);
0029: 
0030: //最古データ(年)
0031: define('OLDYEAR', 1998);
0032: 
0033: //保存用データファイル名(未使用)
0034: define('POLLS_FILE', './polls.xml');

今回は折れ線グラフを描くのに、jQuery プラグイン「jqPlot」を利用する。
公式サイト「jqPlot Charts and Graphs for jQuery」から圧縮ファイルをダウンロードし、適当な場所に解凍しておく。その場所を定数 JQPLOT に定義する。

また、NHK 政治意識月例調査の URL は定数 URL_NHK に、描画期間(当年から過去何年分遡るか)を定数 PERIOD に定義しておく。
なお、スクレイピングしたデータは XML 形式にして保管することを想定しているが、保存ファイル名を定義した定数 POLLS_FILE は未使用である。

解説:スクレイピング

0301: /**
0302:  * 内閣支持率を解析する
0303:  * @param int $infp 解析中のURL
0304:  * @param array $items 解析結果を格納する配列
0305:  * @return bool TRUE/FALSE
0306: */
0307: function analyseCabinet($infp, &$items) {
0308:     $pat1 = "/<h[0-9]+[^>]*>内閣支持率<\/h[0-9]+>/u";
0309:     $pat2 = "/<tr[^>]*>/u";
0310:     $pat3 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
0311:     $pat31 = "/<td[^>]*>(.+)/u";
0312:     $pat4 = "/<t[hd][^>]*>支持(<[^>]+>)?する/u";
0313:     $pat5 = "/<t[hd][^>]*>(<[^>]+>)?支持(<[^>]+>)?しない/u";
0314:     $pat6 = "/<t[hd][^>]*>内閣/u";
0315:     $pat61 = "/<\/table>/u";
0316:     $pat7 = "/<td colspan\=\"([0-9]+)\"[^>]*>([^<]+)/u";
0317:     $pat8 = "/<td[^>]*>([^<]+)/u";
0318:     $pat9 = "/<h[0-9]+[^>]*>.*政党支持率<\/h[0-9]+>/u";
0319:     $flag = 0;
0320:     while (! feof($infp)) {
0321:         $instr = trim(fgets($infp));
0322:         if (($flag == 0) && (preg_match($pat1$instr) > 0)) {
0323:             $flag = 1;
0324:         } else if (($flag == 1) && (preg_match($pat2$instr) > 0)) {
0325:             $flag = 2;
0326:             $i = 0;
0327:         //月
0328:         } else if ($flag == 2) {
0329:             if (preg_match($pat4$instr) > 0) {
0330:                 $flag = 3;
0331:                 $i = 0;
0332:             } else if (preg_match($pat3$instr$arr) > 0) {
0333:                 $items[$i]['month']   = $arr[2];
0334:                 $items[$i]['yes']     = '-';
0335:                 $items[$i]['no']      = '-';
0336:                 $items[$i]['cabinet'] = '-';
0337:                 $i++;
0338:             }
0339:         //支持する
0340:         } else if ($flag == 3) {
0341:             if (preg_match($pat5$instr) > 0) {
0342:                 $flag = 4;
0343:                 $i = 0;
0344:             } else if (preg_match($pat31$instr$arr) > 0) {
0345:                 $items[$i]['yes'] = strip_tags($arr[1]);
0346:                 $i++;
0347:             }
0348:         //支持しない
0349:         } else if ($flag == 4) {
0350:             if (preg_match($pat31$instr$arr) > 0) {
0351:                 $items[$i]['no'] = strip_tags($arr[1]);
0352:                 $i++;
0353:             } else if (preg_match($pat6$instr) > 0) {
0354:                 $flag = 5;
0355:                 $i = 0;
0356:             } else if (preg_match($pat61$instr) > 0) {
0357:                 $flag = 5;
0358:                 $i = 0;
0359:             }
0360:         //内閣
0361:         } else if ($flag == 5) {
0362:             if (preg_match($pat7$instr$arr) > 0) {
0363:                 $n = $i + $arr[1];
0364:                 while ($i < $n) {
0365:                     $items[$i]['cabinet'] = $arr[2];
0366:                     $i++;
0367:                 }
0368:             } else if (preg_match($pat8$instr$arr) > 0) {
0369:                 $items[$i]['cabinet'] = $arr[1];
0370:                 $i++;
0371:             } else if (preg_match($pat9$instr) > 0) {
0372:                 $flag = 6;
0373:             }
0374:         } else if ($flag == 6) {
0375:             break;
0376:         }
0377:     }
0378: 
0379:     return TRUE;
0380: }

0418: /**
0419:  * 政党支持率を解析する
0420:  * @param int   $infp  解析中のURL
0421:  * @param array $items 解析結果を格納する配列
0422:  * @return bool TRUE/FALSE
0423: */
0424: function analyseParty($infp, &$items) {
0425:     $pat1 = "/<tr[^>]*>/u";
0426:     $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
0427:     $pat3 = "/<\/tr>/u";
0428:     $pat4 = "/<t[hd][^>]*>([^<]+)/u";
0429:     $pat5 = "/<\/table>/u";
0430:     $flag = 0;
0431:     while (! feof($infp)) {
0432:         $instr = trim(fgets($infp));
0433:         if (($flag == 0) && (preg_match($pat1$instr) > 0)) {
0434:             $flag = 1;
0435:             $i = 0;
0436:         //月
0437:         } else if ($flag == 1) {
0438:             if (preg_match($pat2$instr$arr) > 0) {
0439:                 $items[$i]['month'] = $arr[2];
0440:                 $i++;
0441:             } else if (preg_match($pat3$instr) > 0) {
0442:                 $flag = 2;
0443:                 $i = 0;
0444:             }
0445:         //政党支持率
0446:         } else if ($flag == 2) {
0447:             if (preg_match($pat2$instr$arr) > 0) {
0448:                 $items[$i][$party] = $arr[2];
0449:                 $i++;
0450:             } else if (preg_match($pat4$instr$arr) > 0) {
0451:                 $party = $arr[1];
0452:                 $i = 0;
0453:             } else if (preg_match($pat5$instr) > 0) {
0454:                 $flag = 3;
0455:             }
0456:         } else if ($flag == 3) {
0457:             break;
0458:         }
0459:     }
0460:     return TRUE;
0461: }

NHK 政治意識月例調査をスクレイピングする処理は、ユーザー関数 analyseCabinet および analyseParty で行う。1行ずつ読み込み、正規表現を使ってデータを分離していく。
スクレイピングしたデータは、配列に格納する。

解説:XMLへの格納

0382: /**
0383:  * 内閣支持率をXMLオブジェクトに追加/更新する
0384:  * @param array  $items 解析結果を格納した配列
0385:  * @param int    $year  西暦年
0386:  * @param object $xml   XMLオブジェクト
0387:  * @return bool TRUE/FALSE
0388: */
0389: function addCabinet($items$year, &$xml) {
0390:     //西暦年の頭出し
0391:     $flag = FALSE;
0392:     foreach ($xml->research as $elem1) {
0393:         if ($elem1->year == $year) {
0394:             break;
0395:         }
0396:     }
0397:     //月次処理
0398:     foreach ($items as $item) {
0399:         foreach ($elem1->article as $elem2) {
0400:             if ($elem2->month == $item['month']) {
0401:                 $flag = TRUE;
0402:                 break;
0403:             }
0404:         }
0405:         if (! $flag) {
0406:             $elem2 = $elem1->addChild('article');
0407:             $elem2->addChild('month', $item['month']);
0408:         }
0409:         if (! isset($elem2->cabinet)) {
0410:             $elem2->addChild('cabinet');
0411:             $elem2->cabinet->addChild('name', $item['cabinet']);
0412:             $elem2->cabinet->addChild('yes', $item['yes']);
0413:             $elem2->cabinet->addChild('no', $item['no']);
0414:         }
0415:     }
0416: }

0463: /**
0464:  * 政党支持率をXMLオブジェクトに追加/更新する
0465:  * @param array  $items 解析結果を格納した配列
0466:  * @param int    $year  西暦年
0467:  * @param object $xml   XMLオブジェクト
0468:  * @return bool TRUE/FALSE
0469: */
0470: function addParty($items$year, &$xml) {
0471:     //西暦年の頭出し
0472:     $flag = FALSE;
0473:     foreach ($xml->research as $elem1) {
0474:         if ($elem1->year == $year) {
0475:             break;
0476:         }
0477:     }
0478: 
0479:     //月次処理
0480:     foreach ($items as $item) {
0481:         foreach ($elem1->article as $elem2) {
0482:             if (isset($item['month']) && ($elem2->month == $item['month'])) {
0483:                 $flag = TRUE;
0484:                 break;
0485:             }
0486:         }
0487:         if (! $flag && isset($item['month'])) {
0488:             $elem2 = $elem1->addChild('article');
0489:             $elem2->addChild('month', $item['month']);
0490:         }
0491: 
0492:         foreach ($item as $key=>$val) {
0493:             if ($key == 'month')    continue;
0494:             $flag = FALSE;
0495:             foreach ($elem2->party as $elem3) {
0496:                 if (isset($elem3->name) && ($elem3->name == $key)) {
0497:                     $flag = TRUE;
0498:                     break;
0499:                 }
0500:             }
0501:             if (! $flag) {
0502:                 $elem4 = $elem2->addChild('party');
0503:                 $elem4->addChild('name', $key);
0504:                 $elem4->addChild('yes', $val);
0505:             }
0506:         }
0507:     }
0508: }

配列に格納したデータを、XML オブジェクトへ展開していく処理は、ユーザー関数 addCabinet および addParty で行う。

解説:jqPlotスクリプト

0542: /**
0543:  * jqPlot用のスクリプト:内閣支持率
0544:  * @param object $xml XMLオブジェクト
0545:  * @return string スクリプト
0546: */
0547: function plotCabinet($xml) {
0548:     //系列の生成
0549:     $series1 = '';      //内閣支持率
0550:     $series2 = '';      //内閣府支持率
0551:     $old = '';
0552:     foreach ($xml as $research) {
0553:         $year = $research->year;
0554:         foreach ($research as $article) {
0555:             $month = $article->month;
0556:             if ($month >= 1 && $month <= 12) {
0557:                 $name = '';
0558:                 if ($old != (string)$article->cabinet->name) {
0559:                     $name = (string)$article->cabinet->name;
0560:                     $old  = (string)$article->cabinet->name;
0561:                 }
0562:                 $yes = $article->cabinet->yes;
0563:                 $no  = $article->cabinet->no;
0564:                 if ($month > 0 && $yes > 0) {
0565:                     $series1 .= sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year$month$yes$name);
0566:                     $series2 .= sprintf("['%04d-%02d-01',%d,''],", $year$month$no);
0567:                 }
0568:             }
0569:         }
0570:     }
0571: 
0572:     //X軸の最小値
0573:     $xmin = sprintf('%04d-01-01', $xml->research[0]->year);
0574:     //X軸の最大値
0575:     if ($year == date('Y')) {
0576:         $xmax = date('Y-m-01', strtotime('next month'));
0577:     } else {
0578:         $xmax = sprintf('%04d-12-31', $year);
0579:     }
0580: 
0581:     //グラフ描画
0582: $js =<<< EOD
0583: jQuery(function() {
0584:     jQuery.jqplot('jqPlot_polls',
0585:     [
0586:         [ {$series1} ],
0587:         [ {$series2} ]
0588:     ],
0589:     {
0590:         //系列
0591:         series: [
0592:             { label: '支持率',   color: '#88CC44' },
0593:             { label: '不支持率', color: '#CC4488'  }
0594:         ],
0595:         legend: {
0596:             show: true,
0597:             placement: 'inside',
0598:             location: 'sw',
0599:             renderer: $.jqplot.EnhancedLegendRenderer,
0600:             rendererOptions: { numberRows: 1 }
0601:         },
0602:         seriesDefaults: {
0603:             showLine: true,
0604:             rendererOptions: { smooth: false },
0605:             markerOptions: { size: 0 },
0606:             pointLabels: {
0607:                 show :true,
0608:                 escapeHTML: false,
0609:                 location: 'ne'
0610:             }
0611:         },
0612:         //軸
0613:         axes: {
0614:             xaxis: {
0615:                 renderer:$.jqplot.DateAxisRenderer,
0616:                 min: '{$xmin}',
0617:                 max: '{$xmax}',
0618:                 tickOptions: { formatString: '%Y/%#m' },
0619:                 label: '年月',
0620:             },
0621:             yaxis: {
0622:                 label: '支持率(%)'
0623:             }
0624:         },
0625:         //ハイライター
0626:         highlighter: {
0627:             show: true,
0628:             showMarker: true,
0629:             tooltipLocation: 'sw',
0630:             fadeTooltip: false,
0631:             bringSeriesToFront: true,
0632:             tooltipAxes: 'xy',
0633:             formatString: '%s<br />(不)支持率%s%'
0634:         }
0635:     }
0636:     );
0637: });
0638: 
0639: EOD;
0640: 
0641:     return $js;
0642: }
0643: 
0644: /**
0645:  * jqPlot用のスクリプト:政党支持率
0646:  * @param object $xml XMLオブジェクト
0647:  * @return string スクリプト
0648: */
0649: function plotParty($xml) {
0650:     //プロットする政党名
0651:     static $table = array(
0652: '自民党', '共産党', '民進党', '立憲民主党', '民主党',
0653: '支持政党なし');
0654: 
0655:     //系列の生成
0656:     $str2 = '';
0657:     foreach ($table as $key=>$val) {
0658:         $str2 .= "\t\t\t{ label: '{$val}' },\n";
0659:         $series[] = '';
0660:     }
0661: 
0662:     foreach ($xml as $research) {
0663:         $year = (int)$research->year;
0664:         foreach ($research as $article) {
0665:             $month = (int)$article->month;
0666:             foreach ($article as $party) {
0667:                 foreach ($table as $key=>$val) {
0668:                     if ($val == (string)$party->name) {
0669:                         if (preg_match('/[0-9\.]+/', $party->yes) > 0 && $year > 0 && $month >= 1 && $month <= 12) {
0670:                             $series[$key] .= sprintf("['%04d-%02d-01',%4.1f],", $year$month, (double)$party->yes);
0671:                         }
0672:                     }
0673:                 }
0674:             }
0675:         }
0676:     }
0677:     $str1 = '';
0678:     foreach ($series as $val) {
0679:         $str1 .= "\t\t[ " . $val . "],\n";
0680:     }
0681: 
0682:     //X軸の最小値
0683:     $xmin = sprintf('%04d-01-01', $xml->research[0]->year);
0684:     //X軸の最大値
0685:     if ($year == date('Y')) {
0686:         $xmax = date('Y-m-01', strtotime('next month'));
0687:     } else {
0688:         $xmax = sprintf('%04d-12-31', $year);
0689:     }
0690: 
0691:     //グラフ描画
0692: $js =<<< EOD
0693: jQuery(function() {
0694:     jQuery.jqplot('jqPlot_polls',
0695:     [
0696: {$str1}
0697:     ],
0698:     {
0699:         //系列
0700:         series: [
0701: {$str2}
0702:         ],
0703:         legend: {
0704:             show: true,
0705:             placement: 'inside',
0706:             location: 'sw',
0707:             renderer: $.jqplot.EnhancedLegendRenderer,
0708:             rendererOptions: { numberRows: 1 }
0709:         },
0710:         seriesDefaults: {
0711:             showLine: true,
0712:             rendererOptions: { smooth: false },
0713:             markerOptions: { size: 0 },
0714:         },
0715:         //軸
0716:         axes: {
0717:             xaxis: {
0718:                 renderer: $.jqplot.DateAxisRenderer,
0719:                 min: '{$xmin}',
0720:                 max: '{$xmax}',
0721:                 tickOptions: { formatString: '%Y/%#m' },
0722:                 label: '年月',
0723:             },
0724:             yaxis: {
0725:                 label: '支持率(%)'
0726:             }
0727:         },
0728:         //ハイライター
0729:         highlighter: {
0730:             show: true,
0731:             showMarker: true,
0732:             tooltipLocation: 'sw',
0733:             fadeTooltip: false,
0734:             bringSeriesToFront: true,
0735:             tooltipAxes: 'xy',
0736:             formatString: '%s<br />支持率%s%'
0737:         }
0738:     }
0739:     );
0740: });
0741: 
0742: EOD;
0743: 
0744:     return $js;
0745: }

XML オブジェクトに格納したデータを、jqPlot スクリプトへ展開していく処理は、ユーザー関数 plotCabinet および plotParty で行う。

ここでは、jqPlot プラグインの「jqplot.dateAxisRenderer.js」(時間軸を扱う)、「jqplot.highlighter.js」(ツールチップなどハイライト表示を行う)、「jqplot.pointLabels.js」(データポイントラベルを表示する)の 3 つを使っている。
jqPlot の使い方は、「jqPlot - jQuery プラグイン」(アルファシス)が詳しい。

参考サイト

(この項おわり)
header