Чтение данных из csv и сохранение в SQLite / tkinter 13

Скачайте код уроков с GitLab: https://gitlab.com/PythonRu/tkinter-uroki

Необходимые файлы есть в папке «lesson_13» из репозитория по ссылке выше.

Чтение записей из CSV-файла

В качестве первой попытки загрузки данных для чтения в приложение используем файл CSV (comma-separated value, то есть — значения, разделенные запятыми). Этот формат сводит в таблицу данные в обычных текстовых файлах. Каждый файл соответствует полю записи, разделенному запятыми, например, вот так:

Gauford,Albertine,agauford0@acme.com,(614) 7171720
Greger,Bryce,bgreger1@acme.com,(616) 3543513
Wetherald,Rickey,rwetherald2@acme.com,(379) 3652495

Такое решение легко реализовать для простых сценариев, особенно если текстовые поля не содержат разрывов строк. Используем модуль csv из стандартной библиотеки, а после загрузки данных заполним ими виджеты из прошлых материалов.

Соберем все виджеты, созданные ранее. После загрузки записей из CSV-файла приложение будет выглядеть как на следующем скриншоте:

Чтение записей из CSV-файла

Помимо импорта класса Contact также импортируем виджеты ContactForm и ContactList:


import csv
import tkinter as tk

from lesson_12.structuring_data import Contact
from lesson_12.widgets import ContactForm, ContactList

class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Контакты из CSV")
self.list = ContactList(self, height=12)
self.form = ContactForm(self)
self.contacts = self.load_contacts()

for contact in self.contacts:
self.list.insert(contact)
self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(side=tk.LEFT, padx=10, pady=10)
self.list.bind_doble_click(self.show_contact)

def load_contacts(self):
with open("contacts.csv", encoding="utf-8", newline="") as f:
return [Contact(*r) for r in csv.reader(f)]

def show_contact(self, index):
contact = self.contacts[index]
self.form.load_details(contact)

if __name__ == "__main__":
app = App()
app.mainloop()

Как работает загрузка данных из CSV

Функция load_contacts отвечает за чтение файла CSV и трансформацию всех записей в список экземпляров Contact.

Каждая строка, считанная csv.reader, возвращается в виде кортежа строк, созданного с помощью разделения соответствующей строки по запятым. Поскольку кортеж использует тот же порядок, что и параметры в методе __init__ класса Contact, можно запросто распаковать его с помощью оператора *. Весь этот код можно собрать в одну строку с помощью «list comprehension»:


def load_contacts(self):
with open("contacts.csv", encoding="utf-8", newline="") as f:
return [Contact(*r) for r in csv.reader(f)]

Нет никаких проблем с возвратом списка с помощью блока with, поскольку менеджер контекста автоматически закрывает файл, когда метод выполнения заканчивает выполнение.

Сохранение данных в базе данных SQLite

Поскольку есть необходимость сохранять изменения данных в приложении, можно реализовать такое решение, которое будет работать как с записью, так и с чтением.

Можно сохранять все записи в тот же текстовый файл после каждого изменения, но это не очень эффективно, особенно когда речь идет об изменении нескольких отдельных строк.

Поскольку вся информация будет храниться локально, для этих целей можно использовать базу данных SQLite. Модуль sqlite3 — это часть стандартной библиотеки, поэтому для ее использования не нужны дополнительные зависимости.

Конечно, этот подход — не исчерпывающее руководство по SQLite, а лишь практическое введение по тому, как интегрировать базу данных в свое приложение.

Перед использованием базы данных в приложении нужно создать ее и заполнить начальными данными. Все контакты хранятся в файле CSV, поэтому используем скрипт миграции для чтения записей и добавления их в базу.

Сначала создадим соединение с файлом contacts.db, где данные будут храниться. После этого создадим таблицу contacts с текстовыми полями last_name, first_name, email и phone.

Поскольку csv.reader возвращает итерируемый объект, состоящий из кортежей, чьи поля следуют порядку, определенному в CREATE TALE, их можно прямо передать в метод executemany. Это выполнит инструкцию INSERT для каждого кортежа, заменяя вопросительные знаки на реальные значения каждой записи:


import csv
import sqlite3

def main():
with open("contacts.csv", encoding="utf-8", newline="") as f, \
sqlite3.connect("contacts.db") as conn:
conn.execute("""CREATE TABLE contacts (
last_name text,
first_name text,
email text,
phone text
)""")
conn.executemany("INSERT INTO contacts VALUES (?,?,?,?)",
csv.reader(f))

if __name__ == "__main__":
main()

Инструкция with автоматически подтверждает транзакцию и закрывает файл и SQLite-соединение в конце выполнения.

Как работать с базой данных

Для добавления новых контактов в базу данных определим подкласс Toplevel, который будет переиспользовать ContactForm для создания экземпляров новых контактов:


class NewContact(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.contact = None
self.form = ContactForm(self)
self.btn_add = tk.Button(self, text="Подтвердить", command=self.confirm)
self.form.pack(padx=10, pady=10)
self.btn_add.pack(pady=10)

def confirm(self):
self.contact = self.form.get_details()
if self.contact:
self.destroy()

def show(self):
self.grab_set()
self.wait_window()
return self.contact

Следующее окно верхнего уровня будет отображаться поверх основного и вернет фокус после подтверждения и закрытия диалогового:

Как работать с базой данных

Также расширим класс ContactForm двумя дополнительными кнопками: одна будет использоваться для обновления информации, а вторая — для удаления выбранного контакта:


class UpdateContactForm(ContactForm):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self.btn_save = tk.Button(self, text="Сохранить")
self.btn_delete = tk.Button(self, text="Удалить")

self.btn_save.pack(side=tk.RIGHT, ipadx=5, padx=5, pady=5)
self.btn_delete.pack(side=tk.RIGHT, ipadx=5, padx=5, pady=5)

def bind_save(self, callback):
self.btn_save.config(command=callback)

def bind_delete(self, callback):
self.btn_delete.config(command=callback)

Методы bind_save и bind_delete позволят связать функцию обратного вызова с соответствующей кнопкой command.

Для интеграции всех этих изменений добавим соответствующий код в класс App:


class App(tk.Tk):
def __init__(self, conn):
super().__init__()
self.title("Контакты из SQLite")
self.conn = conn
self.selection = None
self.list = ContactList(self, height=15)
self.form = UpdateContactForm(self)
self.btn_new = tk.Button(self, text="Добавить контакт",
command=self.add_contact)
self.contacts = self.load_contacts()

for contact in self.contacts:
self.list.insert(contact)
self.list.pack(side=tk.LEFT, padx=10, pady=10)
self.form.pack(padx=10, pady=10)
self.btn_new.pack(side=tk.BOTTOM, pady=5)

self.list.bind_doble_click(self.show_contact)
self.form.bind_save(self.update_contact)
self.form.bind_delete(self.delete_contact)

Также нужно поменять метод load_contacts для создания контактов из результата запроса:


def load_contacts(self):
contacts = []
sql = "SELECT rowid, last_name, first_name, email, phone FROM contacts"
for row in self.conn.execute(sql):
contact = Contact(*row[1:])
contact.rowid = row[0]
contacts.append(contact)
return contacts

def show_contact(self, index):
self.selection = index
contact = self.contacts[index]
self.form.load_details(contact)

Для добавления контакта в список нужно создавать экземпляр диалога NewContact и вызывать его метод show для получения всех данных. Если значения валидны, то сохраним их в кортеже в том же порядке, что и в инструкции INSERT:


def to_values(self, c):
return (c.last_name, c.first_name, c.email, c.phone)

def add_contact(self):
new_contact = NewContact(self)
contact = new_contact.show()
if not contact:
return
values = self.to_values(contact)
with self.conn as c:
cursor = c.cursor()
cursor.execute("INSERT INTO contacts VALUES (?,?,?,?)", values)
contact.rowid = cursor.lastrowid
self.contacts.append(contact)
self.list.insert(contact)

После выбора контактов их детали можно обновить, получив текущие значения форм. Если они валидны, выполним UPDATE, чтобы задать колонки записей с указанным rowid.

Поскольку поля находятся в том же порядке, что и в INSERT, можем заново использовать метод to_values для создания кортежа из экземпляра контакта. Единственным отличием будет то, что нужно добавить параметр замены для rowid:


def update_contact(self):
if self.selection is None:
return
rowid = self.contacts[self.selection].rowid
contact = self.form.get_details()
if contact:
values = self.to_values(contact)
with self.conn as c:
sql = """UPDATE contacts SET
last_name = ?,
first_name = ?,
email = ?,
phone = ?
WHERE rowid = ?"""
c.execute(sql, values + (rowid,))
contact.rowid = rowid
self.contacts[self.selection] = contact
self.list.update(contact, self.selection)

Для удаления выбранного контакта получаем его rowid и подставляем в DELETE. Когда транзакция подтверждена, контакт удаляется из графического представления: он пропадает из формы и удаляется из списка. Значением атрибута selection становится None, что позволяет не выполнять операции над элементами, которые уже не являются валидными:


def delete_contact(self):
if self.selection is None:
return
rowid = self.contacts[self.selection].rowid
with self.conn as c:
c.execute("DELETE FROM contacts WHERE rowid = ?", (rowid,))
self.form.clear()
self.list.delete(self.selection)
self.selection = None

Наконец, оборачиваем код для создания экземпляра приложения в функцию main:


def main():
with sqlite3.connect("contacts.db") as conn:
app = App(conn)
app.mainloop()

if __name__ == "__main__":
main()

Со всеми этими изменениями приложение будет выглядеть следующим образом:

Как работать с базой данных

Как работают SQL запросы

Такой тип приложения часто называют CRUD (Create, Read, Update Delete — Создание, Чтение, Обновление, Удаление). Эти операции соответствуют SQL-инструкциям, таким как INSERT, SELECT, UPDATE и DELETE.


Теперь посмотрим, как реализовать каждую из операций с помощью класса sqlite3.Connection.

INSERT добавляет новые записи в таблицу, указывая названия колонок и соответствующие значения. Вместо этого можно использовать и порядок колонок.

При создании таблицы в SQLite по умолчанию создается колонка rowid, которой автоматически присваивается уникальное значение для идентификации каждой строки. Поскольку это значение часто требуется для последующих операций, получить его можно с помощью атрибута lastrowid, которое есть у класса Cursor:


sql = "INSERT INTO my_table (col1, col2, col3) VALUES (?, ?, ?)"
with connection:
cursor = connection.cursor()
cursor.execute(sql, (value1, value2, value3))
rowid = cursor.lastrowid

SELECT получает значения одной или нескольких колонок из таблицы. Также можно добавить условие WHERE для фильтрации записей. Это пригодится для реализации поиска и разбиения на страницы, но можно обойтись без этого в простом приложении:


sql = "SELECT rowid, col1, col2, col3 FROM my_table"
for row in connection.execute(sql):
# do something with row

UPDATE обновляет значение одной или нескольких колонок из таблицы. Обычно добавляется WHERE для обновления только тех строк, которые соответствуют определенным критериям — в данном случае можно использовать rowid:


sql = "UPDATE my_table SET col1 = ?, col2 = ?, col3 = ?
WHERE rowid = ?"
with connection:
connection.execute(sql, (value1, value2, value3, rowid))

Наконец, DELETE удаляет одну или несколько записей из таблицы. В данном случае еще важнее использовать WHERE, потому что без условия можно удалить сразу все записи:


sql = "DELETE FROM my_table WHERE rowid = ?"
with connection:
connection.execute(sql, (rowid,))

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript