質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Q&A

解決済

1回答

1763閲覧

Python3 Tkinter データベースにデータを書いてからCSV出力したい。

person

総合スコア223

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

0グッド

1クリップ

投稿2020/10/08 00:05

編集2020/10/08 06:32

まずTkinterで、ボタンを押したらデータベースにデータを保存するプログラムを作りました。

Python

1def ボタンが押されたら(): 2 MySQLへデータを保存

のような要領です。

しかし、Raspberry Piではデータベースの書き込みに時間がかかるようで、ボタンを押したら数秒間ボタンが押し込みっぱなしの画面になります。

それが嫌だったので、別スレッドでデータを保存することにしました。

以下、ソースコードです。

Python

1from datetime import datetime 2import csv 3import pymysql 4import queue 5import threading 6import tkinter as tk 7 8tbl = "testtb" 9tmp = [] 10 11def cnct(): 12 global con, cur 13 # データベース接続 14 con = pymysql.connect( 15 host = "localhost", 16 user = "root", 17 password = "root", 18 db = "testdb", 19 charset = "utf8" 20 ) 21 cur = con.cursor() 22 23 24def discnct(): 25 global con, cur 26 # データベース切断 27 cur.close() 28 con.close() 29 30""" 31def read_and_csvout(): 32 global cur, tbl 33 cur.execute("SELECT * FROM " + tbl + ";") 34 results = cur.fetchall() 35 lst = [] 36 for i in range(len(results)): 37 tmp = results[i] 38 lst.append([ tmp[0], tmp[1], tmp[2] ]) 39 with open("/home/pi/デスクトップ/data.csv", "a", newline="") as f: 40 writer = csv.writer(f, lineterminator="\r\n") 41 for i in range(len(lst)): 42 writer.writerow(lst[i]) 43""" 44 45 46def pushed1(e): 47 global data 48 # データベース保存用データをキューに格納 49 for i in range(50): 50 data.put([str(i+1), "ButtonPushed1", ""]) 51 52 53def pushed2(e): 54 global data 55 # データベース保存用データをキューに格納 56 for i in range(50): 57 data.put([str(i+1), "ButtonPushed2", ""]) 58 59 60 61def loop(): 62 global loopflag, data, cur, con, tmp 63 # キュー監視ループ 64 while loopflag: 65 # キューにデータが存在したら、順番通りにデータベースへ保存する 66 while not data.empty(): 67 # データベースとの接続がタイムアウトしていたときのための再接続。 68 con.ping(reconnect=True) 69 # キューにある先頭データを取り出し、tmpに格納 70 tmp = data.get() 71 # tmpにあるデータをデータベースに保存 72 cur.execute("INSERT INTO " + tbl + " VALUES(%s,%s,%s);", (tmp[0], tmp[1], tmp[2])) 73 # コミット。データベースに変更を加えたらそれを反映させるために必要。 74 con.commit() 75 76 77def close(): 78 global loopflag, win, thread 79 # スレッド内のループフラグをFalseにする 80 loopflag = False 81 # スレッドが終わるまで待機 82 thread.join() 83 # データベースから切断 84 discnct() 85 # Tkinterアプリの終了 86 win.destroy() 87 88 89if __name__ == "__main__": 90 91 # データベースへ接続 92 cnct() 93 94 # スレッド内のループ用フラグ 95 loopflag = True 96 97 # データベースに保存するデータ格納用 98 data = queue.Queue() 99 100 # Tkinter UI 101 win = tk.Tk() 102 103 # ボタン 104 button1 = tk.Button(win, text="Button1") 105 button1.grid(row=0, column=0, sticky="nsew") 106 button2 = tk.Button(win, text="Button2") 107 button2.grid(row=0, column=1, sticky="nsew") 108 109 # データベース操作スレッド 110 # ※データベース操作はスレッドセーフではないため、1つのスレッドでのみ操作が可能 111 thread = threading.Thread(target=loop) 112 thread.start() 113 114 # ボタンバインド 115 button1.bind("<ButtonRelease>", pushed1) 116 button2.bind("<ButtonRelease>", pushed2) 117 118 # UIクローズ検知 119 win.protocol("WM_DELETE_WINDOW", close) 120 121 # イベントループ 122 win.mainloop() 123 124 125""" 126MariaDB [testdb]> DESC testtb; 127+-------+------+------+-----+---------+-------+ 128| Field | Type | Null | Key | Default | Extra | 129+-------+------+------+-----+---------+-------+ 130| data1 | text | YES | | NULL | | 131| data2 | text | YES | | NULL | | 132| data3 | text | YES | | NULL | | 133+-------+------+------+-----+---------+-------+ 1343 rows in set (0.00 sec) 135""" 136

ボタンを押したら保存するところまではできたのですが、
ここからさらに機能の実装をしたいと考えています。

内容として、Button2を押したらデータをデータベースに保存してからCSV出力するということをしたいです。

例えば、Button1押下→Button2押下とした場合、上のソースコードだと

CSV

11,ButtonPushed1 22,ButtonPushed1 33,ButtonPushed1 4(省略) 549,ButtonPushed1 650,ButtonPushed1 71,ButtonPushed2 82,ButtonPushed2 9(省略) 1049,ButtonPushed2 1150,ButtonPushed2

というCSVを出力したいです。

このときにButton2を押したときの処理が、メインスレッドだとバッファにキューに保存しているだけなのでMySQL操作しているスレッドの動作まで検知できません。

(グローバル変数で適当なフラグを立てることを考えたのですが、Button1のデータ書き込みが終わったのか、Button2のデータ書き込みが終わったのか、そもそも書き込んでいないのかまではloop()内で区別できないような気がします。変数だけじゃなくコールバック関数などを駆使すればできる?)

どのようにすれば 50,ButtonPushed2 をデータベースに保存した後でCSV出力をすることが可能でしょうか。

(ただし、出力データ 50,ButtonPushed2 はあくまでサンプルデータにすぎないので、実際に保存するデータはどうなるか分かりません。そのためスレッドのループ内に下記のような指定をするのはなしでお願いします。)

def loop(): global loopflag, data, cur, con, tmp # キュー監視ループ while loopflag: # キューにデータが存在したら、順番通りにデータベースへ保存する while not data.empty(): # データベースとの接続がタイムアウトしていたときのための再接続。 con.ping(reconnect=True) # キューにある先頭データを取り出し、tmpに格納 tmp = data.get() # tmpにあるデータをデータベースに保存 cur.execute("INSERT INTO " + tbl + " VALUES(%s,%s,%s);", (tmp[0], tmp[1], tmp[2])) # データの内容を指定して、CSV出力(これは解決策としては×) if tmp == ["50", "ButtonPushed2", ""]: sqldata = データベース読み込み() CSV出力(sqldata) データベースクリア() # コミット。データベースに変更を加えたらそれを反映させるために必要。 con.commit()

質問後追記 変更(2020/10/08 14:39)

Python:

1from datetime import datetime 2import csv 3import pymysql 4import queue 5import threading 6import time 7import tkinter as tk 8 9tbl = "testtb" 10tmp = [] 11 12def cnct(): 13 # データベース接続 14 con = pymysql.connect( 15 host = "localhost", 16 user = "root", 17 password = "root", 18 db = "testdb", 19 charset = "utf8" 20 ) 21 cur = con.cursor() 22 23 return con, cur 24 25 26def discnct(con, cur): 27 # データベース切断 28 cur.close() 29 con.close() 30 31 32def read_and_csvout(cur): 33 global tbl 34 cur.execute("SELECT * FROM " + tbl + ";") 35 results = cur.fetchall() 36 lst = [] 37 for i in range(len(results)): 38 tmp = results[i] 39 lst.append([ tmp[0], tmp[1], tmp[2] ]) 40 with open("/home/pi/デスクトップ/data.csv", "a", newline="") as f: 41 writer = csv.writer(f, lineterminator="\r\n") 42 for i in range(len(lst)): 43 writer.writerow(lst[i]) 44 45 46def clear(cur): 47 global tbl 48 cur.execute("DELETE FROM " + tbl + ";") 49 50 51def pushed1(e): 52 global data 53 # データベース保存用データをキューに格納 54 for i in range(50): 55 data.put(("DB-INSERT", [str(i+1), "ButtonPushed1", ""])) 56 data.put(("DB-COMMIT", None)) 57 58 59def pushed2(e): 60 global data 61 # データベース保存用データをキューに格納 62 for i in range(50): 63 data.put(("DB-INSERT", [str(i+1), "ButtonPushed2", ""])) 64 data.put(("DB-COMMIT", None)) 65 data.put(("CSV-OUTPUT", None)) 66 67 68 69def loop(data): 70 con, cur = cnct() 71 # キュー監視ループ 72 while True: 73 # キューにある先頭データを取り出す 74 msg, args = data.get() 75 print(msg, args) 76 if msg == "DB-INSERT": 77 # データベースとの接続がタイムアウトしていたときのための再接続。 78 con.ping(reconnect=True) 79 # tmpにあるデータをデータベースに保存 80 cur.execute("INSERT INTO " + tbl + " VALUES(%s,%s,%s);", (args[0], args[1], args[2])) 81 elif msg == "CSV-OUTPUT": 82 # データベースとの接続がタイムアウトしていたときのための再接続。 83 con.ping(reconnect=True) 84 read_and_csvout(cur) 85 clear(cur) 86 con.commit() 87 elif msg == "DB-COMMIT": 88 con.commit() 89 elif msg == "DB-DISCNCT": 90 break 91 else: 92 break 93 discnct(con, cur) 94 95 96def close(): 97 global data, win, thread 98 data.put(("DB-DISCNCT", None)) 99 thread.join() 100 # Tkinterアプリの終了 101 win.destroy() 102 103 104if __name__ == "__main__": 105 106 # データベースに保存するデータ格納用 107 data = queue.Queue() 108 109 # Tkinter UI 110 win = tk.Tk() 111 112 # ボタン 113 button1 = tk.Button(win, text="Button1") 114 button1.grid(row=0, column=0, sticky="nsew") 115 button2 = tk.Button(win, text="Button2") 116 button2.grid(row=0, column=1, sticky="nsew") 117 118 # データベース操作スレッド 119 # ※データベース操作はスレッドセーフではないため、1つのスレッドでのみ操作が可能 120 thread = threading.Thread(target=loop, args=(data, )) 121 thread.start() 122 123 # ボタンバインド 124 button1.bind("<ButtonRelease>", pushed1) 125 button2.bind("<ButtonRelease>", pushed2) 126 127 # UIクローズ検知 128 win.protocol("WM_DELETE_WINDOW", close) 129 130 # イベントループ 131 win.mainloop() 132 133 134""" 135MariaDB [testdb]> DESC testtb; 136+-------+------+------+-----+---------+-------+ 137| Field | Type | Null | Key | Default | Extra | 138+-------+------+------+-----+---------+-------+ 139| data1 | text | YES | | NULL | | 140| data2 | text | YES | | NULL | | 141| data3 | text | YES | | NULL | | 142+-------+------+------+-----+---------+-------+ 1433 rows in set (0.00 sec) 144""" 145

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

Button2を押したらデータをデータベースに保存してからCSV出力するということをしたいです。

キューに入れるデータに、処理の振り分けフラグを追加します。

python

1# キューの読み出し 2 3msgtype, args = data.get() 4 5if msgtype == "DB-INSERT": 6 pass # DBへ書き込み 7elif msgtype == "CSV-OUTPUT": 8 pass # CSV 出力 9else: 10 break

python

1# キュー書き込み側(細部は省略) 2 3for _ in range(10): 4 data.put(("DB-INSERT", [...])) 5else: 6 data.put(("CSV-OUTPUT", "test.csv"))

この例ではタプルですが、他の処理をスレッドで行いたい場合
引数の数が変わってくると思うので、メッセージを処理別に
namedtuple や dataclasses を使いクラスを定義すると良いです。


別アプローチでの解決策

concurrent.futures モジュールを使うと、
スレッドで処理を行った後、任意の関数を実行できます。
但し、スレッド部分のコードの大幅な変更が必要になるので、参考適度に。


追記: スレッド内ではグローバル変数は極力使わない方が良いです。
スレッドセーフな操作を保証できません。

現状で問題が無ければ無理に修正しなくても良いかもしれませんが、
もし終了時にエラーが出るようなことがあれば、以下を検討して見て下さい。

  • データベースの初期化や後始末はスレッド内(関数loopの頭と末尾)で行う
  • 双方からアクセスするキューはThread のargs引数としてスレッドで実行する関数に渡す。

 キューはスレッドセーフなので、双方のスレッドで扱っても問題ない。

  • キューの読み出しループは、特定のメッセージを受けるとループを中断出来るようにする。
    (loopflag ではなく内側のループ。読み出すメッセージが無いと get で待機したまま止まってます)

投稿2020/10/08 00:50

編集2020/10/08 01:12
teamikl

総合スコア8664

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2020/10/08 01:42

> キューに入れるデータに、処理の振り分けフラグを追加します。 この方法でできました。ありがとうございます。 ちなみに、キュー書き込み側に書かれている for _ in range(10): data.put(("DB-INSERT", [...])) else: data.put(("CSV-OUTPUT", "test.csv")) のelse:に特別な意味があるのでしょうか? forとelseを一緒に書いているのを今まで見たことがないので・・・。 > キューの読み出しループは、特定のメッセージを受けるとループを中断出来るようにする。 else: break と記述されている部分でしょうか? また、「読み出すメッセージが無い」というのは data.put(("message", ["a", "b"])) のようにメッセージを含むデータのみputしていても、そういった状態に陥る可能性があるのでしょうか? (記述するうえで誤ってメッセージを書き損じたりしなければ問題ない?)
teamikl

2020/10/08 02:00 編集

> のelse:に特別な意味があるのでしょうか? for-else は、途中で break した場合、else は実行されません。 中断処理を想定しましたが、ここではキューを使ってるので else は不要でしたね。現状では特に意味はありません。 >メッセージを含むデータのみputしていても、そういった状態に陥る可能性があるのでしょうか? データがあれば大丈夫ですが、 何もない状態でアプリケーションを終了した場合 スレッド側のコードは loopflag 判別にいかず、data.get() で待機されたままになってます。 終了時の処理を以下の様に工夫して見て下さい。 (loop関数の末尾でログ出力して、ループを抜けたのを確認) - loopflag 変更 - キューに何かデータを送る  受け取り側は、そのデータを受け取るとループを抜け loop関数の実行を終了するようにする - thread.join()
teamikl

2020/10/08 02:06

キューでループを抜けられれば loopflag は不要になります。 後、while not data.empty() ~ con.commit() では、 何も無い時にずっと commit してることになりませんか? con.commit() print("COMMIT") として、コミットが呼ばれている回数を確認して見て下さい。 対策: キューで "DB-COMMIT" メッセージを追加する。
teamikl

2020/10/08 02:13

キューの読み出しループについて # ファイル先頭グローバルに配置。 # この値は定数なのでスレッドを跨いだ参照でも問題ありません QUEUE_QUIT = ('QUEUE-QUIT', None) # スレッド内のキュー読み出しループ # iter() の第二引数の値が終了条件になります。 for msgtype, args in iter(data.get, QUEUE_QUIT):  ... # 終了時の処理 data.put(QUEUE_QUIT) thread.join()
person

2020/10/08 03:03

> データベースの初期化や後始末はスレッド内(関数loopの頭と末尾)で行う > 双方からアクセスするキューはThread のargs引数としてスレッドで実行する関数に渡す。 > キューでループを抜けられれば loopflag は不要になります。 > キューで "DB-COMMIT" メッセージを追加する。 質問文に上記を考慮して変えたコードを載せました。ただ、エラーが出ていて、内容としては実行時に Warning (from warnings module): File "/home/pi/デスクトップ/test.py", line 84 global cur, con SyntaxWarning: name 'cur' is used prior to global declaration >>> Warning (from warnings module): File "/home/pi/デスクトップ/test.py", line 84 global cur, con SyntaxWarning: name 'con' is used prior to global declaration >>> Warning (from warnings module): File "/home/pi/デスクトップ/test.py", line 91 global con SyntaxWarning: name 'con' is used prior to global declaration >>> と出ます。globalをdefの1行下に書いてないからだと思うのですが、1行下に書いてしまうと、データベースを作成していない(con, curを定義していない)状態でglobal con, curとしてしまうため、これはこれでエラーが出てしまいます。 cur, conはグローバル変数にしない方がいいのでしょうか?
person

2020/10/08 03:07

先ほどの私のコメントですが、 data.put(("DB=CNCT", None)) をthread定義の直前に書いたら解消したようです。すみません。
teamikl

2020/10/08 03:14 編集

>cur, conはグローバル変数にしない方がいいのでしょうか? 可能な限り、不要なglobal宣言は避けるべきです。 今回はスレッドも絡んでいて、スレッドを跨いだ変数のアクセスがあります mainスレッドでDBの接続切断、サブスレッドでDBへクエリ発行 では、タイミング次第では切断済みのコネクションに対してのアクセスが 発生する可能性があるので、スレッド側でDBの接続切断をした方が安全になります。 - cnct, discnct は 関数loopの頭と末尾で呼び出すようにします。 - con, cur は関数の戻り値や引数にして受け渡します。
teamikl

2020/10/08 03:18

loopflag が消えましたが、 while が2重になってるので、内側のbreak ではループを抜けられないです。 return でも良いのですが、 ループの後に後始末のコードを書きたい場合があるので while not data.empty(): を省きましょう。ループを1重にできます。 キューにデータがない場合、data.get() はputがあるまで待機します。
person

2020/10/08 04:18 編集

質問文の追記のコードを更新しました。 このような感じでしょうか? 気のせいか書き込む時間が少し遅くなったような気がします。ターミナルでSELECT文を投げたときに反映されるのが修正前と比べて遅いような気がしたので。while内の分岐に時間がかかっているのでしょうか? データの欠落はなさそうなので問題というほどではないですが。
teamikl

2020/10/08 04:53

時間が掛かっているのはtime.sleep(0.1) の影響かな? ping については解りません。 無駄にループを消化して監視するわけではないので、 time.sleep は不要です。data.get が待機してます。 ループ内にprint 文を入れる等して、 ループがどのタイミングで実行されてるか確認して見て下さい。 >質問文の追記のコードを更新しました。 >このような感じでしょうか? ループを抜けた時に後始末のコードが必ず実行されるようにしたいので、 DB接続 while True:  キュー読み出し DB切断 もしくは、コンテキストマネージャを使い、 後始末の処理が保証されるようにします。 具体的なコードは調べてないですが、sqlite3 ではサポートされてるので mysqlのモジュールにも同様の仕組みがあるはず。 with DB接続 as cur:  while True:   キュー読み出し 任意のタイミングでDB接続したい場合は、今のコードでも良いと思いますが、 その場合、DB切断の処理が確実に呼ばれるように気を付ける必要が出てくるので、 将来的に何らかのエラーの原因になる可能性が残ってしまいます。 (現状大丈夫なら問題ありません。)
person

2020/10/08 05:42

前者でやってみます。遅かったのはsleep()のせいでした。get()で高負荷がかからいようであればsleep()は消すことにします。 一応、ご指摘頂いた内容で質問文の方に修正したプログラム載せました。
teamikl

2020/10/08 12:56

>get()で高負荷がかからいようであれば get() 引数なしで呼び出した場合は、ブロッキングモードなので put() があるまでコードの実行はそこで待機されます。ループの負荷はありません。 以前のコードでは while not data.empty() があったため、 get() で待たずに外側のwhile loopflag: のループが回されてました。これは負荷の掛かるループです。 修正した現在のコードでは解消されてます。 ---- 文章で説明が難しくなってきたので、比較用にコード書きました。 リンク先READMEに説明を書いてます。 https://repl.it/@MiKLTea/TestTkQueueThread オフトピになりますが、他に気になった点 CSV出力と、テーブル名のSQLインジェクションについても追記。
teamikl

2020/10/08 22:44

自分の書き込みが矛盾してるように感じたので、補足 >>メッセージを含むデータのみputしていても、そういった状態に陥る可能性があるのでしょうか? > 何もない状態でアプリケーションを終了した場合 > スレッド側のコードは loopflag 判別にいかず、「data.get() で待機されたまま」になってます。 これは、事前の not data.empty() を失念してました。 「他スレッドからキューを読みださなければ」「事前に not data.empty()で確認している為」 data.get() が待機したままスレッドが終われないという事はありません。ですが、 いくつかの条件次第では崩れてしまう潜在的な問題ではあるので、 対策した方が良い事には変わりません。(追記されたコードでは解消済み) マルチスレッドにおいては、not data.empty() で確認した後に、data.get() しても キューの中身が既に空になっている可能性があります。(スレッドとキューの使い方次第) >「 get() で待たずに」外側のwhile loopflag: のループが回されてました。これは負荷の掛かるループです 質問が投稿された時点のコードではこちらの状況です。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問