とりあえず重複とか考えずに。
成果物
前提
- NewsApiを使ってみた
- NewsApiでカテゴリ別にニュースを取得する
- NewsApiで得たニュースを重複なく取り込む方法を考える
- NewsApiで得たニュースを保存するSQLite3テーブルを考える
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へ挿入する方法
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文脈では難しいか。
はっきりいって、「お前らの事情なんてどうでもいい。さっさと本質のコードを書かせろ」と思う。特にSQLファイルの作成とかバカバカしい。何とかならないか。
これらを分離したい。O/Rマップしたい。JSON構造とソースコードのマッピングとかもしたい。自動で。
所感
あまりにも多数の問題が内在しているbashコード。いずれpythonなどに移植しないと管理不能なクソコードになるだろう。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- MeCab 0.996ユーザ辞書
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux