東京都がGitHubに公開した新型コロナウイルス感染症対策サイトのソースがすごいと話題に
2020年3月20日に東京都副知事である宮坂学さんが「東京都新型コロナウイルス対策サイト」の開設を発表しました。
ちなみに、宮坂学さんは、日本人なら誰もが知っているポータルサイト「Yahoo! Japan」の元社長・会長をされていた方です。
東京都新型コロナウイルス感染症対策サイトのデザイン
開設したサイトは次のURLです。
新型コロナウイルス感染症対策サイト
https://stopcovid19.metro.tokyo.lg.jp/
次のようなデザインのサイトです。
配色のコントラストが強く、誰もがみやすい視認性の高いデザインになっています。
東京都新型コロナウイルス感染症対策サイトのプログラムソース
このサイトのプログラムソースはオープンソースとしてGitHubに公開されており、そのソースがモダンでレベルの高いWebプログラミングで作られており話題になっています。
オープンソースとは、誰もが見えるように公開されたプログラムソースのことです。
さすが元IT系企業の社長さんです。
これから日本はIT人材を増やさなければならないので、ITの活用事例としてとても良い発信をされていると思います。
ということで話題になっていましたので、ITエンジニアとして僕も、早速チェックしてみました。
GitHubのURLは次の通りです。
https://github.com/tokyo-metropolitan-gov/covid19
GitHubのサイトは次のようになっています。
ここに東京都新型コロナウイルス感染症対策サイトで使われているプログラムソースが丸ごと入っています。
オープンソースですので、誰もがプログラムソースを見ることができます。
全体的には、Vue.js + Nuxt.jsというフロントエンド の最新技術が使われています。
Vue.jsとはJavaScriptによりSPA(シングル・ページ・アプリケーション)を開発するためのフレームワークです。
また、Nuxt.jsは、Vue.jsの拡張機能です。
ところで、僕がGitHubのソースを読んで気になったのは、データとして使用するJSONデータがそのままGitHubにアップされていたのですが、このJSONを生成する処理はどこにあるのだろう?管理画面のソースがないのではないか?という点でした。
もしかして、JSONを手作業で作成してサーバーにアップいるの?まさか?(笑)
よくソースを読んでみると、JSONの生成処理は「 covid19/tool/convert.php 」というPHPプログラムで行っていました。
「 covid19/tool/convert.php 」のソース内にあるreadContacts関数、readQuerents関数、readPatientsV2関数、readPatients関数などで、PHPによりExcelファイル(拡張子.xlsx)のファイルに記載されているデータを読み込んでいます。
そして、convert.phpの最後の処理で、最終的にJSONデータを生成するために連想配列を作成し、json_encode関数で連想配列からJSON文字列に変換し、file_put_contents関数でJSON文字列を「data.json」ファイルに出力して、最終的にWebサイトJavaScriptが読み込んで表示するためのJSONファイルを生成しています。
以下が、「 covid19/tool/convert.php 」の2020/3/7現在の全プログラムソースです。
処理が分かるように「//※」印をつけて筆者がコメントを追記しています。
<?php require __DIR__.'/vendor/autoload.php'; use Carbon\Carbon; use Tightenco\Collect\Support\Collection; //※日付のリストデータを生成する関数 function makeDateArray($begin) : Collection{ $begin = Carbon::parse($begin); $dates = []; while(true) { if ($begin->diffInDays(Carbon::now()) == 0) { break; } else { $dates[$begin->addDay()->format('Y-m-d').'T08:00:00.000Z'] =0; } } return new Collection($dates); } //※日付をYYYY/MM/DD HH:IIの形式(例:2020/03/08 00:00)に変換する関数 function formatDate(string $date) :string { if (preg_match('#(\d+/\d+/\d+)/ (\d+:\d+)#', $date, $matches)) { $carbon = Carbon::parse($matches[1].' '.$matches[2]); return $carbon->format('Y/m/d H:i'); } else { throw new Exception('Can not parse date:'.$date); } } //※Excelファイル(拡張子xlsx)のデータを連想配列の形式に変換する関数 function xlsxToArray(string $path, string $sheet_name, string $range, $header_range = null) { $reader = new PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $spreadsheet = $reader->load($path); $sheet = $spreadsheet->getSheetByName($sheet_name); $data = new Collection($sheet->rangeToArray($range)); $data = $data->map(function ($row) { return new Collection($row); }); if ($header_range !== null) { $headers = xlsxToArray($path, $sheet_name, $header_range)[0]; // TODO check same columns length return $data->map(function ($row) use($headers){ return $row->mapWithKeys(function ($cell, $idx) use($headers){ return [ $headers[$idx] => $cell ]; }); }); } return $data; } //※Excelファイル(拡張子xlsx)から相談件数を読み込み連想配列を生成する関数 function readContacts() : array { $data = xlsxToArray(__DIR__.'/downloads/コールセンター相談件数-RAW.xlsx', 'Sheet1', 'A2:E100', 'A1:E1'); return [ 'date' => xlsxToArray(__DIR__.'/downloads/コールセンター相談件数-RAW.xlsx', 'Sheet1', 'H1')[0][0], 'data' => $data->filter(function ($row) { return $row['曜日'] && $row['17-21時']; })->map(function ($row) { $date = '2020-'.str_replace(['月', '日'], ['-', ''], $row['日付']); $carbon = Carbon::parse($date); $row['日付'] = $carbon->format('Y-m-d').'T08:00:00.000Z'; $row['date'] = $carbon->format('Y-m-d'); $row['w'] = $carbon->format('w'); $row['short_date'] = $carbon->format('m/d'); $row['小計'] = array_sum([ $row['9-13時'] ?? 0, $row['13-17時'] ?? 0, $row['17-21時'] ?? 0, ]); return $row; }) ]; } //※Excelファイル(拡張子xlsx)から受診相談窓口相談件数を読み連想配列を生成する関数 /* * 取り急ぎreadContactsからコピペ * 過渡期がすぎたら共通処理にしたい。→マクロ入ってる */ function readQuerents() : array { $data = xlsxToArray(__DIR__.'/downloads/帰国者・接触者センター相談件数-RAW.xlsx', 'RAW', 'A2:D100', 'A1:D1'); return [ 'date' => xlsxToArray(__DIR__.'/downloads/帰国者・接触者センター相談件数-RAW.xlsx', 'RAW', 'H1')[0][0], 'data' => $data->filter(function ($row) { return $row['曜日'] && $row['17-翌9時']; })->map(function ($row) { $date = '2020-'.str_replace(['月', '日'], ['-', ''], $row['日付']); $carbon = Carbon::parse($date); $row['日付'] = $carbon->format('Y-m-d').'T08:00:00.000Z'; $row['date'] = $carbon->format('Y-m-d'); $row['w'] = $carbon->format('w'); $row['short_date'] = $carbon->format('m/d'); $row['小計'] = array_sum([ $row['9-17時'] ?? 0, $row['17-翌9時'] ?? 0, ]); return $row; })->values() ]; } //※Excelファイル(拡張子xlsx)から患者数を読み込み連想配列を生成する関数その2 function readPatientsV2() : array { $data = xlsxToArray(__DIR__.'/downloads/東京都患者発生発表数-RAW.xlsx', 'RAW', 'A2:J100', 'A1:J1'); $base_data = $data->filter(function ($row) { return $row['リリース日']; })->map(function ($row) { $date = '2020-'.str_replace(['月', '日'], ['-', ''], $row['リリース日']); $carbon = Carbon::parse($date); $row['リリース日'] = $carbon->format('Y-m-d').'T08:00:00.000Z'; $row['date'] = $carbon->format('Y-m-d'); $row['w'] = $carbon->format('w'); $row['short_date'] = $carbon->format('m/d'); return $row; }); return [ 'date' => xlsxToArray(__DIR__.'/downloads/東京都患者発生発表数-RAW.xlsx', 'RAW', 'M1')[0][0], 'data' => [ '感染者数' => makeDateArray('2020-01-24')->merge($base_data->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })), '退院者数' => makeDateArray('2020-01-24')->merge($base_data->filter(function ($row) { return $row['退院'] === '〇' && !preg_match('/死亡$/', trim($row['備考'])); })->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })), '死亡者数' => makeDateArray('2020-01-24')->merge($base_data->filter(function ($row) { return preg_match('/死亡$/', trim($row['備考'])); })->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })), '軽症' => makeDateArray('2020-01-24')->merge($base_data->filter(function ($row) { return $row['退院'] !== '〇' && trim($row['備考']) == ''; })->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })), '中等症' => makeDateArray('2020-01-24')->merge($base_data->filter(function ($row) { return $row['退院'] !== '〇' && preg_match('/中等症$/', trim($row['備考'])); })->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })), '重症' => makeDateArray('2020-01-24')->merge($base_data->filter(function ($row) { return $row['退院'] !== '〇' && preg_match('/重症$/', trim($row['備考'])); })->groupBy('リリース日')->map(function ($rows) { return $rows->count(); })) ] ]; } //※Excelファイル(拡張子xlsx)から患者数を読み込み連想配列を生成する関数 function readPatients() : array { $data = xlsxToArray(__DIR__.'/downloads/東京都患者発生発表数-RAW.xlsx', 'RAW', 'A2:J100', 'A1:J1'); return [ 'date' => xlsxToArray(__DIR__.'/downloads/東京都患者発生発表数-RAW.xlsx', 'RAW', 'M1')[0][0], 'data' => $data->filter(function ($row) { return $row['リリース日']; })->map(function ($row) { $date = '2020-'.str_replace(['月', '日'], ['-', ''], $row['リリース日']); $carbon = Carbon::parse($date); $row['リリース日'] = $carbon->format('Y-m-d').'T08:00:00.000Z'; $row['date'] = $carbon->format('Y-m-d'); $row['w'] = $carbon->format('w'); $row['short_date'] = $carbon->format('m/d'); return $row; }) ]; } //※連想配列から日別の小計を計算する関数 function createSummary(array $patients) { $dates = makeDateArray('2020-01-23'); return [ 'date' => $patients['date'], 'data' => $dates->map(function ($val, $key) { return [ '日付' => $key, '小計' => $val ]; })->merge($patients['data']->groupBy('リリース日')->map(function ($group, $key) { return [ '日付' => $key, '小計' => $group->count() ]; }))->values() ]; } //※連想配列から日別の退院数を集計する関数 function discharges(array $patients) : array { return [ 'date' => $patients['date'], 'data' => $patients['data']->filter(function ($row) { return $row['退院'] === '〇'; })->values() ]; } //※Excelファイル(拡張子xlsx)から検査数を読み込み連想配列を生成する関数 function readInspections() : array{ $data = xlsxToArray(__DIR__.'/downloads/検査実施日別状況.xlsx', '入力シート', 'A2:J100', 'A1:J1'); $data = $data->filter(function ($row) { return $row['疑い例検査'] !== null; }); return [ 'date' => '2020/3/5/ 00:00', //TODO 現在のエクセルに更新日付がないので変更する必要あり 'data' => $data ]; } //※検査数を集計する関数 function readInspectionsSummary(array $inspections) : array { return [ 'date' => $inspections['date'], 'data' => [ '都内' => $inspections['data']->map(function ($row) { return str_replace(' ', '', $row['(小計①)']); }), 'その他' => $inspections['data']->map(function ($row) { return str_replace(' ', '', $row['(小計②)']); }), ], 'labels' =>$inspections['data']->map(function ($row) { return Carbon::parse($row['判明日'])->format('n/j'); }) ]; } $contacts = readContacts(); //※Excelから相談件数を読み込み $querents = readQuerents(); //※Excelから受診相談窓口相談件数を読み込み $patients = readPatients(); //※Excelから患者数を読み込み $patients_summary = createSummary($patients); //患者数の集計 $better_patients_summary = readPatientsV2(); //※Excelから患者数を読み込みその2 $discharges = discharges($patients); //※Excelから退院数を読み込み $discharges_summary = createSummary($discharges); //※退院数の集計 $inspections =readInspections(); //※検査実施数を読み込み $inspections_summary =readInspectionsSummary($inspections); //※検査実施数を集計 //※以上の集計内容をもとに、以下で連想配列を生成する $data = compact([ 'contacts', 'querents', 'patients', 'patients_summary', 'discharges_summary', 'discharges', 'inspections', 'inspections_summary', 'better_patients_summary' ]); $lastUpdate = ''; $lastTime = 0; foreach ($data as $key => &$arr) { $arr['date'] = formatDate($arr['date']); $timestamp = Carbon::parse()->format('YmdHis'); if ($lastTime <= $timestamp) { $lastTime = $timestamp; $lastUpdate = Carbon::parse($arr['date'])->addDay()->format('Y/m/d 8:00'); } } $data['lastUpdate'] = $lastUpdate; $data['main_summary'] = [ 'attr' => '検査実施人数', 'value' => xlsxToArray(__DIR__.'/downloads/検査実施サマリ.xlsx', '検査実施サマリ', 'A2')[0][0], 'children' => [ [ 'attr' => '陽性患者数', 'value' => $better_patients_summary['data']['感染者数']->sum(), 'children' => [ [ 'attr' => '入院中', 'value' => $better_patients_summary['data']['感染者数']->sum() - $better_patients_summary['data']['退院者数']->sum() - $better_patients_summary['data']['死亡者数']->sum(), 'children' => [ [ 'attr' => '軽症・中等症', 'value' => $better_patients_summary['data']['軽症']->sum() + $better_patients_summary['data']['中等症']->sum() ], [ 'attr' => '重症', 'value' => $better_patients_summary['data']['重症']->sum() ] ] ], [ 'attr' => '退院', 'value' => $better_patients_summary['data']['退院者数']->sum() ], [ 'attr' => '死亡', 'value' => $better_patients_summary['data']['死亡者数']->sum() ] ] ] ] ]; //※連想配列をJSON文字列に変換してdata.jsonファイルに出力 file_put_contents(__DIR__.'/../data/data.json', json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK));
このプログラムの使い方ですが、パソコン上でExcelにデータを入力し、WindowsのDOSプロンプトでこのPHPスクリプトを実行することでExcelデータからJSONを生成して、そのJSONをサーバーにアップするという流れで使います。
PHPはWebに特化したプログラミング言語と言われていますが、このようにパソコン上でMicrosoft の Excelファイルを読み込んで処理をする用途でも使うことができます。
※これがもし「CSVファイル(テキストファイル)」だったら当たり前なのですが、Excelファイル(.xlsxファイル)も読み込んで処理できるというのがポイントです。
WindowsやmacOSのGUIアプリケーションを作るのでなければ、PHPを使ってパソコン上で実行するプログラムを作ることもできますので、知識として知ってもらえたらと思います。
おわりに
PHPプログラムがこのような多くの日本人に貢献できる場面で活用されているのは、PHPを使っている僕にとっては嬉しい限りでした^^
僕も、現在参画しているプロジェクトで、これからJavaScript(Vue.js) + PHP(Laravel)を使用したWebサービスを開発するところでしたので、とても参考になりました♪