Pythonロボティクスコース レッスン 40
テーマ.10-3 ユーザー認証システムの制作
情報を管理するためのデータベースや文章の検索に使われる正規表現について学ぼう
チャプター1
このレッスンで学ぶこと
このレッスンでは、情報を整理して管理するための「データベース」の技術と、ある文章の中から特定の文字列を検索するときなどに使われる「正規表現とマッチング」の技術について新たに学習します。
チャプター2
データベースとは
皆さんの中には、「データべース」という言葉を初めて聞いた人もいるのではないでしょうか。実は言葉を知らないだけで、私たちは普段からインターネットを通して、データベースをよく利用してます。
例えば、GoogleやBingなどの検索エンジンを利用すると、世界中にある多種多様な情報へ簡単にアクセスできます。情報にアクセスできるのは、世界のどこかにそのデータが蓄積されているためです。そして、情報を蓄積・管理する仕組みのひとつとして広く使われているものが、これから説明するデータベースです。
2. 1 データベースの基本
データベースに保管されている情報は、あとから分析や検索ができるように、特定のルールに従って整理されています。この情報の整理の仕方によってデータべースはいくつかの種類に分類されています。
■ データベースの種類
ここでは、5種類のデータベースについて簡単に紹介します。
- カード型データベース
1つのまとまった情報を1枚のカードとして管理する古い型のデータベース。現在はほとんど使われていない。
 - key-value型データベース
キー(key)と値(value)のセットで情報を管理するシンプルなデータベース。動作が軽いため、膨大な情報を管理するときに使われる。
 - リレーショナルデータベース
テーブルと呼ばれる複数の表で情報を管理する仕組みで、現在最も使われているデータベース。あるテーブルの値が別のテーブルのキーとなっていて、テーブル間の情報に関連性を持たせている。
 - ネットワーク型データベース
網目状に情報がつながって管理されているデータベース。情報どうしは親子関係で表現されている。
 - オブジェクト指向データベース
情報を1つのオブジェクトとして管理するデータベース。Pythonのようなオブジェクト指向のプログラミング言語と相性が良く、プログラム内のオブジェクトの形を崩さずに情報を保管することができる。
 
一般的には、保管する情報の性質や量によって、これらの中から適したタイプのデータベースを選んで使用します。残念ながら、MicroPythonにはkey-value型のデータベースを構築するための機能しか用意されていません。しかし、これだけでもデータベースの良さを体感するには十分です。ここからは実際にデータベースを構築して、どういった機能が提供されているのかをもう少し詳しく見ていきましょう。
2. 2 MicroPythonでのデータベースの構築
Micropythonには、key-value型のデータベースを扱うための、btreeモジュールが用意されています。btreeモジュールで作成したデータベースは、辞書型と同じようにキーを使ってデータを保管したり、データにアクセスしたりすることができますが、辞書型とは次の2点で大きな違いがあります。
- キーと値はどちらもバイト列である。
 - キーの範囲検索を行うことができる。
 
では、実際にデータベースの構築とデータベース上の情報を検索するプログラムを作成しながら、使い方を見ていきましょう。
■ データベースを作成する
データベースを作成するためには、情報を記録しておくためのファイルが必要です。このファイルは「バイナリモード※」で開きます。
※ バイナリモードとは・・・例えば、テキストモードでは、\rや\r\nのような特定の文字列を意味のある改行コードとして扱いますが、バイナリモードではそのまま他の文字列と同様に扱います。つまり、あるがままにデータを読み書きするモードです。
そして、開いたファイルをbtreeモジュールのopen()関数に渡し、データベースのオブジェクトを作成します。
import btree
try:
# 読み込み(r+なので書き込みも可)のバイナリモード(b)でファイルを開く
file = open("mydb", "r+b")
except OSError: # ファイルが存在しなかった場合は「OSError」が発生
# 書き込み(w+なので読み込みも可)のバイナリモード(b)でファイルを開く。
# ファイルが存在しない場合は新たに作成する。
file = open("mydb", "w+b")
db = btree.open(file) # ファイルからデータベースのオブジェクトを作成
参考:"r+b" と "w+b" は、Pythonのopen()関数を使ってファイルを開くときに指定するモードです。それぞれの意味を解説します。
"r+b" モード
- 読み書き可能なバイナリモードでファイルを開きます。
 "r"は読み取り専用を意味しますが、"+"を付けることで読み書き両方が可能になります。"b"はバイナリモードを意味します。バイナリモードでは、ファイルの内容がそのままバイト列として扱われます。- すでに存在しているファイルを開きます。ファイルが存在しない場合はエラーが発生します。
 
"w+b" モード
- 読み書き可能なバイナリモードでファイルを開きます。
 "w"は書き込み専用を意味しますが、"+"を付けることで読み書き両方が可能になります。"b"はバイナリモードです。- ファイルが存在しない場合は新しいファイルが作成されます。
 - すでに存在するファイルを開くと、そのファイルの内容は最初に削除されて空っぽになります(既存の内容が消えてしまいます)。
 
まとめ
"w+b" は、新しいファイルを作成して読み書きするか、既存のファイルの内容を削除してから読み書きしたい場合に使います。
"r+b" は、既存のファイルをバイナリモードで読み書きしたい場合に使います。ファイルがないとエラーになります。
■ データベースに情報を保管する
作成したデータベースのオブジェクトにデータを保管するときは、辞書型と同じ書き方をします。
db[キー] = 値
ただし、キーも値もバイト列である必要があるため、文字列型のencode()メソッドや数値型のto_bytes()メソッドでバイト列型に変換しておきます。例として、以下の情報をデータベースに追加してみましょう。
| キー:果物名 | 値:価格(円) | 
|---|---|
| りんご(apple) | 150 | 
| オレンジ(orange) | 250 | 
| ばなな(banana) | 250 | 
※ 辞書と同様にキーはデータベース内で重ならないようにする必要があります。値は同じものがあっても問題ありません。
追加【11行目~14行目】
import btree
try:
file = open("mydb", "r+b")
except OSError:
file = open("mydb", "w+b")
db = btree.open(file)
# データべ―スへの情報の追加
db["apple".encode("utf-8")] = (150).to_bytes(2, "big")
db["orange".encode("utf-8")] = (250).to_bytes(2, "big")
db["banana".encode("utf-8")] = (250).to_bytes(2, "big")
db.flush() # ファイルに書き込む
13行目まででは、データは一時的にデータベースに保管された状態になっています。データベースの元になっているファイルへ書き込み、永久的に保管する場合は、最後にflush()メソッドを実行します。
参考:「ビッグエンディアン」と「リトルエンディアン」の違い
https://wa3.i-3-i.info/diff112endiannes.html
■ データベースにある情報を検索する
情報を検索する場合も、キーがバイト列であること以外は辞書と同じ使い方ができます。上で追加した3つのデータをデータベースから読み込んでターミナルに表示してみましょう。
【 サンプルコード 2-2-1 】
追加【16・17行目、20行目~23行目、26行目~28行目、31行目~33行目】
import btree
try:
file = open("mydb", "r+b")
except OSError:
file = open("mydb", "w+b")
db = btree.open(file)
db["apple".encode("utf-8")] = (150).to_bytes(2, "big")
db["orange".encode("utf-8")] = (250).to_bytes(2, "big")
db["banana".encode("utf-8")] = (250).to_bytes(2, "big")
db.flush()
# データの検索
price = db["apple".encode("utf-8")]
print("apple", int.from_bytes(price, "big")) # バイト列から数値に戻して表示
# keysメソッドの利用
print("Using keys()...")
for key in db.keys():
price = db[key]
print(key.decode("utf-8"), int.from_bytes(price, "big")) # バイト列から文字列や数値に戻してから表示
# valuesメソッドの利用
print("Using values()...")
for val in db.values():
print(int.from_bytes(val, "big")) # バイト列から数値に戻して表示
# itemsメソッドの利用
print("Using items()...")
for key, val in db.items():
print(key.decode("utf-8"), int.from_bytes(val, "big")) # バイト列から文字列や数値に戻してから表示
# データベースとファイルを閉じる
db.close()
file.close()
(実行結果)
apple 150 Using keys()... apple 150 banana 250 orange 250 Using values()... 150 250 250 Using items()... apple 150 banana 250 orange 250
btreeのオブジェクトには、辞書型と同様に、keys()やvalues()、items()のように順番にデータを取り出すメソッドも用意されています。また、データベースを使い終えた後は、close()メソッドで閉じ、さらにデータベースで利用していたファイルも閉じるようにしましょう。閉じる順番は必ず「データベース ⇒ ファイル」とすることに注意してください。
■ 範囲検索を行う
keys()やvalues()、items()メソッドは、範囲を絞ってキーや値を検索することもできます。上の【 サンプルコード 2-2-1 】でkeys()メソッドを利用していたコードを以下のように書き換えて、実行してみましょう。
追加・変更【21行目~23行目】
# keysメソッドの利用
print("Using keys()...")
start = "a".encode("utf-8") # 検索の開始位置
end = "c".encode("utf-8") # 検索の終了位置
for key in db.keys(start, end): # 引数に検索範囲を指定する
price = db[key]
print(key.decode("utf-8"), int.from_bytes(price, "big"))
(実行結果)
Using keys()... apple 150 banana 250
書き換えたコードでは、「"a"~"c"」と範囲を絞ってキーの検索を行っているため、「orange」はヒットしていません。
ここまでで、データベースの基本を確認してきました。次は、文章内の特定の文字列の検索に使われたり、入力された情報に誤りがないかをチェックしたりするときに使われる「正規表現」と正規表現を利用した「マッチング」について見ていきましょう。
チャプター3
正規表現とマッチング
皆さんもインターネット上で提供されているサービスを利用するときに、会員になるために個人情報の登録を求められた経験があるのではないでしょうか。個人情報には、名前やメールアドレス、電話番号、住所などサービスを利用するために必要な様々な情報が含まれています。もし、利用者が誤った情報を登録してしまうと、サービスを利用するときに不都合が生じてしまいます。
【 誤った情報を登録することで起きる問題の例 】
- 誤ったメールアドレスを登録したことで、重要なお知らせが届かない。
 - 誤った住所を登録したことで、購入した荷物が届かない。
 - 誤った電話番号を登録したことで、サービス提供者が利用者と連絡を取ることができない。
 
このようなトラブルを防ぐために、個人情報の登録時には、利用者へフォームに記入した内容の再確認を促したり、メールアドレスなど特に重要なものは、2回同じものを入力してもらったりしています。
ここで紹介する正規表現とマッチングもこのトラブルを防ぐためによく使われる技術で、あらかじめ設定したパターンに入力された内容がマッチするかどうかをチェックするときに役立ちます。
例えば、メールアドレスや電話番号は、限られた種類の数字や文字の組合せだったり、文字数が制限されていたりするなど、ある程度パターン化されています。
- メールアドレス:XXX@XX.XX.XX
 - 携帯の電話番号:XXX-XXXX-XXXX
 
このパターンを定義した表現が「正規表現」で、正規表現で定義されたパターンに検索対象の文字列が当てはまるかどうかをチェックする処理が「マッチング」です。
まずは、正規表現から詳しく見ていきましょう。
3. 1 正規表現
正規表現では、通常の文字と「メタ文字」や「特殊シーケンス」と呼ばれる特殊な文字や記号を組み合わせて、特定の文字列のパターンを表します。
例えば、上で確認した携帯の電話番号を正規表現で表すと次のようになります。
"^\d\d\d-\d\d\d\d-\d\d\d\d$"
Pythonの正規表現では、パターンを文字列型のオブジェクトとして定義します。上のパターンにある最初の「^」と最後の「$」は、それぞれ文字列の先頭と末尾を表すメタ文字です。そして、複数個並んでいる「\d」は「0~9」の範囲にある数字を表す特殊シーケンスです。つまりこれで、「012-3456-7890」のように、‐(ハイフン)で区切られた数字が並ぶパターンを表しています。
MicroPythonでは、紹介した3つ以外にも次のメタ文字や特殊シーケンスを使って、パターンを表すことができます。数が多いため、いきなりすべてを覚えるのは大変ですので、練習としていくつかのパターンを定義しながら覚えていきましょう。
【 MicroPythonで扱えるメタ文字の一覧 】
| メタ文字 | 説明 | 指定例 | マッチする文字列 | 
|---|---|---|---|
. | 改行以外の任意の一文字にマッチします。 | a.c | abc, acc, aac | 
^ | 文字列の先頭にマッチします。改行がある場合は各行の先頭にマッチします。 | ^abc | abcdef | 
$ | 行の末尾にマッチします。 | abc$ | defabc | 
* | 直前の正規表現を0回以上の繰り返したものにマッチします。 | ab* | a, ab, abb, abbb | 
+ | 直前の正規表現を1回以上の繰り返したものにマッチします。 | ab+ | ab, abb, abbb | 
? | 直前の正規表現を0回または1回繰り返したものにマッチします。 | ab? | a, ab | 
[◎〇△] | 文字の集合を表します。[]内ののどれか1文字にマッチします。 | [abc] | a, b, c | 
[◎-〇] | -を2つの文字で挟んで指定すると、連続した文字の範囲の集合を表します。 | [a-e] | a, b, c, d, e | 
[^◎〇△] | [の次に^を付けると、集合にない文字にマッチします。 | [^a-z] | 0, 1, 2, A, B, C | 
| | |を挟んでいずれかの正規表現にマッチする正規表現を作成します。 | ab|cd | abd, acd | 
(...) | 正規表現をグループ化します。 | (a[bc]?)+ | a, ab, aba, abac | 
\ | エスケープ文字。バックスラッシュの後に続く文字は通常の文字として扱われます。 | \.\?\+ | .?+ | 
【 MicroPythonで扱える特殊シーケンスの一覧 】
| 特殊シーケンス | 説明 | 同じ意味の正規表現 | 
|---|---|---|
\d | 数字にマッチします。 | [0-9] | 
\D | 数字以外にマッチします。 | [^0-9] | 
\s | 空白にマッチします。 | [\t-\r] | 
\S | 空白以外にマッチします。 | [^\t\n\r\f\v] | 
\w | 任意の英数字にマッチします。 | [a-zA-Z0-9_] | 
\W | 任意の英数字以外にマッチします。 | [^a-zA-Z0-9_] | 
■ 正規表現の練習
では、練習として次の例にマッチするパターンを正規表現で表してみましょう。
- Python(決まった言葉)
 
"^Python$"
- 「a」で始まり「e」で終わる5文字の英単語
 
"^a[a-z][a-z][a-z]e$"
- 頭文字が「a」または「b」の3文字以上の英単語
 
"^[ab][a-z][a-z]+$"
- 7桁の郵便番号(XXX-XXXXまたはXXXXXXXの形式)
 
"^\d\d\d-\d\d\d\d|\d\d\d\d\d\d\d$"
- メールアドレス(XXXXXX@XXXX.XX.XX)※ この正規表現は、英数字と
_(アンダースコア)や-(ハイフン)のみを使用したメールアドレスを想定しています。 
"^(\w|-)+@(\w|-)+(\.(\w|-)+)+$"
メールアドレスは少し難しいので解説すると、正規表現が3つのパートに分かれていて、それぞれ次のパターンにマッチするようになっています。

それでは、このメールアドレスの正規表現を使って、実際にマッチングを行うプログラムを作成してみましょう。
3. 2 マッチング
マッチングを行うときは、MicroPythonのureモジュールを使います。ureモジュールには以下の関数が用意されています。
【 ureモジュールの関数 】
| 関数名 | 内容 | 
|---|---|
compile(正規表現) | 正規表現をコンパイル※して、regex※オブジェクトを返します。 | 
match(正規表現, 検索対象の文字列) | 正規表現をコンパイルして、対象の文字列とマッチするかどうかを照合します。マッチする場合はmacthオブジェクトを返し、マッチしない場合はNoneオブジェクトを返します。対象の文字列の先頭から照合を行うため、文字列の途中に正規表現とマッチする部分あった場合でもNoneオブジェクトが返される。 | 
search(正規表現, 検索対象の文字列) | 正規表現をコンパイルして、対象の文字列とマッチするかどうかを照合します。マッチする場合はmacthオブジェクトを返し、マッチしない場合はNoneオブジェクトを返します。match関数とは違い、文字列の途中からでも正規表現にマッチする部分があれば、macthオブジェクトを返します。 | 
※ コンパイル・・・プログラミング言語で書かれた文字列を、コンピュータが実行可能な形式に変換すること。
※ 英語で正規表現のことを、「Regular expression」といいます。
compile()関数で返されるregexオブジェクトもureモジュールのmatch()関数やsearch()関数と同じ名前と機能のメソッドを持っています。どちらを使用しても結果は変わりませんが、何度も繰り返しマッチングを行う場合は、正規表現をあらかじめコンパイルしてregexオブジェクトを作成しておく方が、無駄な処理が減るためプログラムの実行効率が良くなります。
また、match()関数とsearch()関数は、どちらも正規表現でマッチングを行う点は同じですが、以下の点に違いがあります。
【 match関数とsearch関数の実行結果の違い 】

■ 入力したメールアドレスが有効かどうかを確認するプログラム
以下のサンプルコードがureモジュールを使用して、ターミナルから入力された文字列がメールアドレスの正規表現とマッチングするかどうかをチェックするプログラムです。コードを書き写して実行し、ターミナルからmain()関数を呼び出して、正しくチェックできることを確認しましょう。
【 サンプルコード 3-2-1 】
import ure
emailaddress = "^(\\w|-)+@(\\w|-)+(\\.(\\w|-)+)+$" # メールアドレスの正規表現
regex = ure.compile(emailaddress) # 正規表現をコンパイルしてregexオブジェクトを作成
def main():
# ターミナルからメールアドレスを入力する
user_emailaddress = input("Input your email adress >>> ")
matchobj = regex.match(user_emailaddress)
if matchobj: # matchオブジェクトが返ってきた場合は有効
print("'{}' is a valid email address.".format(user_emailaddress))
else: # Noneオブジェクトが返ってきた場合は無効
print("'{}' is an invalid email adress.".format(user_emailaddress))
(実行結果)
>>> main() Input your email adress >>> example@ex-ex.exam 'example@ex-ex.exam' is a valid email address. >>> main() Input your email adress >>> sample@ex-ex 'example@ex-ex' is an invalid email adress.
チャプター4
データベースを使った簡単な認証システムの制作
私たちが普段から利用しているSNSやネットショップなど、Web上で提供されているサービスの多くは、利用を始める前にアカウントの登録が求められます。そして、サービスを利用するときには、登録したアカウント情報によるユーザー認証を行った上で、情報の発信や取得、買い物などができるようになっています。

このように限定されたユーザー向けにサービスを提供するWebサイトのシステムでは、その多くが、ユーザーのアカウント情報の記録や管理にデータベースを利用しています。
Webサイト上でユーザーが入力したカウント情報は、そのWebサイトを運用しているサーバーに送信され、そのサーバーからデーターベースへ検索を要求します。そして、サーバーはデータベースから返された検索結果を受けて認証を行い、その結果をユーザーに知らせる仕組みになっています。

また、事前にユーザーが入力した情報の書式に誤りがないかどうかをチェックすることも大切です。特に新しくユーザーがアカウント情報を登録するときは、メールアドレスやパスワードなど大事な情報に誤りがあると、サービスを提供できなくなるため、正規表現とマッチングの機能を使用して書式に間違いがあれば、ユーザーに知らせます。

実際にサービスとして使える認証システムを構築するためには、他にもHTTPメソッドや暗号化通信、本格的なデータベース操作など数多くの専門知識が必要です。しかしながら、学習用のシンプルな認証システムなら、このレッスンで学習したデータベースと正規表現・マッチングを応用して制作することができます。
それでは、実際にプログラムを作成しながら認証システムの構築方法を確認していきましょう。
4. 1 シスステムに必要な機能の整理
これから作成する認証システムでは、「ユーザーのアカウント情報(メールアドレスとパスワード)の登録機能」と「システムへのログイン機能」の2つだけを提供します。まずは、これらの機能を実現するための処理の流れを見ていきましょう。
【 アカウント情報の登録に必要な処理 】
- ターミナルから新規のアカウント情報の入力を求める
 - 入力されたアカウント情報の書式をチェックする
 - データベースを開く
 - データベースへアカウント情報を登録する
 - データベースを閉じる
 
【 システムへのログインに必要な処理 】
- ターミナルから登録済みのアカウント情報の入力を求める
 - 入力されたアカウント情報の書式をチェックする
 - データベースを開く
 - データベース上でアカウント情報を検索して照合する
 - データベースを閉じる
 - 認証結果を表示する
 
整理してみると、2つの機能には共通する処理がいくつかあります。そこで、共通する処理は別で関数にまとめることにして、全体で以下の関数を作成します。
【 作成する関数の一覧 】
|関数名|処理内容|
|:—|:—|
|open_db()関数|データベースを開く処理|
|close_db()関数|データベースを閉じる処理|
|get_account_info()関数|ターミナルからアカウント情報の入力を求める処理と入力された情報の書式をチェックする処理|
|register()関数|アカウント情報の登録を行う処理|
|login()関数|システムへのログインを行う処理|
それでは、この表の上から順番に関数を作成していきましょう。
4. 2 認証システムのプログラムの作成
2つの機能で共通して必要な処理から用意していきます。
■ open_db関数の作成
データベースの開き方は、前半のレッスンで説明した通りです。データベースやデータベースに使用するファイルのオブジェクトはグローバル変数として管理するようにしましょう。
import btree
file = None # データベースに使用するファイルのオブジェクト
db = None # データベースのオブジェクト
def open_db():
global db, file # グローバル宣言が必要
try:
file = open("mydb", "r+b") # ファイルが存在している場合はそれを開く
except OSError:
file = open("mydb", "w+b") # ファイルが存在していない場合は新たに作成する
db = btree.open(file) # データベースを開く
■ close_db関数の作成
「データベース ⇒ ファイル」の順で閉じる必要があったことに注意して、下記のようにコードを書きましょう。
追加【15行目~19行目】
import btree
file = None
db = None
def open_db():
global db, file
try:
file = open("mydb", "r+b")
except OSError:
file = open("mydb", "w+b")
db = btree.open(file)
def close_db():
if db: # データベースを閉じる
db.close()
if file: # ファイルを閉じる
file.close()
■ get_account_info関数の作成
次は、ターミナルからメールアドレスとパスワードの入力を求め、入力された文字列がそれぞれの書式に沿っているかどうかをチェックする関数を作成します。
メールアドレスの正規表現はレッスンの前半で確認したものをそのまま利用します。パスワードは、ここでは6文字~8文字の英数字としておきましょう。それぞれ正規表現を定義して、regexオブジェクトを作成しましょう。
追加【2行目、6行目~9行目】
import btree
import ure
file = None
db = None
pat_email = "^(\w|-)+@(\w|-)+(\.(\w|-)+)+$" # メールアドレスの正規表現
pat_pass = "^\w\w\w\w\w\w$|^\w\w\w\w\w\w\w$|^\w\w\w\w\w\w\w\w$" # パスワードの正規表現
regex_email = ure.compile(pat_email) # コンパイルされたメールアドレスの正規表現
regex_pass = ure.compile(pat_pass) # コンパイルされたパスワードの正規表現
「メールアドレス ⇒ パスワード」の順に、ターミナルからの入力を受け取り、用意した正規表現とマッチするかどうかを確認します。もしマッチしなかった場合は、再度入力を促します。問題がなければ、受け取ったメールアドレスとパスワードをタプルにまとめて、この関数の呼び出し元へ返します。
追加【27行目~42行目】
def close_db():
if db:
db.close()
if file:
file.close()
def get_account_info():
while True:
email_address = input("Email address >>> ") # メールアドレスの入力
matchobj = regex_email.match(email_address) # 書式のチェック
if matchobj: # 正規表現とマッチする場合
break # 28行目のwhile文を抜ける
else: # 正規表現とマッチしない場合
print("Invalid email address. Please try again.") # 再入力を促す
while True:
password = input("Password (6-8 characters) >>> ") # パスワードの入力
matchobj = regex_pass.match(password) # 書式のチェック
if matchobj: # 正規表現とマッチする場合
break # 35行目のwhile文を抜ける
else: # 正規表現とマッチしない場合
print("Invalid password. Please try again.") #再入力を促す
return (email_address, password) # 受け取った情報をタプルにまとめて返す
■ register関数の作成
入力されたアカウント情報をデータベースへ登録する関数を作成します。上で作成した3つの関数を利用して、次の手順で処理を進めましょう。
追加【45行目~53行目】
def register():
account_info = get_account_info() # アカウント情報の入力受付
email_address = account_info[0] # アカウント情報(メールアドレス)
password = account_info[1] # アカウント情報(パスワード)
open_db() # データベースを開く
db[email_address.encode("utf-8")] = password.encode("utf-8") # データベースへの一時的な記録
db.flush() # 恒久的なデータとしてファイルへ書き込む
close_db() # データベースを閉じる
print("Your account is registered.\n", email_address, password) # 登録が完了したことを知らせる
作成したregister()関数で、データベースへアカウント情報が登録できることを確認します。データベース上のアカウント情報が確認できるように、一覧を表示するshow_db()関数を追加して、テスト用のメールアドレス(test@sample.com)とパスワード(testtest)でプログラムを試しましょう。
追加【56行目~60行目】
def show_db():
open_db() # データベースを開く
for key, value in db.items(): # データベースに記録されているアカウント情報を順番に表示
print(key.decode("utf-8"), value.decode("utf-8"))
close_db() # データベースを閉じる
(プログラムの実行例)
>>> register() Email address >>> test@sample.com Password (6-8 characters) >>> testtest Your account is registered. test@sample.com testtest >>> show_db() test@sample.com testtest
■ login関数の作成
最後に、入力されたアカウント情報をデータベース上で検索して、ログイン認証を行う関数を作成します。先に入力されたメールアドレスがデータベース上に存在していることを確認し、存在していた場合のみパスワードを照合するようにしましょう。
追加【63行目~78行目】
def login():
account_info = get_account_info() # アカウント情報の入力を受け付ける
email_address = account_info[0] # アカウント情報(メールアドレス)
password = account_info[1] # アカウント情報(パスワード)
open_db() # データベースを開く
try: # データベース上に入力されたメールアドレスが登録されているかどうかを確認する
db_pass = db[email_address.encode("utf-8")]
except KeyError: # メールアドレスが登録されていないとき
print("Your email address is not registered.")
close_db() # データベースを閉じてここで処理を終了する
return
close_db() # 登録があった場合はデータベースを閉じて、パスワードが一致するかどうかを確認する
if db_pass == password.encode("utf-8"):
print("'{}' logged in.".format(email_address)) # 認証成功時のメッセージ
else:
print("The password you entered is incorrect.") # 認証失敗時のメッセージ
4. 3 プログラムの動作確認
これでプログラムの完成です。register()関数の動作確認時に登録しておいたアカウント情報がデータベース内に残っているので、これを使用してログインができるかどうかを確認しましょう。
>>> login() Email address >>> test@sample.com Password (6-8 characters) >>> testtest 'test@sample.com' logged in.
もし、プログラムの実行時にエラーが出てしまい、うまく解決できない場合は、下のサンプルコードと見比べて、書いたコードに誤りがないかどうかを確かめましょう。
【 サンプルコード 4-3-1 】
import btree
import ure
file = None
db = None
pat_email = "^(\w|-)+@(\w|-)+(\.(\w|-)+)+$"
pat_pass = "^\w\w\w\w\w\w$|^\w\w\w\w\w\w\w$|^\w\w\w\w\w\w\w\w$"
regex_email = ure.compile(pat_email)
regex_pass = ure.compile(pat_pass)
def open_db():
global db, file
try:
file = open("mydb", "r+b")
except OSError:
file = open("mydb", "w+b")
db = btree.open(file)
def close_db():
if db:
db.close()
if file:
file.close()
def get_account_info():
while True:
email_address = input("Email address >>> ")
matchobj = regex_email.match(email_address)
if matchobj:
break
else:
print("Invalid email address. Please try again.")
while True:
password = input("Password (6-8 characters) >>> ")
matchobj = regex_pass.match(password)
if matchobj:
break
else:
print("Invalid password. Please try again.")
return (email_address, password)
def register():
account_info = get_account_info()
email_address = account_info[0]
password = account_info[1]
open_db()
db[email_address.encode("utf-8")] = password.encode("utf-8")
db.flush()
close_db()
print("Your account is registered.\n", email_address, password)
def show_db():
open_db()
for key, value in db.items():
print(key.decode("utf-8"), value.decode("utf-8"))
close_db()
def login():
account_info = get_account_info()
email_address = account_info[0]
password = account_info[1]
open_db()
try:
db_pass = db[email_address.encode("utf-8")]
except KeyError:
print("Your email address is not registered.")
close_db()
return
close_db()
if db_pass == password.encode("utf-8"):
print("{} logged in.".format(email_address))
else:
print("The password you entered is incorrect.")
確認方法:
1. show_bd() で登録情報を確認
2. login() でログインできるか確認
チャプター5
課題:データベースへ登録する情報の追加
【 サンプルコード 3-3-1 】では、データベースへ登録するアカウント情報として、キーにメールアドレスを、値にパスワードを設定していました。実際のサービスで使われているデータベースには、この他にもユーザーの名前や生年月日、電話番号など様々な情報が登録されています。
そこでこの課題では、ユーザーのニックネーム(nickname)をアカウント情報に追加して登録できるようにしてみましょう。
5. 1 複数の情報をひとつの値として記録する方法
一般的なデータベースシステムでは、キーに対して複数の値をひもづけすることができますが、bTreeモジュールのデータベースでは、キーにひとつの値しかひもづけすることができません。
そのため、次の工夫を行い、疑似的に複数の値を登録して利用できるようにします。
- 複数の値をタプルの形式で表記した1つの文字列「
"('password', 'nickname')"」にまとめる。 - 1の文字列をバイト列にエンコードして、データベースに登録する。
 - データベースから取得した値はバイト列からデコードして文字列に戻す。
 - 3の文字列を
eval()関数で評価して、タプル('password', 'nickname')を作成して利用する。 
では、認証成功時に以下のメッセージが表示できるように、get_account_info()、register()、login()の3つの関数を変更していきましょう。
Welcome, (nickname)!
5. 2 プログラムの改造手順
3つの関数を、get_account_info()、register()、login()の順番に変更していきます。
■ get_account_info関数の変更
新たにニックネームを登録できるようにするため、プログラムの冒頭に、ニックネームの正規表現を定義します。ニックネームは特別な制限を設けずに、1文字以上の英数字としておきましょう。
追加【8行目、11行目】
import btree
import ure
file = None
db = None
pat_email = "^(\w|-)+@(\w|-)+(\.(\w|-)+)+$"
pat_pass = "^\w\w\w\w\w\w$|^\w\w\w\w\w\w\w$|^\w\w\w\w\w\w\w\w$"
pat_nickname = "^\w+$" # ニックネームの正規表現
regex_email = ure.compile(pat_email)
regex_pass = ure.compile(pat_pass)
regex_nickname = ure.compile(pat_nickname) # コンパイルしたニックネームの正規表現
ニックネームはアカウントの登録時には入力を求めますが、ログイン時には必要ないため、get_account_info()関数へ新たに引数_fromを追加して、呼び出し元の関数名("register"または"login")を渡してもらい、処理を分けるようにします。
login()関数から呼び出されたときは、メールアドレスとパスワードの入力を受け取った時点で情報を返し、register()関数から呼び出されたときは、続けてニックネームの入力を受け付けます。
変更・追加【29行目、44~53行目】
def get_account_info(_from):
while True:
email_address = input("Email address >>> ")
matchobj = regex_email.match(email_address)
if matchobj:
break
else:
print("Invalid email address. Please try again.")
while True:
password = input("Password (6-8 characters) >>> ")
matchobj = regex_pass.match(password)
if matchobj:
break
else:
print("Invalid password. Please try again.")
if _from == "login": # login関数から呼び出されたときはここで終わり
return (email_address, password) # 2つの入力値だけを返す
while True: # register関数から呼び出されたときは以下のコードも実行
nickname = input("Nickname >>> ")
matchobj = regex_nickname.match(nickname)
if matchobj:
break
else:
print("Invalid nickname. Please try again.")
return (email_address, password, nickname) # 3つの入力値を返す
■ register関数の変更
パスワードとニックネームの2つの情報をタプル形式の文字列にまとめ、それをバイト列にエンコードして、データベースへ登録します。また、get_account_info()関数に呼び出し元の関数名を渡す点に注意しましょう。
変更・追加【57行目、60行目、62・63行目、66行目】
def register():
account_info = get_account_info("register") # 引数に関数名を渡す
email_address = account_info[0] password = account_info[1]
nickname = account_info[2] # 追加されたニックネーム
open_db()
value = "('{}', '{}')".format(account_info[1], account_info[2]) # タプル形式の文字列に2つの情報をまとめる
db[email_address.encode("utf-8")] = value.encode("utf-8") # password から value に変更
db.flush()
close_db()
print("Your account is registered.\n", email_address, password, nickname) # ニックネームも表示
■ login関数の変更
データベースから取得した値をバイト列から文字列にデコーディングします。しして、その文字列をeval()関数で評価します。この文字列はタプル形式で書かれているため、評価されるとタプルが作成されます。
変更・追加【77行目、82行目、88行目~92行目】
def login():
account_info = get_account_info("login") # 引数に関数名を渡す
email_address = account_info[0]
password = account_info[1]
open_db()
try:
db_value = db[email_address.encode("utf-8")] # dp_pass から db_value に変更
except KeyError:
print("Your email address is not registered.")
close_db()
return
close_db()
db_value = eval(db_value.decode("utf-8")) # バイト列からデコードして取得した文字列をeval関数で評価
db_pass = db_value[0] # パスワード
db_nickname = db_value[1] # ニックネーム
if db_pass == password: # デコードしたパスワードを得ているので条件式も変更
print("Welcome, {}!".format(db_nickname)) # ニックネーム付きのメッセージを表示
else:
print("The password you entered is incorrect.")
5. 3 プログラムの動作確認
完成したプログラム例が以下になります。実行して、動作を確認してみましょう。
【 サンプルコード 5-3-1 】
import btree
import ure
file = None
db = None
pat_email = "^(\w|-)+@(\w|-)+(\.(\w|-)+)+$"
pat_pass = "^\w\w\w\w\w\w$|^\w\w\w\w\w\w\w$|^\w\w\w\w\w\w\w\w$"
pat_nickname = "^\w+$"
regex_email = ure.compile(pat_email)
regex_pass = ure.compile(pat_pass)
regex_nickname = ure.compile(pat_nickname)
def open_db():
global db, file
try:
file = open("mydb", "r+b")
except OSError:
file = open("mydb", "w+b")
db = btree.open(file)
def close_db():
if db:
db.close()
if file:
file.close()
def get_account_info(_from):
while True:
email_address = input("Email address >>> ")
matchobj = regex_email.match(email_address)
if matchobj:
break
else:
print("Invalid email address. Please try again.")
while True:
password = input("Password (6-8 characters) >>> ")
matchobj = regex_pass.match(password)
if matchobj:
break
else:
print("Invalid password. Please try again.")
if _from == "login":
return (email_address, password)
while True:
nickname = input("Nickname >>> ")
matchobj = regex_nickname.match(nickname)
if matchobj:
break
else:
print("Invalid nickname. Please try again.")
return (email_address, password, nickname)
def register():
account_info = get_account_info("register")
email_address = account_info[0]
password = account_info[1]
nickname = account_info[2]
open_db()
value = "('{}', '{}')".format(account_info[1], account_info[2])
db[email_address.encode("utf-8")] = value.encode("utf-8")
db.flush()
close_db()
print("Your account is registered.\n", email_address, password, nickname)
def show_db():
open_db()
for key, value in db.items():
print(key.decode("utf-8"), value.decode("utf-8"))
close_db()
def login():
account_info = get_account_info("login")
email_address = account_info[0]
password = account_info[1]
open_db()
try:
db_value = db[email_address.encode("utf-8")]
except KeyError:
print("Your email address is not registered.")
close_db()
return
close_db()
db_value = eval(db_value.decode("utf-8"))
db_pass = db_value[0]
db_nickname = db_value[1]
if db_pass == password:
print("Welcome, {}!".format(db_nickname))
else:
print("The password you entered is incorrect.")
確認方法:
1. register() でemail、PW、ニックネームを登録する(3つくらい登録してください)
2. show_db() で登録内容を確認
3.  login() でログインができ、ニックネームが表示されればOK!
※注意!:チャプター4で登録したemailアドレスとPWの組み合わせは、show_db()で確認はできますが、login()では認識できません。DBへの書き込みなど、プログラムの内容が4と5では違うためです。
チャプター6
おわりに
6. 1 このレッスンのまとめ
今回は、「データベース」と「正規表現とマッチング」について新たに学習しました。
データベースと正規表現はどちらもインターネットを利用したサービスを開発する上では欠かせない技術です。Pythonには様々な開発環境が用意されているため、ArtecRoboのようなロボットを制御することもできれば、ゲームやアプリケーションを開発したり、インターネット上のサーバーを開発したりすることだってできます。
もう既に私達の生活はインターネットなしでは成り立たなくなってきています。もし、将来的に自分で何か新しいサービスを開発したいという思うようになったとき、このレッスンで学んだことがきっと役に立つでしょう。
6. 2 次のレッスンについて
次のレッスンでは、ネットワーク上での情報のやり取りを安全に行うための技術について学習していきます。