やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

NewsApiのJSONからSQLite3DBファイルへ挿入する

 とりあえず重複とか考えずに。

成果物

前提

 NewsAPIにてJSONを取得し、ファイル保存したものとする。SQLite3のテーブルを上記のように作成したものとする。

コード

# $1: JSONテキスト, $2: JSONパス
json_extract() { sqlite3 :memory: 'select json_extract(readfile('\'"$1"\''), '\'"$2"\'');'; }
make_insert_stmt() { echo 'insert into news(published,url,title,body) values('\'"$1"\'','\'"$2"\'','\'"$3"\'','\'"$4"\'');'; }
# $1: NewsApiJSONパス
run() {
    local json_path="$1"
    local insert_sql="insert.sql"
    [ 'ok' != "`json_extract "$json_path" '$.status'`" ] && { echo 'エラー。JSONのstatusがokでない。: '"`json_extract "$json_path" '$.status'`" 1>&2; exit 1; }
    # SQLファイル内容を空にする(さもなくば連続使用時に前の分と合わせて追記されてしまう)
    : > "$insert_sql"
    local totalResults="`json_extract "$json_path" '$.totalResults'`"
    for idx in $(seq 0 $(expr $totalResults - 1)); do
        # JSONから項目を抽出する
        local published="`json_extract "$json_path" '$.articles['"$idx"'].publishedAt'`"
        local url="`json_extract "$json_path" '$.articles['"$idx"'].url'`"
        local title="`json_extract "$json_path" '$.articles['"$idx"'].title'`"
        local body="`json_extract "$json_path" '$.articles['"$idx"'].description'`" # とりあえずdescriptionで代用する
        # totalResultsが多すぎたとき各項目はNULL(空文字)になる。このときは終了する。JSONが正しい限り起こり得ない。
        [ -z "$title" ] && { echo "JSON不正。titleが空。totalResults:$totalResults,idx:$idx" 1>&2; break; }
        # insert文を作る
        make_insert_stmt "$published" "$url" "$title" "$body" >> "$insert_sql"
    done
    local db="news.db"
    sqlite3 "$db" < "$insert_sql"
}
run "$1"

JSONからSQLiteDBへ挿入する方法

  • 1件ずつinsert発行
  • TSVを作ってimport
    • id入力しなくてもいいなら楽でいいのだが……
  • SQLファイルを作成して実行
    • 一時ファイルが必要だしinsert文も書かなきゃだが、一番マシ
    • SQLファイルによる実行は自動的にトランザクションになる

SQLファイル初期化

 本スクリプトを実行するたびに初期化する。さもなくば前回のinsert文が残ったまま、再実行されてしまう。

 以下のようなコードでファイルサイズを0にできるらしい。bashはまだまだ謎が多い闇。

: > 対象ファイルパス

エスケープ地獄

 json_extract(),make_insert_stmt()のクォート事情がとてつもなくややこしい。

json_extract()

bashメタ文字

 最初はJSONファイルパスでなく、ファイル内容を渡そうとした。だが、これはテキスト内にクォート文字がある場合があるため断念した。JSONテキストをbashにて展開すると、bash文脈で解釈される。リテラルとして扱ってくれない。もしJSON内にクォート文字があれば、それはbash文脈のメタ文字として解釈されてしまう。ダブルクォートはJSONのメタ文字なので必ず出る。シングルクォートだってあるかもしれない。よってbashの文が破壊されてしまう。

 これを回避するため、SQL関数readfile(パス)を使った。JSONメタ文字であるダブルクォートが展開されず、ファイル名でしかない。よって先述のようなbash文破壊は起こらない。ファイルパスにクォート文字があれば壊れるだろう。

 ただ、ループ回数(ニュース件数)分だけI/Oが生じてしまう。その非効率を避けたくて最初はファイル内容を渡していたのだが……。何かこの問題を解決できる方法はないか。

bashシンクルクォートをエスケープする方法

echo 'I'\''m ''Andy'
I'm Andy

 可読性が最悪。

sqlite3 :memory: 'select json_extract('\''{"a":2,"c":[4,5,{"f":7}]}'\'', '\''$'\'');'
sqlite3 :memory: 'select json_extract(readfile('\''test.json'\''),'\''$.status'\'');'
  • SQL文脈として文字列はシングルクォートせねばならない
  • JSON文脈として文字列はダブルクォートせねばならない
  • BASH文脈として文字列はシングルクォート、展開するならダブルクォートせねばならない
  • BASH文脈として、シングルクォートをエスケープするなら文字列\'文字列とすること

 まるで絡まる網の目をかいくぐるかのような過酷な作業。人間業じゃない。

分離すべき文脈の混在

 このコードには、「JSONからSQLite3DBに取り込む」という本質的な部分以外が多すぎる。可読性どころかメンテすら困難になる。ぜひ分離したいのだが、bash文脈では難しいか。

  • NewsApi仕様(JSON仕様)
  • JSON解析
  • SQLite3仕様
    • 取り込み(insertと一時ファイル)

 はっきりいって、「お前らの事情なんてどうでもいい。さっさと本質のコードを書かせろ」と思う。特にSQLファイルの作成とかバカバカしい。何とかならないか。

 これらを分離したい。O/Rマップしたい。JSON構造とソースコードマッピングとかもしたい。自動で。

所感

 あまりにも多数の問題が内在しているbashコード。いずれpythonなどに移植しないと管理不能なクソコードになるだろう。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux