【シェルと遊ぼう】日本のコロナ感染者情報取得スクリプト その1(元データ取得とDB作成)

はじめに

またまた最近シェルスクリプトを書いてない。絶対に構文諸々忘れてる。

ということで、今さら感はあるけれど、日本のコロナ(COVID-19)感染者についての情報を取得するスクリプトを作ってみよう。

元データは下のサイトさんで公開されているCSVファイルを使うことにした。
練習課題として、オーソドックスなCSVはもってこいではなかろうか。

感染者(発症例)1件の情報が1行になっているっぽい。
更新タイミングは決まっておらず、政府や各自治体の公表に気付いたら随時更新とのこと。

フォーマットやらデータの統一感はちょっと苦しいけれども、ここまでまとめてくれているのはありがたいですね。
#REF!とか出てくるからExcelで作ってるのかな。ツラそう。。

ちなみに、集計なら下のサイトさんで使用されているCSVの方が楽。
というか県別の集計結果がCSVファイルになっている。病床数データもあるっぽい。

ダッシュボードというだけあって、速報値も使うこちらのサイトさんの方が更新は早い。

自治体によって公表フォーマットやら項目やら粒度が違うようで、データをまとめるのはどこも苦労してそう。

無理のない範囲でがんばってくださいまし。

作るもの

基本的にPOSIX準拠でがんばる。
ただ、Webからデータを取ってくるのにはcurlを利用する。
(wgetでもfetchでも好きなのでいいよ)

以下の機能くらいは実装したい。

  1. データベースの作成・更新と全行全項目の出力
  2. 全感染者の指定項目を取得 (列の抽出,並べ替え)
  3. 指定項目の条件でフィルタ (行の抽出)
  4. 指定項目の内訳集計 (出現回数のカウント)

awkが主役の予感。

コードも記事も長くなるので今回は1まで。

以降では、今回作るスクリプト名をcovidとした前提で説明する。

データベースの更新

実行時、スクリプト内で指定したパスにCSVファイルがない場合は、下のURLから取得する。
取得したらスクリプトで使う項目だけ抜き出したCSVファイル(以降DBファイル)を作成する。

https://dl.dropboxusercontent.com/s/6mztoeb6xf78g5w/COVID-19.csv

実行イメージは下記。

$ covid -u      # データベースを更新して全行全列を取得
ID,確定日,受診都道府県,居住地,居住地(詳細),年代,性別,職業,備考,状態
1,2020/01/15,神奈川県,神奈川県,神奈川県,30,男性,,,退院
2,2020/01/24,東京都,中華人民共和国,中華人民共和国,40,男性,,,退院または死亡
 ...
 ...
16054,2020/05/13,東京都,東京都,東京都,不明,不明,,,

環境によるかもだけど、データベース更新には3〜5秒くらいかかるかと。

全行全項目の出力

引数を指定しない場合は、後述の抜き出した列を全て表示する。

$ covid
ID,確定日,受診都道府県,居住地,居住地(詳細),年代,性別,職業,備考,状態
1,2020/01/15,神奈川県,神奈川県,神奈川県,30,男性,,,退院
2,2020/01/24,東京都,中華人民共和国,中華人民共和国,40,男性,,,退院または死亡
3,2020/01/25,東京都,中華人民共和国,中華人民共和国,30,女性,,,退院または死亡
4,2020/01/26,愛知県,中華人民共和国,中華人民共和国,40,男性,,国籍:中国,
 ...
 ...
16054,2020/05/13,東京都,東京都,東京都,不明,不明,,,

引数ありの挙動については後日説明する。

列項目について

このスクリプトでは元データから必要そうな列だけ抜き出す。
列項目の詳細は以下。
出力の際の区切り文字はカンマとする。

項目 パラメータ 備考 出力例
ID id データの通し番号。元データの「通し」 1/2/3...
確定日 date PCR検査の陽性確定日 '2020/05/13'など
受診都道府県 area1 陽性確定時に感染者が受診していた医療機関がある都道府県。空港名もある... 東京都/沖縄県/不明など
居住地 area2 感染者の住居都道府県または居住国 東京都/沖縄県/不明など
居住地(詳細) area3 元データ項目では「キー」。居住都道府県と同じデータも多数 東京都/沖縄県那覇市/不明など
年代 age 感染者の年代。元データは100歳以上も90に含むっぽい 0/10/20/30/40/50/60/70/80/90/100/不明
性別 sex 感染者の性別 男性/女性/不明
職業 job 感染者の職業。記載がある件数が少なく同職種でも記載に統一感なし... 会社員/看護師/無職/不明(#1)など
備考 note 詳細地区や年齢やその他補足情報っぽい 足立区/100歳以上/再陽性
状態 sts 感染者の状態。元データの「ステータス」と「無症状病原体保有者」の組み合わせ 死亡/退院/退院または死亡(国内無症状)/空欄

年代については元データをスクリプトで以下のように加工して出力する。

元データ 出力 備考
0-10 0 10歳未満
90 100 備考に「100歳以上」と記載がある場合のみ
空欄 不明

また、確定日の日付は比較したくなるかもしれないのでゼロ埋めして月も日も2桁にしておく。
何月以降などの指定が欲しかったときにきっと楽になるはず。

ちなみに「状態」については元データの空欄が多くて集計には全く使えそうにない。。。困った。

元データの詳細については以下を参照されたし。

コード

今回はDBファイル作成の主要部を中心に説明。

コード全体はこちらを参照されたし。

設定箇所

以下の変数でWebへのアクセスコマンドとURLを指定。

GET_CMD="curl -s"
DB_URL=https://dl.dropboxusercontent.com/s/6mztoeb6xf78g5w/COVID-19.csv

また、xxx_DB_FILEという変数でDBファイルを指定。

DB_DIR="${XDG_DATA_HOME:-${HOME}/.local/share/}/${CMD_NAME}"
[ ! -d "$DB_DIR" ] && mkdir -p "$DB_DIR"
RAW_DB_FILE="${DB_DIR}/COVID-19.csv"            # Webから取ってきた生データ
ITEM_DB_FILE="${DB_DIR}/ITEM_DB_COVID-19.csv"   # 必要な項目だけ抜き出したデータ

DB作成

下が元データのCSVから抜き出す項目の列位置情報。awkで使う。
それと、出力の際のヘッダ情報。
日本語をシェルスクリプトに入れたくないけど、元データにも混じってるし、いっか。
だったらコメントも日本語にしとけば良かった。。

PICKUP_DB_ITEMS='$1,$40,$10,$11,$14,$6,$7,$32,$19,$18 "(" $3 ")"'
PICKUP_DB_HEADER='ID,確定日,受診都道府県,居住地,居住地(詳細),年代,性別,職業,備考,状態'

実際に元データのCSVを取得してDBファイルを作成するコードは以下。
DBファイルが無い or -uオプション指定時のみ更新する。

# get raw database from web
if [ ! -f "$RAW_DB_FILE" -o -n "$UPDATE_DB" ]; then
  $GET_CMD "$DB_URL" > "$RAW_DB_FILE"
  if [ $? -ne 0 ]; then
    err_exit "can't db update error from $DB_URL"
  fi
  rm "$ITEM_DB_FILE" "$TOTAL_DB_FILE" 1>&2 2>/dev/null
fi

# make database for this script
if [ ! -f "$ITEM_DB_FILE" ]; then
  echo "$PICKUP_DB_HEADER" > "$ITEM_DB_FILE"
  cat "$RAW_DB_FILE"    \
    | make_database >> "$ITEM_DB_FILE"
fi

普段使うDBファイルの本体を作成している関数は以下。
元データのCSVから必要な行と列を抜き出して、前述のように年齢や日付を整形している。
awkなら正規表現で頑張らなくても良いので楽だー。

make_database(){
  get_body_recode   \
    | awk -F","     \
    '   BEGIN{ OFS="," }
        {
          if($19~"100歳以上"){$6=100}
          if($6==""){$6="不明"}
          if($6=="0-10"){$6="0"}
          print '"$PICKUP_DB_ITEMS"'
        }' \
    | sed 's/()$//' \
    | delete_space  \
    | format_date
}

上で呼び出している関数は以下。
改行コードはCRLFからLFに変換。
カンマ前後の空白は、通常の0x20の他、0xA0(HTMLでいう )も混じっていたので両方削除している。

get_body_recode(){
  tr -d '\r'        \
    | sed '1d'      \
    | grep -v '^,'
}
delete_space(){
  sed 's/[  ]\{1,\},/,/g'     \
    | sed 's/,[  ]\{1,\}/,/g' \
    | sed 's/[  ]\{1,\}$//'
}
format_date(){
  sed 's@,\([0-9]\{4\}\)/\([1-9]\)/\([0-9]\{1,2\}\),@,\1/0\2/\3,@'  \
    | sed 's@,\([0-9]\{4\}\)/\([0-9][0-9]\)/\([0-9]\),@,\1/\2/0\3,@'
}

データの出力

データベースを作成したらあとは出力するだけ。

出力する項目列は以下の変数にセット。awkで使う。
デフォルト値は全列。

OUTPUT_ITEMS='$0'

各項目を出力するコードは以下。

else
  # output pickup recodes
  cat "$DB_FILE"    \
    | awk -F"," '
      BEGIN{OFS=","}
      '"$PICKUP_COND"'{
        print '"${OUTPUT_ITEMS}"'
      }'
fi

awkの詳細や$DB_FILE$PICKUP_CONDについてはまた後日。

さいごに

今回はここまで。

やっぱり構文とかコマンドオプションとか色々忘れてた。
定期的に書かねば。

それにしてもデータの作り方というか構造ってやっぱり大切。
実装の容易さや実行速度への影響がダイレクトに出ちゃう。

国が機械で処理することを想定した統一フォーマット作って、
全自治体がそれを利用したらみんな幸せだったかもしれないのだけれども。
ドタバタの中では難しかったのかな。

さて。
次回は、欲しい列だけ欲しい順で指定して取得する方法。

確認環境

PC Thinkpad X1 Carbon 2nd Gen
OS FreeBSD 12.1-RELEASE-p4

参考