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

(1/1)
NHK選挙WEB(2020年3月までは「NHK政治意識月例調査」)に、内閣支持率や政党支持率が公開されている。PHPを用いて、これらの数値データを取り出し、グラフに表示するプログラムを作る。応答速度を向上するため、一度読み込んだデータはXMLファイルとしてローカルに保存しておく。

(2021年5月8日)2020年4月以降に対応,PHP8対応。POLLS_FILE へのセーブ/ロード。

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

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

目次

サンプル・プログラム

圧縮ファイルの内容
viewPolls.phpサンプル・プログラム本体。
polls.xml内閣支持率・政党支持率のデータ・ファイル。

解説:準備

0021: //jqPlotのあるフォルダ
0022: define('JQPLOT', '../../../../common/jqplot/');
0023: 
0024: //描画期間(年)
0025: define('PERIOD', 7);
0026: 
0027: //最古データ(年)
0028: define('OLDYEAR', 1998);
0029: 
0030: //表示幅(ピクセル)
0031: define('WIDTH', 600);
0032: 
0033: //表示高さ(ピクセル)
0034: define('HEIGHT', 500);
0035: 
0036: //保存用データファイル名
0037: define('POLLS_FILE', './polls.xml');

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

また、描画期間(当年から過去何年分遡るか)を定数 PERIOD に定義しておく。
なお、スクレイピングしたデータはXML形式ファイルとして定数 POLLS_FILE で示すファイルにセーブし、次に描画するときは、このファイルからロードできるようにした。

解説:スクレイピング

0354: /**
0355:  * 内閣支持率を解析する
0356:  * @param int $infp 解析中のURL
0357:  * @param array $items 解析結果を格納する配列
0358:  * @return bool TRUE/FALSE
0359: */
0360: function analyseCabinet($infp, &$items) {
0361:     $pat1 = "/<h[0-9]+[^>]*>内閣支持率<\/h[0-9]+>/u";
0362:     $pat2 = "/<tr[^>]*>/u";
0363:     $pat3 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
0364:     $pat31 = "/<td[^>]*>(.+)/u";
0365:     $pat4 = "/<t[hd][^>]*>支持(<[^>]+>)?する/u";
0366:     $pat5 = "/<t[hd][^>]*>(<[^>]+>)?支持(<[^>]+>)?しない/u";
0367:     $pat6 = "/<t[hd][^>]*>内閣/u";
0368:     $pat61 = "/<\/table>/u";
0369:     $pat7 = "/<td colspan\=\"([0-9]+)\"[^>]*>([^<]+)/u";
0370:     $pat8 = "/<td[^>]*>([^<]+)/u";
0371:     $pat9 = "/<h[0-9]+[^>]*>.*政党支持率<\/h[0-9]+>/u";
0372:     $flag = 0;
0373:     while (! feof($infp)) {
0374:         $instr = trim(fgets($infp));
0375:         if (($flag == 0) && (preg_match($pat1$instr) > 0)) {
0376:             $flag = 1;
0377:         } else if (($flag == 1) && (preg_match($pat2$instr) > 0)) {
0378:             $flag = 2;
0379:             $i = 0;
0380:         //月
0381:         } else if ($flag == 2) {
0382:             if (preg_match($pat4$instr) > 0) {
0383:                 $flag = 3;
0384:                 $i = 0;
0385:             } else if (preg_match($pat3$instr$arr) > 0) {
0386:                 $items[$i]['month']   = $arr[2];
0387:                 $items[$i]['yes']     = '-';
0388:                 $items[$i]['no']      = '-';
0389:                 $items[$i]['cabinet'] = '-';
0390:                 $i++;
0391:             }
0392:         //支持する
0393:         } else if ($flag == 3) {
0394:             if (preg_match($pat5$instr) > 0) {
0395:                 $flag = 4;
0396:                 $i = 0;
0397:             } else if (preg_match($pat31$instr$arr) > 0) {
0398:                 $items[$i]['yes'] = strip_tags($arr[1]);
0399:                 $i++;
0400:             }
0401:         //支持しない
0402:         } else if ($flag == 4) {
0403:             if (preg_match($pat31$instr$arr) > 0) {
0404:                 $items[$i]['no'] = strip_tags($arr[1]);
0405:                 $i++;
0406:             } else if (preg_match($pat6$instr) > 0) {
0407:                 $flag = 5;
0408:                 $i = 0;
0409:             } else if (preg_match($pat61$instr) > 0) {
0410:                 $flag = 5;
0411:                 $i = 0;
0412:             }
0413:         //内閣
0414:         } else if ($flag == 5) {
0415:             if (preg_match($pat7$instr$arr) > 0) {
0416:                 $n = $i + $arr[1];
0417:                 while ($i < $n) {
0418:                     $items[$i]['cabinet'] = $arr[2];
0419:                     $i++;
0420:                 }
0421:             } else if (preg_match($pat8$instr$arr) > 0) {
0422:                 $items[$i]['cabinet'] = $arr[1];
0423:                 $i++;
0424:             } else if (preg_match($pat9$instr) > 0) {
0425:                 $flag = 6;
0426:             }
0427:         } else if ($flag == 6) {
0428:             break;
0429:         }
0430:     }
0431: 
0432:     return TRUE;
0433: }

0520: /**
0521:  * 政党支持率を解析する
0522:  * @param int   $infp  解析中のURL
0523:  * @param array $items 解析結果を格納する配列
0524:  * @return bool TRUE/FALSE
0525: */
0526: function analyseParty($infp, &$items) {
0527:     $pat1 = "/<tr[^>]*>/u";
0528:     $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
0529:     $pat3 = "/<\/tr>/u";
0530:     $pat4 = "/<t[hd][^>]*>([^<]+)/u";
0531:     $pat5 = "/<\/table>/u";
0532:     $flag = 0;
0533:     while (! feof($infp)) {
0534:         $instr = trim(fgets($infp));
0535:         if (($flag == 0) && (preg_match($pat1$instr) > 0)) {
0536:             $flag = 1;
0537:             $i = 0;
0538:         //月
0539:         } else if ($flag == 1) {
0540:             if (preg_match($pat2$instr$arr) > 0) {
0541:                 $items[$i]['month'] = $arr[2];
0542:                 $i++;
0543:             } else if (preg_match($pat3$instr) > 0) {
0544:                 $flag = 2;
0545:                 $i = 0;
0546:             }
0547:         //政党支持率
0548:         } else if ($flag == 2) {
0549:             if (preg_match($pat2$instr$arr) > 0) {
0550:                 $items[$i][$party] = $arr[2];
0551:                 $i++;
0552:             } else if (preg_match($pat4$instr$arr) > 0) {
0553:                 $party = $arr[1];
0554:                 $i = 0;
0555:             } else if (preg_match($pat5$instr) > 0) {
0556:                 $flag = 3;
0557:             }
0558:         } else if ($flag == 3) {
0559:             break;
0560:         }
0561:     }
0562:     return TRUE;
0563: }

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

0435: /**
0436:  * 内閣支持率・政党支持率を解析する【2020年1月以降】
0437:  * @param int   $infp  解析中のURL
0438:  * @param array $items内閣支持率の解析結果を格納する配列
0439:  * @param array $items政党支持率の解析結果を格納する配列
0440:  * @return bool TRUE/FALSE
0441: */
0442: function analyse2020($infp, &$items1, &$items2) {
0443:     $pat01 = "/<i\s+class\=\"far\s+fa\-calendar\-alt\"><\/i>[0-9]+年([0-9]+)月<span>/ui";
0444:     $pat11 = "/<h3>内閣支持([0-9]+)[%\%]、不支持([0-9]+)[%\%]/ui";
0445:     $pat12 = "/(\p{Han}+内閣)を「支持する/ui";
0446:     $pat21 = "/<h2><span>政党支持率<\/span>/ui";
0447:     $pat22 = "/<td>([^<]+)<\/td>/ui";
0448:     $pat23 = "/<td\s+class\=\"right\">([0-9\.]+)<\/td>/ui";
0449: 
0450:     $flag = FALSE;
0451:     while (! feof($infp)) {
0452:         $instr1 = trim(fgets($infp));
0453:         $instr2 = strip_tags($instr1);
0454:         //月
0455:         if (preg_match($pat01$instr1$arr) > 0) {
0456:             $month = (int)$arr[1];
0457:         //内閣支持率・不支持率
0458:         } else if (preg_match($pat11$instr1$arr) > 0) {
0459:             $items1[$month - 1]['month'] = (int)$month;
0460:             $items1[$month - 1]['yes']   = (int)$arr[1];
0461:             $items1[$month - 1]['no']    = (int)$arr[2];
0462:         //内閣名
0463:         } else if (preg_match($pat12$instr2$arr) > 0) {
0464:             $items1[$month - 1]['cabinet'] = (string)$arr[1];
0465:         //政党支持率の解析開始
0466:         } else if (preg_match($pat21$instr1$arr) > 0) {
0467:             $flag = TRUE;
0468:             $items2[$month - 1]['month'] = (int)$month;
0469:         //政党名
0470:         } else if ($flag && (preg_match($pat22$instr1$arr) > 0)) {
0471:             $party = (string)$arr[1];
0472:             if ($party == '特に支持している政党はない') {
0473:                 $party = '支持政党なし';
0474:             }
0475:         //支持率
0476:         } else if ($flag && (preg_match($pat23$instr1$arr) > 0)) {
0477:             $items2[$month - 1][$party] = (string)$arr[1];
0478:         }
0479:     }
0480: 
0481:     return TRUE;
0482: }

2020年(令和2年)4月以降は、NHK選挙WEBに移動し、ページのレイアウトが大きく変わったので、別のユーザー関数 analyse2020 を用意した。スクレイピングしたデータは、同様に配列に格納する。

解説:XMLへの格納

0484: /**
0485:  * 内閣支持率をXMLオブジェクトに追加/更新する
0486:  * @param array  $items 解析結果を格納した配列
0487:  * @param int    $year  西暦年
0488:  * @param object $xml   XMLオブジェクト
0489:  * @return bool TRUE/FALSE
0490: */
0491: function addCabinet($items$year, &$xml) {
0492:     //西暦年の頭出し
0493:     $flag = FALSE;
0494:     foreach ($xml->research as $elem1) {
0495:         if ($elem1->year == $year) {
0496:             break;
0497:         }
0498:     }
0499:     //月次処理
0500:     foreach ($items as $item) {
0501:         foreach ($elem1->article as $elem2) {
0502:             if ($elem2->month == $item['month']) {
0503:                 $flag = TRUE;
0504:                 break;
0505:             }
0506:         }
0507:         if (! $flag) {
0508:             $elem2 = $elem1->addChild('article');
0509:             $elem2->addChild('month', $item['month']);
0510:         }
0511:         if (! isset($elem2->cabinet)) {
0512:             $elem2->addChild('cabinet');
0513:             $elem2->cabinet->addChild('name', $item['cabinet']);
0514:             $elem2->cabinet->addChild('yes', $item['yes']);
0515:             $elem2->cabinet->addChild('no', $item['no']);
0516:         }
0517:     }
0518: }

0565: /**
0566:  * 政党支持率をXMLオブジェクトに追加/更新する
0567:  * @param array  $items 解析結果を格納した配列
0568:  * @param int    $year  西暦年
0569:  * @param object $xml   XMLオブジェクト
0570:  * @return bool TRUE/FALSE
0571: */
0572: function addParty($items$year, &$xml) {
0573:     //西暦年の頭出し
0574:     $flag = FALSE;
0575:     foreach ($xml->research as $elem1) {
0576:         if ($elem1->year == $year) {
0577:             break;
0578:         }
0579:     }
0580: 
0581:     //月次処理
0582:     foreach ($items as $item) {
0583:         foreach ($elem1->article as $elem2) {
0584:             if (isset($item['month']) && ($elem2->month == $item['month'])) {
0585:                 $flag = TRUE;
0586:                 break;
0587:             }
0588:         }
0589:         if (! $flag && isset($item['month'])) {
0590:             $elem2 = $elem1->addChild('article');
0591:             $elem2->addChild('month', $item['month']);
0592:         }
0593: 
0594:         foreach ($item as $key=>$val) {
0595:             if ($key == 'month')    continue;
0596:             $flag = FALSE;
0597:             foreach ($elem2->party as $elem3) {
0598:                 if (isset($elem3->name) && ($elem3->name == $key)) {
0599:                     $flag = TRUE;
0600:                     break;
0601:                 }
0602:             }
0603:             if (! $flag) {
0604:                 $elem4 = $elem2->addChild('party');
0605:                 $elem4->addChild('name', $key);
0606:                 $elem4->addChild('yes', $val);
0607:             }
0608:         }
0609:     }
0610: }

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

解説:XMLファイルへのセーブとロード

0317: /**
0318:  * データ・ファイルを書き込む
0319:  * @param string $fname出力ファイル名
0320:  * @param object XMLオブジェクト
0321:  * @return bool TRUE/FALSE
0322: */
0323: function writeDataFile($fname$xml) {
0324:     $str = $xml->asXML();
0325:     $str = cleanUpXML($str);
0326:     $outfp = fopen($fname, 'w');
0327:     fwrite($outfp$str);
0328: 
0329:     return fclose($outfp);
0330: }

0290: /**
0291:  * データ・ファイルを読み込む
0292:  * @param string $fname入力ファイル名
0293:  * @return object XMLオブジェクト/FALSE:読み込み失敗
0294: */
0295: function readDataFile($fname) {
0296: $xmlstr =<<< EOT
0297: <?xml version="1.0encoding="utf-8" ?>
0298: <polls>
0299: </polls>
0300: 
0301: EOT;
0302:     if (! isphp5over())  return FALSE;        //PHP5以上でないと動作しない
0303: 
0304:     //ファイル無し
0305:     if (! file_exists($fname)) {
0306:         $xml = new SimpleXMLElement($xmlstr);
0307: 
0308:     //ファイル有り
0309:     } else {
0310:         $xml = simplexml_load_file($fname);
0311:         if ($xml == FALSE)      return FALSE;
0312:     }
0313: 
0314:     return $xml;
0315: }

XMLオブジェクトをXMLファイルへセーブするユーザー関数は writeDataFile、ロードするユーザー関数は readDataFile である。

解説:jqPlotスクリプト

0728: /**
0729:  * jqPlot用のスクリプト:内閣支持率
0730:  * @param object $xml XMLオブジェクト
0731:  * @param int    $start  開始年
0732:  * @param int    $finish 終了年
0733:  * @return stringスクリプト
0734: */
0735: function plotCabinet($xml$start$finish) {
0736:     //系列の生成
0737:     $series1 = '';      //内閣支持率
0738:     $series2 = '';      //内閣府支持率
0739:     $old = '';
0740:     foreach ($xml as $research) {
0741:         //描画範囲内かどうか
0742:         $year = $research->year;
0743:         if (($year < $start|| ($year > $finish))  continue;
0744:         //描画データ作成
0745:         foreach ($research as $article) {
0746:             $month = $article->month;
0747:             if ($month >= 1 && $month <= 12) {
0748:                 $name = '';
0749:                 if ($old != (string)$article->cabinet->name) {
0750:                     $name = (string)$article->cabinet->name;
0751:                     $old  = (string)$article->cabinet->name;
0752:                 }
0753:                 $yes = $article->cabinet->yes;
0754:                 $no  = $article->cabinet->no;
0755:                 if ($month > 0 && $yes > 0) {
0756:                     $series1 .= sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year$month$yes$name);
0757:                     $series2 .= sprintf("['%04d-%02d-01',%d,''],", $year$month$no);
0758:                 }
0759:             }
0760:         }
0761:     }
0762: 
0763:     //X軸の最小値
0764:     $xmin = sprintf('%04d-01-01', $start);
0765:     //X軸の最大値
0766:     if (date('Y') == $finish) {
0767:         $xmax = date('Y-m-01', strtotime('next month'));
0768:      } else {
0769:         $xmax = sprintf('%04d-12-31', $finish);
0770:     }
0771: 
0772:     //グラフ描画
0773: $js =<<< EOT
0774: jQuery(function() {
0775:     jQuery.jqplot('jqPlot_polls',
0776:     [
0777:         [ {$series1} ],
0778:         [ {$series2} ]
0779:     ],
0780:     {
0781:         //系列
0782:         series: [
0783:             { label: '支持率',   color: '#88CC44' },
0784:             { label: '不支持率', color: '#CC4488'  }
0785:         ],
0786:         legend: {
0787:             show: true,
0788:             placement: 'inside',
0789:             location: 'sw',
0790:             renderer: $.jqplot.EnhancedLegendRenderer,
0791:             rendererOptions: { numberRows: 1 }
0792:         },
0793:         seriesDefaults: {
0794:             showLine: true,
0795:             rendererOptions: { smooth: false },
0796:             markerOptions: { size: 0 },
0797:             pointLabels: {
0798:                 show :true,
0799:                 escapeHTML: false,
0800:                 location: 'ne'
0801:             }
0802:         },
0803:         //軸
0804:         axes: {
0805:             xaxis: {
0806:                 renderer:$.jqplot.DateAxisRenderer,
0807:                 min: '{$xmin}',
0808:                 max: '{$xmax}',
0809:                 tickOptions: { formatString: '%Y/%#m' },
0810:                 label: '年月',
0811:             },
0812:             yaxis: {
0813:                 label: '支持率(%)'
0814:             }
0815:         },
0816:         //ハイライター
0817:         highlighter: {
0818:             show: true,
0819:             showMarker: true,
0820:             tooltipLocation: 'sw',
0821:             fadeTooltip: false,
0822:             bringSeriesToFront: true,
0823:             tooltipAxes: 'xy',
0824:             formatString: '%s<br />(不)支持率%s%'
0825:         }
0826:     }
0827:     );
0828: });
0829: 
0830: EOT;
0831: 
0832:     return $js;
0833: }

0835: /**
0836:  * jqPlot用のスクリプト:政党支持率
0837:  * @param object $xml XMLオブジェクト
0838:  * @param int $start  開始年
0839:  * @param int $finish 終了年
0840:  * @return stringスクリプト
0841: */
0842: function plotParty($xml$start$finish) {
0843:     //プロットする政党名
0844:     static $table = array(
0845: '自民党', '公明党', '共産党', '民進党', '立憲民主党', '民主党',
0846: '支持政党なし');
0847: 
0848:     //系列の生成
0849:     $str2 = '';
0850:     foreach ($table as $key=>$val) {
0851:         $str2 .= "\t\t\t{ label: '{$val}' },\n";
0852:         $series[] = '';
0853:     }
0854: 
0855:     foreach ($xml as $research) {
0856:         $year = (int)$research->year;
0857:         //描画範囲内かどうか
0858:         if (($year < $start|| ($year > $finish))  continue;
0859:         //描画データ作成
0860:         foreach ($research as $article) {
0861:             $month = (int)$article->month;
0862:             foreach ($article as $party) {
0863:                 foreach ($table as $key=>$val) {
0864:                     if ($val == (string)$party->name) {
0865:                         if (preg_match('/[0-9\.]+/', $party->yes) > 0 && $year > 0 && $month >= 1 && $month <= 12) {
0866:                             $series[$key] .= sprintf("['%04d-%02d-01',%4.1f],", $year$month, (double)$party->yes);
0867:                         }
0868:                     }
0869:                 }
0870:             }
0871:         }
0872:     }
0873:     $str1 = '';
0874:     foreach ($series as $val) {
0875:         $str1 .= "\t\t[ " . $val . "],\n";
0876:     }
0877: 
0878:     //X軸の最小値
0879:     $xmin = sprintf('%04d-01-01', $start);
0880:     if (date('Y') == $finish) {
0881:         $xmax = date('Y-m-01', strtotime('next month'));
0882:      } else {
0883:         $xmax = sprintf('%04d-12-31', $finish);
0884:     }
0885: 
0886:     //グラフ描画
0887: $js =<<< EOT
0888: jQuery(function() {
0889:     jQuery.jqplot('jqPlot_polls',
0890:     [
0891: {$str1}
0892:     ],
0893:     {
0894:         //系列
0895:         series: [
0896: {$str2}
0897:         ],
0898:         legend: {
0899:             show: true,
0900:             placement: 'inside',
0901:             location: 'sw',
0902:             renderer: $.jqplot.EnhancedLegendRenderer,
0903:             rendererOptions: { numberRows: 1 }
0904:         },
0905:         seriesDefaults: {
0906:             showLine: true,
0907:             rendererOptions: { smooth: false },
0908:             markerOptions: { size: 0 },
0909:         },
0910:         //軸
0911:         axes: {
0912:             xaxis: {
0913:                 renderer: $.jqplot.DateAxisRenderer,
0914:                 min: '{$xmin}',
0915:                 max: '{$xmax}',
0916:                 tickOptions: { formatString: '%Y/%#m' },
0917:                 label: '年月',
0918:             },
0919:             yaxis: {
0920:                 label: '支持率(%)'
0921:             }
0922:         },
0923:         //ハイライター
0924:         highlighter: {
0925:             show: true,
0926:             showMarker: true,
0927:             tooltipLocation: 'sw',
0928:             fadeTooltip: false,
0929:             bringSeriesToFront: true,
0930:             tooltipAxes: 'xy',
0931:             formatString: '%s<br />支持率%s%'
0932:         }
0933:     }
0934:     );
0935: });
0936: 
0937: EOT;
0938: 
0939:     return $js;
0940: }

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

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

参考サイト

(この項おわり)
header