サンプル・プログラムの実行例
サンプル・プログラム
viewPolls.php | サンプル・プログラム本体。 |
polls.xml | 内閣支持率・政党支持率のデータ・ファイル。 |
viewPolls.php | サンプル・プログラム本体。 |
バージョン | 更新日 | 内容 |
---|---|---|
3.3.0 | 2024/03/24 | 2023年8月以降のデータに対応 |
3.2.0 | 2023/05/17 | plotParty()に3政党を追加 |
3.1 | 2021/05/08 | POLLS_FILE へのセーブ/ロード |
3.0 | 2021/05/08 | 2020年4月以降に対応,PHP8対応 |
2.0 | 2018/05/15 | 描画期間を選択できるようにした |
解説:準備
21: //jqPlotのあるフォルダ
22: define('JQPLOT', '../../../../common/jqplot/');
23:
24: //描画期間(年)
25: define('PERIOD', 7);
26:
27: //最古データ(年)
28: define('OLDYEAR', 1998);
29:
30: //表示幅(ピクセル)
31: define('WIDTH', 600);
32:
33: //表示高さ(ピクセル)
34: define('HEIGHT', 500);
35:
36: //保存用データファイル名
37: define('POLLS_FILE', './polls.xml');
公式サイト「jqPlot Charts and Graphs for jQuery」から圧縮ファイルをダウンロードし、適当な場所に解凍しておく。その場所を定数 JQPLOT に定義する。
また、描画期間(当年から過去何年分遡るか)を定数 PERIOD に定義しておく。
なお、スクレイピングしたデータはXML形式ファイルとして定数 POLLS_FILE で示すファイルにセーブし、次に描画するときは、このファイルからロードできるようにした。
解説:スクレイピング
354: /**
355: * 内閣支持率を解析する
356: * @param int $infp 解析中のURL
357: * @param array $items 解析結果を格納する配列
358: * @return bool TRUE/FALSE
359: */
360: function analyseCabinet($infp, &$items) {
361: $pat1 = "/<h[0-9]+[^>]*>内閣支持率<\/h[0-9]+>/u";
362: $pat2 = "/<tr[^>]*>/u";
363: $pat3 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
364: $pat31 = "/<td[^>]*>(.+)/u";
365: $pat4 = "/<t[hd][^>]*>支持(<[^>]+>)?する/u";
366: $pat5 = "/<t[hd][^>]*>(<[^>]+>)?支持(<[^>]+>)?しない/u";
367: $pat6 = "/<t[hd][^>]*>内閣/u";
368: $pat61 = "/<\/table>/u";
369: $pat7 = "/<td colspan\=\"([0-9]+)\"[^>]*>([^<]+)/u";
370: $pat8 = "/<td[^>]*>([^<]+)/u";
371: $pat9 = "/<h[0-9]+[^>]*>.*政党支持率<\/h[0-9]+>/u";
372: $flag = 0;
373: while (! feof($infp)) {
374: $instr = trim(fgets($infp));
375: if (($flag == 0) && (preg_match($pat1, $instr) > 0)) {
376: $flag = 1;
377: } else if (($flag == 1) && (preg_match($pat2, $instr) > 0)) {
378: $flag = 2;
379: $i = 0;
380: //月
381: } else if ($flag == 2) {
382: if (preg_match($pat4, $instr) > 0) {
383: $flag = 3;
384: $i = 0;
385: } else if (preg_match($pat3, $instr, $arr) > 0) {
386: $items[$i]['month'] = $arr[2];
387: $items[$i]['yes'] = '-';
388: $items[$i]['no'] = '-';
389: $items[$i]['cabinet'] = '-';
390: $i++;
391: }
392: //支持する
393: } else if ($flag == 3) {
394: if (preg_match($pat5, $instr) > 0) {
395: $flag = 4;
396: $i = 0;
397: } else if (preg_match($pat31, $instr, $arr) > 0) {
398: $items[$i]['yes'] = strip_tags($arr[1]);
399: $i++;
400: }
401: //支持しない
402: } else if ($flag == 4) {
403: if (preg_match($pat31, $instr, $arr) > 0) {
404: $items[$i]['no'] = strip_tags($arr[1]);
405: $i++;
406: } else if (preg_match($pat6, $instr) > 0) {
407: $flag = 5;
408: $i = 0;
409: } else if (preg_match($pat61, $instr) > 0) {
410: $flag = 5;
411: $i = 0;
412: }
413: //内閣
414: } else if ($flag == 5) {
415: if (preg_match($pat7, $instr, $arr) > 0) {
416: $n = $i + $arr[1];
417: while ($i < $n) {
418: $items[$i]['cabinet'] = $arr[2];
419: $i++;
420: }
421: } else if (preg_match($pat8, $instr, $arr) > 0) {
422: $items[$i]['cabinet'] = $arr[1];
423: $i++;
424: } else if (preg_match($pat9, $instr) > 0) {
425: $flag = 6;
426: }
427: } else if ($flag == 6) {
428: break;
429: }
430: }
431:
432: return TRUE;
433: }
538: /**
539: * 政党支持率を解析する
540: * @param int $infp 解析中のURL
541: * @param array $items 解析結果を格納する配列
542: * @return bool TRUE/FALSE
543: */
544: function analyseParty($infp, &$items) {
545: $pat1 = "/<tr[^>]*>/u";
546: $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
547: $pat3 = "/<\/tr>/u";
548: $pat4 = "/<t[hd][^>]*>([^<]+)/u";
549: $pat5 = "/<\/table>/u";
550: $flag = 0;
551: while (! feof($infp)) {
552: $instr = trim(fgets($infp));
553: if (($flag == 0) && (preg_match($pat1, $instr) > 0)) {
554: $flag = 1;
555: $i = 0;
556: //月
557: } else if ($flag == 1) {
558: if (preg_match($pat2, $instr, $arr) > 0) {
559: $items[$i]['month'] = $arr[2];
560: $i++;
561: } else if (preg_match($pat3, $instr) > 0) {
562: $flag = 2;
563: $i = 0;
564: }
565: //政党支持率
566: } else if ($flag == 2) {
567: if (preg_match($pat2, $instr, $arr) > 0) {
568: $items[$i][$party] = $arr[2];
569: $i++;
570: } else if (preg_match($pat4, $instr, $arr) > 0) {
571: $party = $arr[1];
572: $i = 0;
573: } else if (preg_match($pat5, $instr) > 0) {
574: $flag = 3;
575: }
576: } else if ($flag == 3) {
577: break;
578: }
579: }
580: return TRUE;
581: }
スクレイピングしたデータは、配列に格納する。
435: /**
436: * 内閣支持率・政党支持率を解析する【2020年1月以降】
437: * @param int $infp 解析中のURL
438: * @param array $items 内閣支持率の解析結果を格納する配列
439: * @param array $items 政党支持率の解析結果を格納する配列
440: * @return bool TRUE/FALSE
441: */
442: function analyse2020($infp, &$items1, &$items2) {
443: //プロットする政党名
444: static $table = array(
445: '', '自民党', '立憲民主党', '日本維新の会', '公明党', '共産党', '国民民主党', 'れいわ新選組', '社民党', 'みんなでつくる党', '参政党', '特に支持している政党はない', '支持政党なし');
446:
447: $pat01 = "/<i\s+class\=\"far\s+fa\-calendar\-alt\"><\/i>[0-9]+年([0-9]+)月<span>/ui";
448: $pat11 = "/<h3>内閣支持([0-9]+)[%\%]、不支持([0-9]+)[%\%]/ui";
449: $pat12 = "/(\p{Han}+内閣)を「支持する/ui";
450: $pat21 = "/<h2><span>政党支持率<\/span>/ui";
451: $pat31 = "/<td>([^<]+)<\/td>/ui";
452: $pat32 = "/<td\s+class\=\"right\">([0-9\.]+)<\/td>/ui";
453: //2023年8月以降
454: $pat22 = "/各党の支持率は/ui";
455: $pat33 = '/「([^」]+)」が([0-9\.]+)\%/ui';
456:
457: $flag = FALSE;
458: while (! feof($infp)) {
459: $instr1 = trim(fgets($infp));
460: $instr2 = strip_tags($instr1);
461: //月
462: if (preg_match($pat01, $instr1, $arr) > 0) {
463: $month = (int)$arr[1];
464: //内閣支持率・不支持率
465: } else if (preg_match($pat11, $instr1, $arr) > 0) {
466: $items1[$month - 1]['month'] = (int)$month;
467: $items1[$month - 1]['yes'] = (int)$arr[1];
468: $items1[$month - 1]['no'] = (int)$arr[2];
469: //内閣名
470: } else if (preg_match($pat12, $instr2, $arr) > 0) {
471: $items1[$month - 1]['cabinet'] = (string)$arr[1];
472: //政党支持率の解析開始
473: } else if (preg_match($pat21, $instr1, $arr) > 0) {
474: $flag = TRUE;
475: $items2[$month - 1]['month'] = (int)$month;
476: //政党名
477: } else if ($flag && (preg_match($pat31, $instr1, $arr) > 0)) {
478: $party = (string)$arr[1];
479: if ($party == '特に支持している政党はない') {
480: $party = '支持政党なし';
481: }
482: //支持率
483: } else if ($flag && (preg_match($pat32, $instr1, $arr) > 0)) {
484: $items2[$month - 1][$party] = (string)$arr[1];
485: //政党名と支持率 2023年8月以降
486: } else if (preg_match($pat22, $instr1, $arr) > 0) {
487: if (preg_match_all($pat33, $instr1, $arr, PREG_PATTERN_ORDER) > 0) {
488: foreach ($arr[1] as $key=>$party) {
489: if ($party == '特に支持している政党はない') {
490: $party = '支持政党なし';
491: }
492: if (array_search($party, $table) != FALSE) {
493: $items2[$month - 1][$party] = (string)$arr[2][$key];
494: }
495: }
496: }
497: }
498: }
499: return TRUE;
500: }
解説:XMLへの格納
502: /**
503: * 内閣支持率をXMLオブジェクトに追加/更新する
504: * @param array $items 解析結果を格納した配列
505: * @param int $year 西暦年
506: * @param object $xml XMLオブジェクト
507: * @return bool TRUE/FALSE
508: */
509: function addCabinet($items, $year, &$xml) {
510: //西暦年の頭出し
511: $flag = FALSE;
512: foreach ($xml->research as $elem1) {
513: if ($elem1->year == $year) {
514: break;
515: }
516: }
517: //月次処理
518: foreach ($items as $item) {
519: foreach ($elem1->article as $elem2) {
520: if ($elem2->month == $item['month']) {
521: $flag = TRUE;
522: break;
523: }
524: }
525: if (! $flag) {
526: $elem2 = $elem1->addChild('article');
527: $elem2->addChild('month', $item['month']);
528: }
529: if (! isset($elem2->cabinet)) {
530: $elem2->addChild('cabinet');
531: $elem2->cabinet->addChild('name', $item['cabinet']);
532: $elem2->cabinet->addChild('yes', $item['yes']);
533: $elem2->cabinet->addChild('no', $item['no']);
534: }
535: }
536: }
583: /**
584: * 政党支持率をXMLオブジェクトに追加/更新する
585: * @param array $items 解析結果を格納した配列
586: * @param int $year 西暦年
587: * @param object $xml XMLオブジェクト
588: * @return bool TRUE/FALSE
589: */
590: function addParty($items, $year, &$xml) {
591: //西暦年の頭出し
592: $flag = FALSE;
593: foreach ($xml->research as $elem1) {
594: if ($elem1->year == $year) {
595: break;
596: }
597: }
598:
599: //月次処理
600: foreach ($items as $item) {
601: foreach ($elem1->article as $elem2) {
602: if (isset($item['month']) && ($elem2->month == $item['month'])) {
603: $flag = TRUE;
604: break;
605: }
606: }
607: if (! $flag && isset($item['month'])) {
608: $elem2 = $elem1->addChild('article');
609: $elem2->addChild('month', $item['month']);
610: }
611:
612: foreach ($item as $key=>$val) {
613: if ($key == 'month') continue;
614: $flag = FALSE;
615: foreach ($elem2->party as $elem3) {
616: if (isset($elem3->name) && ($elem3->name == $key)) {
617: $flag = TRUE;
618: break;
619: }
620: }
621: if (! $flag) {
622: $elem4 = $elem2->addChild('party');
623: $elem4->addChild('name', $key);
624: $elem4->addChild('yes', $val);
625: }
626: }
627: }
628: }
解説:XMLファイルへのセーブとロード
317: /**
318: * データ・ファイルを書き込む
319: * @param string $fname 出力ファイル名
320: * @param object XMLオブジェクト
321: * @return bool TRUE/FALSE
322: */
323: function writeDataFile($fname, $xml) {
324: $str = $xml->asXML();
325: $str = cleanUpXML($str);
326: $outfp = fopen($fname, 'w');
327: fwrite($outfp, $str);
328:
329: return fclose($outfp);
330: }
290: /**
291: * データ・ファイルを読み込む
292: * @param string $fname 入力ファイル名
293: * @return object XMLオブジェクト/FALSE:読み込み失敗
294: */
295: function readDataFile($fname) {
296: $xmlstr =<<< EOT
297: <?xml version="1.0" encoding="utf-8" ?>
298: <polls>
299: </polls>
300:
301: EOT;
302: if (! isphp5over()) return FALSE; //PHP5以上でないと動作しない
303:
304: //ファイル無し
305: if (! file_exists($fname)) {
306: $xml = new SimpleXMLElement($xmlstr);
307:
308: //ファイル有り
309: } else {
310: $xml = simplexml_load_file($fname);
311: if ($xml == FALSE) return FALSE;
312: }
313:
314: return $xml;
315: }
解説:jqPlotスクリプト
746: /**
747: * jqPlot用のスクリプト:内閣支持率
748: * @param object $xml XMLオブジェクト
749: * @param int $start 開始年
750: * @param int $finish 終了年
751: * @return string スクリプト
752: */
753: function plotCabinet($xml, $start, $finish) {
754: //系列の生成
755: $series1 = ''; //内閣支持率
756: $series2 = ''; //内閣府支持率
757: $old = '';
758: foreach ($xml as $research) {
759: //描画範囲内かどうか
760: $year = $research->year;
761: if (($year < $start) || ($year > $finish)) continue;
762: //描画データ作成
763: foreach ($research as $article) {
764: $month = $article->month;
765: if ($month >= 1 && $month <= 12) {
766: $name = '';
767: if ($old != (string)$article->cabinet->name) {
768: $name = (string)$article->cabinet->name;
769: $old = (string)$article->cabinet->name;
770: }
771: $yes = $article->cabinet->yes;
772: $no = $article->cabinet->no;
773: if ($month > 0 && $yes > 0) {
774: $series1 .= sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year, $month, $yes, $name);
775: $series2 .= sprintf("['%04d-%02d-01',%d,''],", $year, $month, $no);
776: }
777: }
778: }
779: }
780:
781: //X軸の最小値
782: $xmin = sprintf('%04d-01-01', $start);
783: //X軸の最大値
784: if (date('Y') == $finish) {
785: $xmax = date('Y-m-01', strtotime('next month'));
786: } else {
787: $xmax = sprintf('%04d-12-31', $finish);
788: }
789:
790: //グラフ描画
791: $js =<<< EOT
792: jQuery(function() {
793: jQuery.jqplot('jqPlot_polls',
794: [
795: [ {$series1} ],
796: [ {$series2} ]
797: ],
798: {
799: //系列
800: series: [
801: { label: '支持率', color: '#88CC44' },
802: { label: '不支持率', color: '#CC4488' }
803: ],
804: legend: {
805: show: true,
806: placement: 'inside',
807: location: 'sw',
808: renderer: $.jqplot.EnhancedLegendRenderer,
809: rendererOptions: { numberRows: 1 }
810: },
811: seriesDefaults: {
812: showLine: true,
813: rendererOptions: { smooth: false },
814: markerOptions: { size: 0 },
815: pointLabels: {
816: show :true,
817: escapeHTML: false,
818: location: 'ne'
819: }
820: },
821: //軸
822: axes: {
823: xaxis: {
824: renderer:$.jqplot.DateAxisRenderer,
825: min: '{$xmin}',
826: max: '{$xmax}',
827: tickOptions: { formatString: '%Y/%#m' },
828: label: '年月',
829: },
830: yaxis: {
831: label: '支持率(%)'
832: }
833: },
834: //ハイライター
835: highlighter: {
836: show: true,
837: showMarker: true,
838: tooltipLocation: 'sw',
839: fadeTooltip: false,
840: bringSeriesToFront: true,
841: tooltipAxes: 'xy',
842: formatString: '%s<br />(不)支持率%s%'
843: }
844: }
845: );
846: });
847:
848: EOT;
849:
850: return $js;
851: }
853: /**
854: * jqPlot用のスクリプト:政党支持率
855: * @param object $xml XMLオブジェクト
856: * @param int $start 開始年
857: * @param int $finish 終了年
858: * @return string スクリプト
859: */
860: function plotParty($xml, $start, $finish) {
861: //プロットする政党名
862: static $table = array(
863: '自民党', '公明党', '共産党', '民進党', '立憲民主党', '民主党', '日本維新の会', '国民民主党', 'れいわ新選組', '支持政党なし');
864:
865: //系列の生成
866: $str2 = '';
867: foreach ($table as $key=>$val) {
868: $str2 .= "\t\t\t{ label: '{$val}' },\n";
869: $series[] = '';
870: }
871:
872: foreach ($xml as $research) {
873: $year = (int)$research->year;
874: //描画範囲内かどうか
875: if (($year < $start) || ($year > $finish)) continue;
876: //描画データ作成
877: foreach ($research as $article) {
878: $month = (int)$article->month;
879: foreach ($article as $party) {
880: foreach ($table as $key=>$val) {
881: if ($val == (string)$party->name) {
882: if (preg_match('/[0-9\.]+/', $party->yes) > 0 && $year > 0 && $month >= 1 && $month <= 12) {
883: $series[$key] .= sprintf("['%04d-%02d-01',%4.1f],", $year, $month, (double)$party->yes);
884: }
885: }
886: }
887: }
888: }
889: }
890: $str1 = '';
891: foreach ($series as $val) {
892: $str1 .= "\t\t[ " . $val . "],\n";
893: }
894:
895: //X軸の最小値
896: $xmin = sprintf('%04d-01-01', $start);
897: if (date('Y') == $finish) {
898: $xmax = date('Y-m-01', strtotime('next month'));
899: } else {
900: $xmax = sprintf('%04d-12-31', $finish);
901: }
902:
903: //グラフ描画
904: $js =<<< EOT
905: jQuery(function() {
906: jQuery.jqplot('jqPlot_polls',
907: [
908: {$str1}
909: ],
910: {
911: //系列
912: series: [
913: {$str2}
914: ],
915: legend: {
916: show: true,
917: placement: 'inside',
918: location: 'sw',
919: renderer: $.jqplot.EnhancedLegendRenderer,
920: rendererOptions: { numberRows: 1 }
921: },
922: seriesDefaults: {
923: showLine: true,
924: rendererOptions: { smooth: false },
925: markerOptions: { size: 0 },
926: },
927: //軸
928: axes: {
929: xaxis: {
930: renderer: $.jqplot.DateAxisRenderer,
931: min: '{$xmin}',
932: max: '{$xmax}',
933: tickOptions: { formatString: '%Y/%#m' },
934: label: '年月',
935: },
936: yaxis: {
937: label: '支持率(%)'
938: }
939: },
940: //ハイライター
941: highlighter: {
942: show: true,
943: showMarker: true,
944: tooltipLocation: 'sw',
945: fadeTooltip: false,
946: bringSeriesToFront: true,
947: tooltipAxes: 'xy',
948: formatString: '%s<br />支持率%s%'
949: }
950: }
951: );
952: });
953:
954: EOT;
955:
956: return $js;
957: }
ここでは、jqPlotプラグインの「jqplot.dateAxisRenderer.js」(時間軸を扱う)、「jqplot.highlighter.js」(ツールチップなどハイライト表示を行う)、「jqplot.pointLabels.js」(データポイントラベルを表示する)の3つを使っている。
jqPlotの使い方は、「jqPlot - jQuery プラグイン」(アルファシス)が詳しい。
(2024年3月24日)2023年(令和5年)8月以降のデータに対応
(2023年5月17日)政党支持率に「日本維新の会」「国民民主党」「れいわ新選組」を追加