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

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

(2026年8月12日)新党4党(中道改革連合・チームみらい・いのちの党・日本保守党)の解析・グラフ表示に対応

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

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

目次

サンプル・プログラム

圧縮ファイルの内容
viewPolls.phpサンプル・プログラム本体。
polls.xml内閣支持率・政党支持率のデータ・ファイル。
圧縮ファイルの内容
viewPolls.phpサンプル・プログラム本体。
viewPolls.php 更新履歴
バージョン 更新日 内容
3.1.0 2026/08/12 新党4党の解析・グラフ表示に対応
3.0.0 2026/05/14 データ読み込み方法を大幅改訂
2.2.0 2026/01/24 PHP8.5対応:curl_closeを使わない
3.5.0 2025/05/24 2024~2025年のHTML表記ゆれに対応
3.4.0 2024/05/12 HTMLヘッダ見直し

解説:準備

viewPolls.php

   1: <?php
   2: /** viewPolls.php
   3:  * NHK政治意識月例調査をグラフ表示
   4:  *
   5:  * @copyright   (c)studio pahoo
   6:  * @author      パパぱふぅ
   7:  * @動作環境    PHP 5/7/8
   8:  * @参考URL     https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-15-01.shtm
   9:  *
  10: */
  11: // 初期化処理 ================================================================
  12: define('INTERNAL_ENCODING', 'UTF-8');
  13: define('MYSELF', basename($_SERVER['SCRIPT_NAME']));
  14: define('REFERENCE', 'https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-15-01.shtm');
  15: mb_internal_encoding(INTERNAL_ENCODING);
  16: mb_regex_encoding(INTERNAL_ENCODING);
  17: 
  18: // プログラム・タイトル
  19: define('TITLE', 'NHK政治意識月例調査をグラフ表示');
  20: 
  21: // 各種定数(START) ===========================================================
  22: 
  23: // 描画期間(年)
  24: define('PERIOD', 3);
  25: 
  26: // 最古データ(年)
  27: define('OLDYEAR', 1998);
  28: 
  29: // グラフを描くDOMオブジェジェクト名
  30: define('GRAPH_ID', 'jqPlot_polls');
  31: 
  32: // 表示幅(ピクセル)
  33: define('WIDTH', 600);
  34: 
  35: // 表示高さ(ピクセル)
  36: define('HEIGHT', 500);
  37: 
  38: // 保存用データファイル名
  39: define('POLLS_FILE', './polls.xml');

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

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

解説:スクレイピング

viewPolls.php

 510: /**
 511:  * 内閣支持率を解析する
 512:  * @param   string $contents 読み込んだコンテンツ
 513:  * @param   array  $items    解析結果を格納する配列
 514:  * @return  bool TRUE/FALSE
 515: */
 516: function analyseCabinet($contents, &$items) {
 517:     $pat1 = "/<h[0-9]+[^>]*>内閣支持率<\/h[0-9]+>/u";
 518:     $pat2 = "/<tr[^>]*>/u";
 519:     $pat3 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
 520:     $pat31 = "/<td[^>]*>(.+)/u";
 521:     $pat4 = "/<t[hd][^>]*>支持(<[^>]+>)?する/u";
 522:     $pat5 = "/<t[hd][^>]*>(<[^>]+>)?支持(<[^>]+>)?しない/u";
 523:     $pat6 = "/<t[hd][^>]*>内閣/u";
 524:     $pat61 = "/<\/table>/u";
 525:     $pat7 = "/<td colspan\=\"([0-9]+)\"[^>]*>([^<]+)/u";
 526:     $pat8 = "/<td[^>]*>([^<]+)/u";
 527:     $pat9 = "/<h[0-9]+[^>]*>.*政党支持率<\/h[0-9]+>/u";
 528:     $flag = 0;
 529:     foreach (explode("\n", $contentsas $line) {
 530:         $instr = trim($line);
 531:         if (($flag == 0&& (preg_match($pat1, $instr> 0)) {
 532:             $flag = 1;
 533:         } else if (($flag == 1&& (preg_match($pat2, $instr> 0)) {
 534:             $flag = 2;
 535:             $i = 0;
 536:         // 
 537:         } else if ($flag == 2) {
 538:             if (preg_match($pat4, $instr> 0) {
 539:                 $flag = 3;
 540:                 $i = 0;
 541:             } else if (preg_match($pat3, $instr, $arr> 0) {
 542:                 $items[$i]['month']   = $arr[2];
 543:                 $items[$i]['yes']     = '-';
 544:                 $items[$i]['no']      = '-';
 545:                 $items[$i]['cabinet'] = '-';
 546:                 $i++;
 547:             }
 548:         // 支持する
 549:         } else if ($flag == 3) {
 550:             if (preg_match($pat5, $instr> 0) {
 551:                 $flag = 4;
 552:                 $i = 0;
 553:             } else if (preg_match($pat31, $instr, $arr> 0) {
 554:                 $items[$i]['yes'] = strip_tags($arr[1]);
 555:                 $i++;
 556:             }
 557:         // 支持しない
 558:         } else if ($flag == 4) {
 559:             if (preg_match($pat31, $instr, $arr> 0) {
 560:                 $items[$i]['no'] = strip_tags($arr[1]);
 561:                 $i++;
 562:             } else if (preg_match($pat6, $instr> 0) {
 563:                 $flag = 5;
 564:                 $i = 0;
 565:             } else if (preg_match($pat61, $instr> 0) {
 566:                 $flag = 5;
 567:                 $i = 0;
 568:             }
 569:         // 内閣
 570:         } else if ($flag == 5) {
 571:             if (preg_match($pat7, $instr, $arr> 0) {
 572:                 $n = $i + $arr[1];
 573:                 while ($i < $n) {
 574:                     $items[$i]['cabinet'] = $arr[2];
 575:                     $i++;
 576:                 }
 577:             } else if (preg_match($pat8, $instr, $arr> 0) {
 578:                 $items[$i]['cabinet'] = $arr[1];
 579:                 $i++;
 580:             } else if (preg_match($pat9, $instr> 0) {
 581:                 $flag = 6;
 582:             }
 583:         } else if ($flag == 6) {
 584:             break;
 585:         }
 586:     }
 587: 
 588:     return TRUE;
 589: }

viewPolls.php

 697: /**
 698:  * 政党支持率を解析する
 699:  * @param   string $contents 読み込んだコンテンツ
 700:  * @param   array  $items    解析結果を格納する配列
 701:  * @return  bool TRUE/FALSE
 702: */
 703: function analyseParty($contents, &$items) {
 704:     $pat1 = "/<tr[^>]*>/u";
 705:     $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
 706:     $pat3 = "/<\/tr>/u";
 707:     $pat4 = "/<t[hd][^>]*>([^<]+)/u";
 708:     $pat5 = "/<\/table>/u";
 709:     $flag = 0;
 710:     foreach (explode("\n", $contentsas $line) {
 711:         $instr = trim($line);
 712:         if (($flag == 0&& (preg_match($pat1, $instr> 0)) {
 713:             $flag = 1;
 714:             $i = 0;
 715:         // 
 716:         } else if ($flag == 1) {
 717:             if (preg_match($pat2, $instr, $arr> 0) {
 718:                 $items[$i]['month'] = $arr[2];
 719:                 $i++;
 720:             } else if (preg_match($pat3, $instr> 0) {
 721:                 $flag = 2;
 722:                 $i = 0;
 723:             }
 724:         // 政党支持率
 725:         } else if ($flag == 2) {
 726:             if (preg_match($pat2, $instr, $arr> 0) {
 727:                 $items[$i][$party] = $arr[2];
 728:                 $i++;
 729:             } else if (preg_match($pat4, $instr, $arr> 0) {
 730:                 $party = $arr[1];
 731:                 $i = 0;
 732:             } else if (preg_match($pat5, $instr> 0) {
 733:                 $flag = 3;
 734:             }
 735:         } else if ($flag == 3) {
 736:             break;
 737:         }
 738:     }
 739:     return TRUE;
 740: }

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

viewPolls.php

 591: /**
 592:  * 内閣支持率・政党支持率を解析する【2020年1月以降】
 593:  * @param   string $contents 読み込んだコンテンツ
 594:  * @param   array  $items    内閣支持率の解析結果を格納する配列
 595:  * @param   array  $items    政党支持率の解析結果を格納する配列
 596:  * @return  bool TRUE/FALSE
 597: */
 598: function analyse2020($contents, &$items1, &$items2) {
 599:     // プロットする政党名
 600:     static $table = array(
 601:     '', '自民党', '立憲民主党', '日本維新の会', '公明党', '共産党', '国民民主党', 'れいわ新選組', 'いのちの党', '社民党', 'みんなでつくる党', '参政党', '中道改革連合', 'チームみらい', '日本保守党', '特に支持している政党はない', '支持政党なし');
 602: 
 603:     $pat01 = "/<i\s+class\=\"far\s+fa\-calendar\-alt\"><\/i>[0-9]+年([0-9]+)月(<span>|\s*)/ui";
 604:     $pat11 = "/<h3>内閣支持([0-9]+)[%\%]、不支持([0-9]+)[%\%]/ui";
 605: //  $pat12 = "/(\p{Han}+内閣)を「支持する/ui";
 606:     $pat12 = "/([一-龠]+内閣)を「支持する/ui";
 607:     $pat21 = "/<h2><span>政党支持率<\/span>/ui";
 608:     $pat31 = "/<td>([^<]+)<\/td>/ui";
 609:     $pat32 = "/<td\s+class\=\"right\">([0-9\.]+)<\/td>/ui";
 610:     // 2023年8月以降
 611:     $pat22 = "/各党の支持率は/ui";
 612:     $pat33 = '/「([^」]+)」が([0-9\.]+)[%\%]/ui';
 613: 
 614:     $flag = FALSE;
 615:     foreach (explode("\n", $contentsas $line) {
 616:         $instr1 = trim($line);
 617:         $instr2 = strip_tags($instr1);
 618:         // 
 619:         if (preg_match($pat01, $instr1, $arr> 0) {
 620:             $month = (int)$arr[1];
 621:         // 内閣支持率・不支持率
 622:         } else if (preg_match($pat11, $instr1, $arr> 0) {
 623:             $items1[$month - 1]['month'] = (int)$month;
 624:             $items1[$month - 1]['yes']   = (int)$arr[1];
 625:             $items1[$month - 1]['no']    = (int)$arr[2];
 626:         // 内閣名
 627:         } else if (preg_match($pat12, $instr2, $arr> 0) {
 628:             $items1[$month - 1]['cabinet'] = (string)$arr[1];
 629:         // 政党名と支持率 2023年8月以降
 630:         } else if (preg_match($pat22, $instr1> 0) {
 631:             $items2[$month - 1]['month'] = (int)$month;
 632:             if (preg_match_all($pat33, $instr1, $arr, PREG_PATTERN_ORDER> 0) {
 633:                 foreach ($arr[1as $key=>$party) {
 634:                     if ($party == '特に支持している政党はない') {
 635:                         $party = '支持政党なし';
 636:                     }
 637:                     if (array_search($party, $table!FALSE) {
 638:                         $items2[$month - 1][$party] = (string)$arr[2][$key];
 639:                     }
 640:                 }
 641:             }
 642:             break;
 643:         // 政党支持率の解析開始
 644:         } else if (preg_match($pat21, $instr1, $arr> 0) {
 645:             $flag = TRUE;
 646:             $items2[$month - 1]['month'] = (int)$month;
 647:         // 政党名
 648:         } else if ($flag && (preg_match($pat31, $instr1, $arr> 0)) {
 649:             $party = (string)$arr[1];
 650:             if ($party == '特に支持している政党はない') {
 651:                 $party = '支持政党なし';
 652:             }
 653:         // 支持率
 654:         } else if ($flag && (preg_match($pat32, $instr1, $arr> 0)) {
 655:             $items2[$month - 1][$party] = (string)$arr[1];
 656:         }
 657:     }
 658:     return TRUE;
 659: }

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

解説:XMLへの格納

viewPolls.php

 661: /**
 662:  * 内閣支持率をXMLオブジェクトに追加/更新する
 663:  * @param   array  $items 解析結果を格納した配列
 664:  * @param   int    $year  西暦年
 665:  * @param   object $xml   XMLオブジェクト
 666:  * @return  bool TRUE/FALSE
 667: */
 668: function addCabinet($items, $year, &$xml) {
 669:     // 西暦年の頭出し
 670:     $flag = FALSE;
 671:     foreach ($xml->research as $elem1) {
 672:         if ($elem1->year == $year) {
 673:             break;
 674:         }
 675:     }
 676:     // 月次処理
 677:     foreach ($items as $item) {
 678:         foreach ($elem1->article as $elem2) {
 679:             if ($elem2->month == $item['month']) {
 680:                 $flag = TRUE;
 681:                 break;
 682:             }
 683:         }
 684:         if (! $flag) {
 685:             $elem2 = $elem1->addChild('article');
 686:             $elem2->addChild('month', $item['month']);
 687:         }
 688:         if (! isset($elem2->cabinet)) {
 689:             $elem2->addChild('cabinet');
 690:             $elem2->cabinet->addChild('name', $item['cabinet']);
 691:             $elem2->cabinet->addChild('yes', $item['yes']);
 692:             $elem2->cabinet->addChild('no', $item['no']);
 693:         }
 694:     }
 695: }

viewPolls.php

 742: /**
 743:  * 政党支持率をXMLオブジェクトに追加/更新する
 744:  * @param   array  $items 解析結果を格納した配列
 745:  * @param   int    $year  西暦年
 746:  * @param   object $xml   XMLオブジェクト
 747:  * @return  bool TRUE/FALSE
 748: */
 749: function addParty($items, $year, &$xml) {
 750:     // 西暦年の頭出し
 751:     $flag = FALSE;
 752:     foreach ($xml->research as $elem1) {
 753:         if ($elem1->year == $year) {
 754:             break;
 755:         }
 756:     }
 757: 
 758:     // 月次処理
 759:     foreach ($items as $item) {
 760:         foreach ($elem1->article as $elem2) {
 761:             if (isset($item['month']) && ($elem2->month == $item['month'])) {
 762:                 $flag = TRUE;
 763:                 break;
 764:             }
 765:         }
 766:         if (! $flag && isset($item['month'])) {
 767:             $elem2 = $elem1->addChild('article');
 768:             $elem2->addChild('month', $item['month']);
 769:         }
 770: 
 771:         foreach ($item as $key=>$val) {
 772:             if ($key == 'month')    continue;
 773:             $flag = FALSE;
 774:             foreach ($elem2->party as $elem3) {
 775:                 if (isset($elem3->name&& ($elem3->name == $key)) {
 776:                     $flag = TRUE;
 777:                     break;
 778:                 }
 779:             }
 780:             if (! $flag) {
 781:                 $elem4 = $elem2->addChild('party');
 782:                 $elem4->addChild('name', $key);
 783:                 $elem4->addChild('yes', $val);
 784:             }
 785:         }
 786:     }
 787: }

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

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

viewPolls.php

 473: /**
 474:  * データ・ファイルを書き込む
 475:  * @param   string $fname 出力ファイル名
 476:  * @param   object XMLオブジェクト
 477:  * @return  bool TRUE/FALSE
 478: */
 479: function writeDataFile($fname, $xml) {
 480:     $str = $xml->asXML();
 481:     $str = cleanUpXML($str);
 482:     $outfp = fopen($fname, 'w');
 483:     fwrite($outfp, $str);
 484: 
 485:     return fclose($outfp);
 486: }

viewPolls.php

 446: /**
 447:  * データ・ファイルを読み込む
 448:  * @param   string $fname 入力ファイル名
 449:  * @return  object XMLオブジェクト/FALSE:読み込み失敗
 450: */
 451: function readDataFile($fname) {
 452:     $xmlstr =<<< EOT
 453: <?xml version="1.0" encoding="utf-8" ?>
 454: <polls>
 455: </polls>
 456: 
 457: EOT;
 458:     if (! isphp5over()) return FALSE;       // PHP5以上でないと動作しない
 459: 
 460:     // ファイル無し
 461:     if (! file_exists($fname)) {
 462:         $xml = new SimpleXMLElement($xmlstr);
 463: 
 464:     // ファイル有り
 465:     } else {
 466:         $xml = simplexml_load_file($fname);
 467:         if ($xml == FALSE)      return FALSE;
 468:     }
 469: 
 470:     return $xml;
 471: }

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

解説:jqPlotの導入

グラフを描くために、jqPlot というJavaScriptのオープンソース・ライブラリを使用する。PHP側でデータを描画スクリプトを生成し、クライアント側(ブラウザ)でグラフを描かせる。

公式サイト http://www.jqplot.com/ から最新のライブラリをダウンロードしたら、本プログラム冒頭の定数 JQPLOT で示すディレクトリへ解凍する。jqPlotjQUery UI を使うので、jQueryも定数 JQPLOT で示すディレクトリへ格納してほしい。jQuery 1.7.1 以降のmin版があればよい。また、jqPlot は canvas文を使うのでHTML5対応ブラウザがあった方がいいが、IE8以前にも対応するよう代替スクリプトを用意している。

viewPolls.php

   1: <?php
   2: /** viewPolls.php
   3:  * NHK政治意識月例調査をグラフ表示
   4:  *
   5:  * @copyright   (c)studio pahoo
   6:  * @author      パパぱふぅ
   7:  * @動作環境    PHP 5/7/8
   8:  * @参考URL     https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-15-01.shtm
   9:  *
  10: */
  11: // 初期化処理 ================================================================
  12: define('INTERNAL_ENCODING', 'UTF-8');
  13: define('MYSELF', basename($_SERVER['SCRIPT_NAME']));
  14: define('REFERENCE', 'https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-15-01.shtm');
  15: mb_internal_encoding(INTERNAL_ENCODING);
  16: mb_regex_encoding(INTERNAL_ENCODING);
  17: 
  18: // プログラム・タイトル
  19: define('TITLE', 'NHK政治意識月例調査をグラフ表示');
  20: 
  21: // 各種定数(START) ===========================================================
  22: 
  23: // 描画期間(年)
  24: define('PERIOD', 3);
  25: 
  26: // 最古データ(年)
  27: define('OLDYEAR', 1998);
  28: 
  29: // グラフを描くDOMオブジェジェクト名
  30: define('GRAPH_ID', 'jqPlot_polls');
  31: 
  32: // 表示幅(ピクセル)
  33: define('WIDTH', 600);
  34: 
  35: // 表示高さ(ピクセル)
  36: define('HEIGHT', 500);
  37: 
  38: // 保存用データファイル名
  39: define('POLLS_FILE', './polls.xml');
  40: 
  41: // 偽装ユーザー
  42: define('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
  43: 
  44: // NHK政治意識月例調査URL【変更不可】
  45: define('URL_NHK',  'http://www.nhk.or.jp/bunken/yoron/political/');
  46: 
  47: // NHK選挙WEB URL【変更不可】
  48: define('URL2_NHK',  'https://www.nhk.or.jp/senkyo/shijiritsu/');
  49: 
  50: // 各種定数(END) ===============================================================
  51: 
  52: // リファラチェック+リリースフラグの設定
  53: if (isset($_SERVER['HTTP_HOST']) && ($_SERVER['HTTP_HOST'] == 'localhost')) {
  54:     define('FLAG_RELEASE', FALSE);
  55:     define('REFER_ON', '');
  56:     ini_set('display_errors', 1);
  57:     ini_set('error_reporting', E_ALL);
  58: else {
  59:     // リリース・フラグ(公開時にはTRUEにすること)
  60:     define('FLAG_RELEASE', TRUE);
  61:     // リファラ・チェック(直リン防止用;空文字ならチェックしない)
  62:     define('REFER_ON', 'www.pahoo.org');
  63: }
  64: 
  65: /**
  66:  * 処理選択肢
  67:  * @global array $SelectFuncs
  68: */
  69: $SelectFuncs = array(
  70:     // 関数名               タイトル             ラジオボタンcheked
  71:     'plotCabinet' => array('title'=>'内閣支持率', 'checked'=>'checked'),
  72:     'plotParty'   => array('title'=>'政党支持率', 'checked'=>'')
  73: );
  74: 
  75: /**
  76:  * 共通HTMLヘッダ
  77:  * @global string $HtmlHeader
  78: */
  79: $encode = INTERNAL_ENCODING;
  80: $title  = TITLE;
  81: $width  = WIDTH;
  82: $HtmlHeader =<<< EOT
  83: <!DOCTYPE html>
  84: <html lang="ja">
  85: <head>
  86: <meta charset="{$encode}">
  87: <title>{$title}</title>
  88: <meta name="author" content="studio pahoo">
  89: <meta name="copyright" content="studio pahoo">
  90: <meta name="ROBOTS" content="NOINDEX,NOFOLLOW">
  91: <meta http-equiv="pragma" content="no-cache">
  92: <meta http-equiv="cache-control" content="no-cache">
  93: <meta http-equiv="X-UA-Compatible" content="IE=edge">
  94: <meta name="viewport" content="width={$width},user-scalable=yes">
  95: <!-- jqPlot 本体(START) -->
  96: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css">

jqPlot 関連のライブラリとスタイルシートを読み込むのが、PHPプログラムの上述の部分になる。
ここでは、jqPlotプラグインの「jqplot.dateAxisRenderer.js」(時間軸を扱う)、「jqplot.highlighter.js」(ツールチップなどハイライト表示を行う)、「jqplot.pointLabels.js」(データポイントラベルを表示する)の3つを使っている。
このように、jqPlotは使いたい機能に応じてJavaScriptのプラグインを読み込む形をとる。
jqPlotの使い方は、「jqPlot - jQuery プラグイン」(アルファシス)が詳しい。

解説:jqPlotスクリプト

viewPolls.php

 926: /**
 927:  * jqPlot用のスクリプト:内閣支持率
 928:  * @param   object $xml XMLオブジェクト
 929:  * @param   int    $start  開始年
 930:  * @param   int    $finish 終了年
 931:  * @return  string スクリプト
 932: */
 933: function plotCabinet($xml, $start, $finish) {
 934:     $graphId = GRAPH_ID;
 935: 
 936:     // 系列の生成
 937:     $series1 = '';      // 内閣支持率
 938:     $series2 = '';      // 内閣不支持率
 939:     $old = '';
 940:     foreach ($xml as $research) {
 941:         // 描画範囲内かどうか
 942:         $year = $research->year;
 943:         if (($year < $start|| ($year > $finish))   continue;
 944:         // 描画データ作成
 945:         foreach ($research as $article) {
 946:             $month = $article->month;
 947:             if ($month >1 && $month <12) {
 948:                 $name = '';
 949:                 if ($old != (string)$article->cabinet->name) {
 950:                     $name = (string)$article->cabinet->name;
 951:                     $old  = (string)$article->cabinet->name;
 952:                 }
 953:                 $yes = $article->cabinet->yes;
 954:                 $no  = $article->cabinet->no;
 955:                 if ($month > 0 && $yes > 0) {
 956:                     $series1 .sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year, $month, $yes, $name);
 957:                     $series2 .sprintf("['%04d-%02d-01',%d,''],", $year, $month, $no);
 958:                 }
 959:             }
 960:         }
 961:     }
 962: 
 963:     // X軸の最小値
 964:     $xmin = sprintf('%04d-01-01', $start);
 965:     // X軸の最大値
 966:     if (date('Y') == $finish) {
 967:         $xmax = date('Y-m-01', strtotime('next month'));
 968:      } else {
 969:         $xmax = sprintf('%04d-12-31', $finish);
 970:     }
 971: 
 972:     // グラフ描画
 973:     $js =<<< EOT
 974: jQuery(function() {
 975:     jQuery.jqplot('{$graphId}',
 976:     [
 977:         [ {$series1} ],
 978:         [ {$series2} ]
 979:     ],
 980:     {
 981:         // 系列
 982:         series: [
 983:             { label: '支持率',   color: '#88CC44' },
 984:             { label: '不支持率', color: '#CC4488'  }
 985:         ],
 986:         legend: {
 987:             show: true,
 988:             placement: 'inside',
 989:             location: 'sw',
 990:             renderer: $.jqplot.EnhancedLegendRenderer,
 991:             rendererOptions: { numberRows: 1 }
 992:         },
 993:         seriesDefaults: {
 994:             showLine: true,
 995:             rendererOptions: { smooth: false },
 996:             markerOptions: { size: 0 },
 997:             pointLabels: {
 998:                 show :true,
 999:                 escapeHTML: false,
1000:                 location: 'ne'
1001:             }
1002:         },
1003:         // 軸
1004:         axes: {
1005:             xaxis: {
1006:                 renderer:$.jqplot.DateAxisRenderer,
1007:                 min: '{$xmin}',
1008:                 max: '{$xmax}',
1009:                 tickOptions: { formatString: '%Y/%#m' },
1010:                 label: '年月',
1011:             },
1012:             yaxis: {
1013:                 label: '支持率(%)'
1014:             }
1015:         },
1016:         // ハイライター
1017:         highlighter: {
1018:             show: true,
1019:             showMarker: true,
1020:             tooltipLocation: 'sw',
1021:             fadeTooltip: false,
1022:             bringSeriesToFront: true,
1023:             tooltipAxes: 'xy',
1024:             formatString: '%s<br>(不)支持率%s%'
1025:         }
1026:     }
1027:     );
1028: });
1029: 
1030: EOT;
1031: 
1032:     return $js;
1033: }

内閣支持率は、ユーザー関数 plotCabinet により、XMLオブジェクトに格納したデータから jqPlot を使って折れ線グラフに描く。

内閣支持率と不支持率の2本の折れ線グラフを描くのだが、各々、プロットする1つの点を "[x, y]" で表し、これをカンマで区切った文字列として生成する。ここでは "[日付, パーセント]" の形式で、支持率を変数 $seriese1 へ、不支持率を 変数 $seriese2 へ格納していく。

jqPlotスクリプトは jQuery(function() 以降の部分になる。
まず、グラフを描画する領域のDOMオブジェクト名を設定する。本プログラムでは、HTML側のdivタグで表示をしており、グラフの縦横サイズはdivタグのスタイルとして指定する。
上述のプロットデータ(文字列)はオブジェクト series に格納する。
オブジェクト legend凡例の表示方法を指定する。表示場所(placement)はグラフの内側(inside)で、表示位置を南西(sw;下右)に、表示は1列(numberRows: 1)にする。表示を1列にするのに rendererOptionsを使うので、あわせて拡張機能 renderer: $.jqplot.EnhancedLegendRenderer を指定する。
オブジェクト seriesDefaultsグラフの表示方法を指定する。折れ線(showLine)を表示し(true)、折れ線のスムーズ処理(smooth)は行わず(false)、点(markerOptions)をプロットしない(size: 0)。
オブジェクト axesX軸のラベルや目盛りの表示方法を指定する。X軸の最小値(min)は変数 $xmin を指定し、最大値(max)は $xmax を指定する。目盛りの書式(formatString)は年/月(%Y/%#m)にして、ラベル(label)は年月にする。ここで、目盛りの書式を使うために、拡張機能 renderer: $.jqplot.DateAxisRenderer を指定する。Y軸については、ラベルのみを表示する。
オブジェクト highlighter は、折れ線の上にマウスを乗せるデータを表示するハイライト表示する方法を指定する。ハイライターを表示し(show: true)、マーカーも表示し(showMarker: true)、表示位置(tooltipLocation)は南西(sw;下右)に、フェード効果(fadeTooltip)は無効に(false)、ハイライト表示した系列を他の系列よりも前面に移動し(bringSeriesToFront: true)、ツールチップにXとYの値を表示し(tooltipAxes: 'xy')、表示書式(formatString)は '%s
支持率%s%' に指定する。

viewPolls.php

1035: /**
1036:  * jqPlot用のスクリプト:政党支持率
1037:  * @param   object $xml XMLオブジェクト
1038:  * @param   int $start  開始年
1039:  * @param   int $finish 終了年
1040:  * @return  string スクリプト
1041: */
1042: function plotParty($xml, $start, $finish) {
1043:     $graphId = GRAPH_ID;
1044: 
1045:     // プロットする政党名
1046:     static $table = array(
1047: '自民党', '公明党', '共産党', '民進党', '立憲民主党', '民主党', '日本維新の会', '国民民主党', 'れいわ新選組', 'いのちの党', '参政党', '中道改革連合', 'チームみらい', '日本保守党', '支持政党なし');
1048: 
1049:     // 系列の生成
1050:     $str2 = '';
1051:     foreach ($table as $key=>$val) {
1052:         $str2 ."\t\t\t{ label: '{$val}' },\n";
1053:         $series[] = '';
1054:     }
1055: 
1056:     foreach ($xml as $research) {
1057:         $year = (int)$research->year;
1058:         // 描画範囲内かどうか
1059:         if (($year < $start|| ($year > $finish))   continue;
1060:         // 描画データ作成
1061:         foreach ($research as $article) {
1062:             $month = (int)$article->month;
1063:             foreach ($article as $party) {
1064:                 foreach ($table as $key=>$val) {
1065:                     if ($val == (string)$party->name) {
1066:                         if (preg_match('/[0-9\.]+/', $party->yes> 0 && $year > 0 && $month >1 && $month <12) {
1067:                             $series[$key.sprintf("['%04d-%02d-01',%4.1f],", $year, $month, (float)$party->yes);
1068:                         }
1069:                     }
1070:                 }
1071:             }
1072:         }
1073:     }
1074:     $str1 = '';
1075:     foreach ($series as $val) {
1076:         $str1 ."\t\t[ " . $val . "],\n";
1077:     }
1078: 
1079:     // X軸の最小値
1080:     $xmin = sprintf('%04d-01-01', $start);
1081:     if (date('Y') == $finish) {
1082:         $xmax = date('Y-m-01', strtotime('next month'));
1083:      } else {
1084:         $xmax = sprintf('%04d-12-31', $finish);
1085:     }
1086: 
1087:     // グラフ描画
1088:     $js =<<< EOT
1089: jQuery(function() {
1090:     jQuery.jqplot('{$graphId}',
1091:     [
1092: {$str1}
1093:     ],
1094:     {
1095:         // 系列
1096:         series: [
1097: {$str2}
1098:         ],
1099:         legend: {
1100:             show: true,
1101:             placement: 'inside',
1102:             location: 'sw',
1103:             renderer: $.jqplot.EnhancedLegendRenderer,
1104:             rendererOptions: { numberRows: 1 }
1105:         },
1106:         seriesDefaults: {
1107:             showLine: true,
1108:             rendererOptions: { smooth: false },
1109:             markerOptions: { size: 0 },
1110:         },
1111:         // 軸
1112:         axes: {
1113:             xaxis: {
1114:                 renderer: $.jqplot.DateAxisRenderer,
1115:                 min: '{$xmin}',
1116:                 max: '{$xmax}',
1117:                 tickOptions: { formatString: '%Y/%#m' },
1118:                 label: '年月',
1119:             },
1120:             yaxis: {
1121:                 label: '支持率(%)'
1122:             }
1123:         },
1124:         // ハイライター
1125:         highlighter: {
1126:             show: true,
1127:             showMarker: true,
1128:             tooltipLocation: 'sw',
1129:             fadeTooltip: false,
1130:             bringSeriesToFront: true,
1131:             tooltipAxes: 'xy',
1132:             formatString: '%s<br>支持率%s%'
1133:         }
1134:     }
1135:     );
1136: });
1137: 
1138: EOT;
1139: 
1140:     return $js;
1141: }

政党支持率は、ユーザー関数 plotParty により、XMLオブジェクトに格納したデータから jqPlot を使って折れ線グラフに描く。生成する jqPlotスクリプトは、上述のユーザー関数 plotCabinet とほぼ同じであるが、折れ線の数が増える。

補足:スクレイピング対策について

2025年(令和7年)10月の NHK ONE スタートにともない、本プログラムが参考しているサイトが、かなり厳しめのスクレイピング対策を施したことを確認しています。
PHPの cURL関数や、Pythonの curl_cffi関数を使っても容易に読み出すことができないため、AIを使って定期的に "polls.xml" を更新し、当サイトにアップロードするようにしています。NHKが公表しているデータを正確に取得していますが、この方式に変更していることをご了承ください。

参考サイト

(この項おわり)
header