ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python通讯录系统实战:从零实现CRUD与JSON数据持久化

Python通讯录系统实战:从零实现CRUD与JSON数据持久化 简介本资源是一个基于Python开发的轻量级桌面通讯录系统实现方案面向计算机、软件工程及通信工程等专业的本科生课程设计与毕业设计实践需求解决联系人信息管理、增删改查、数据持久化与基础UI交互等典型应用场景。压缩包共10个文件70KB包含4个核心Python源码含主界面TableUi.py、信息弹窗InfoDlg.py及数据库操作模块、2个Qt Designer设计的.ui界面文件、1个SQLite数据库peopleinfo.db、1个图标资源ico/png、1个qrc资源编译文件及1个预编译的资源模块完整覆盖GUI构建、数据存储与业务逻辑三层结构。已有221人学习下载提供可直接运行的完整项目骨架含清晰目录组织、本地数据库集成、表单验证逻辑与基础CRUD功能实现便于学生快速理解PythonPyQt5桌面应用开发全流程并在此基础上拓展搜索、分组、导入导出等进阶功能。1. 项目概述与核心价值最近在整理旧项目时翻出来一个几年前写的通讯录系统用Python实现的打包成了一个zip文件。这个项目虽然看起来简单就是个“增删改查”的典型应用但我觉得它特别适合用来理解Python如何从零开始构建一个完整的、带数据持久化功能的小型桌面应用。无论是刚学完Python基础语法想找个练手项目巩固一下的新手还是已经工作、想快速重温一下文件操作和面向对象设计的老手这个项目都能提供一条清晰的实践路径。它不依赖任何复杂的数据库比如MySQL或MongoDB而是直接使用Python内置的json或pickle模块操作文件来存储数据这让整个项目的依赖极简环境配置几乎零成本。通过这个项目你能把散落的知识点——类与对象、列表字典操作、文件读写、异常处理、用户交互逻辑——像串珠子一样串联起来形成一个可运行、有实际功能的作品。接下来我就把这个“通讯录系统.zip”拆开从设计思路到每一行关键代码再到我踩过的坑和优化技巧毫无保留地分享给你。2. 系统整体设计与架构解析2.1 为什么选择纯Python与文件存储很多教程一上来就教用数据库但对于一个通讯录这样的轻量级应用杀鸡焉用牛刀。我选择纯Python文件存储的方案主要基于以下几点考量极简部署与零依赖用户只需要有Python环境3.6以上版本均可解压即用。不需要安装额外的数据库服务如MySQL Server或配置连接避免了环境差异带来的各种“玄学”问题。这对于教学演示、个人工具或作为大项目中的一个独立模块来说非常友好。聚焦核心逻辑剥离了数据库连接、SQL语句编写等外围知识我们可以把全部精力集中在业务逻辑本身如何设计联系人数据结构、如何组织代码模块、如何实现用户交互。这有助于初学者建立清晰的编程思维而不是被复杂的配置劝退。理解数据持久化的本质无论是用json还是pickle本质都是将内存中的数据结构列表、字典序列化成字节流写入硬盘文件。这个过程让你直观地理解什么是“持久化”以及程序关闭后数据如何得以保存。这是理解更高级数据存储技术如数据库的基石。当然这个方案也有其局限性比如不适合海量数据性能问题和高并发访问文件锁问题。但对于一个学习型项目或个人使用的通讯录这些都不是问题。2.2 核心模块与功能规划一个完整的通讯录系统至少需要包含以下几个核心功能模块我称之为“CRUDL”即创建、读取、更新、删除和列表展示联系人模型 (Contact Model)定义一个Contact类用来表示一个联系人的所有信息。通常包括姓名、手机号还可以扩展邮箱、地址、分组等字段。这个类是系统的数据基石。通讯录管理核心 (AddressBook Core)定义一个AddressBook类它内部维护一个联系人列表通常是Contact对象的列表并对外提供所有操作方法如添加联系人、删除联系人、查找联系人、修改联系人信息、显示所有联系人等。这个类是系统的业务逻辑核心。数据持久化层 (Persistence Layer)负责将AddressBook管理的数据保存到文件以及从文件加载数据到内存。这里会用到json模块人类可读易调试或pickle模块支持Python所有对象但二进制格式。用户交互界面 (User Interface)提供一个命令行菜单让用户可以通过输入数字或字母来选择要执行的操作。这是系统与用户直接打交道的部分。整个系统的数据流非常清晰用户通过UI发出指令 -AddressBook类执行业务逻辑操作内存中的联系人列表- 数据持久化层在适当的时候如退出时将数据保存到文件或启动时从文件加载数据。2.3 技术选型json vs pickle在数据持久化方案上json和pickle是最常见的两个选择。我的项目里最终选择了json原因如下特性json模块pickle模块本项目选择理由可读性优秀。保存为纯文本可直接用记事本打开查看、编辑。差。保存为二进制格式人类无法直接阅读。通讯录数据量小可读性利于调试和手动应急修改。安全性相对安全。只能序列化基本数据类型dict, list, str, int, float, bool, None。有风险。可以序列化几乎任何Python对象反序列化时可能执行任意代码。对于来源不明的数据文件pickle存在安全漏洞。我们的数据是自生成的风险低但json更规范。跨语言支持。JSON是通用数据交换格式几乎所有编程语言都支持。仅限Python。其他语言无法直接读取.pkl文件。虽然本项目不涉及跨语言但使用通用格式是更好的实践。序列化范围有限。无法直接序列化自定义类的实例对象。广泛。可以直接序列化自定义类的实例对象。我们需要将Contact对象列表保存json无法直接处理。这是关键难点也是需要额外处理的地方。注意json不能直接序列化Contact对象但我们可以通过将对象转换为字典__dict__属性或自定义to_dict()方法来解决。反序列化时再从字典还原为对象。这个过程虽然多了一步但使得数据格式清晰、可控是更推荐的做法。3. 核心代码实现与逐行解析3.1 联系人模型 (Contact Class) 的实现这是整个系统的数据单元。我们不仅要定义数据字段还要考虑如何打印它、如何比较两个联系人是否相同例如根据手机号判断以及如何将其转换为可被json序列化的字典。# contact.py class Contact: 联系人类定义单个联系人的属性和行为 def __init__(self, name, phone, email, address, group默认): 初始化联系人 :param name: 姓名 (必填) :param phone: 手机号 (必填) :param email: 电子邮箱 (可选) :param address: 地址 (可选) :param group: 分组 (可选默认为‘默认’) self.name name self.phone phone self.email email self.address address self.group group def __str__(self): 定义打印联系人时的格式方便阅读 return f姓名: {self.name}, 电话: {self.phone}, 邮箱: {self.email}, 地址: {self.address}, 分组: {self.group} def __repr__(self): 定义在解释器中直接显示对象时的格式通常用于调试 return fContact(name{self.name}, phone{self.phone}) def to_dict(self): 将Contact对象转换为字典用于json序列化 return { name: self.name, phone: self.phone, email: self.email, address: self.address, group: self.group } classmethod def from_dict(cls, data): 从字典创建Contact对象用于json反序列化 # 这里使用字典的get方法可以避免因字典缺少某个键而报错 return cls( namedata.get(name, ), phonedata.get(phone, ), emaildata.get(email, ), addressdata.get(address, ), groupdata.get(group, 默认) ) # 可选实现__eq__方法方便根据手机号判断两个联系人是否相同 def __eq__(self, other): if isinstance(other, Contact): return self.phone other.phone return False关键点解析与避坑指南__init__方法中的默认参数email、address、group都设置了默认值。这样在创建联系人时如果只提供name和phone其他字段会自动填充为默认值使API更友好。__str__与__repr__的区别__str__是给用户看的如print(contact)格式友好__repr__是给开发者看的如在交互式环境中直接输入变量名应包含足够的信息以重新创建该对象。两者都实现是好习惯。to_dict和from_dict类方法这是解决json无法序列化自定义对象的关键。to_dict将对象属性打包成字典from_dict是一个类方法用classmethod装饰它不依赖于某个实例而是像工厂一样根据传入的字典数据创建一个新的Contact对象。这种设计模式非常清晰。使用data.get(key, default)在from_dict中我们使用get方法从字典取值。这比直接用data[key]更安全因为如果旧的保存文件格式中缺少某个键比如后来新增了group字段程序不会崩溃而是使用默认值。3.2 通讯录管理核心 (AddressBook Class) 的实现这个类是业务逻辑的集大成者它内部管理着一个联系人列表并提供各种操作方法。# address_book.py import json from contact import Contact class AddressBook: 通讯录管理类核心业务逻辑 def __init__(self, filenamecontacts.json): 初始化通讯录 :param filename: 数据存储的文件名 self.filename filename self.contacts [] # 用于在内存中存储所有Contact对象的列表 self.load_from_file() # 初始化时自动从文件加载数据 def add_contact(self, contact): 添加一个新联系人 # 添加前可以做一些校验比如手机号是否已存在 if contact in self.contacts: print(f警告手机号 {contact.phone} 已存在添加失败。) return False self.contacts.append(contact) print(f联系人 {contact.name} 添加成功。) self.save_to_file() # 每次修改后自动保存也可改为手动触发 return True def delete_contact(self, phone): 根据手机号删除联系人 for i, contact in enumerate(self.contacts): if contact.phone phone: deleted_contact self.contacts.pop(i) print(f联系人 {deleted_contact.name} 删除成功。) self.save_to_file() return True print(f未找到手机号为 {phone} 的联系人。) return False def find_contact(self, keyword): 根据姓名或手机号查找联系人支持模糊搜索 results [] keyword_lower keyword.lower() for contact in self.contacts: # 搜索姓名或手机号中包含关键词的联系人不区分大小写 if (keyword_lower in contact.name.lower()) or (keyword in contact.phone): results.append(contact) return results def update_contact(self, phone, **kwargs): 根据手机号更新联系人信息kwargs为要更新的字段字典 for contact in self.contacts: if contact.phone phone: # 遍历传入的更新字段 for key, value in kwargs.items(): if hasattr(contact, key): # 检查联系人是否有该属性 setattr(contact, key, value) # 动态设置属性值 else: print(f警告联系人没有属性 {key}已忽略。) print(f联系人 {contact.name} 信息更新成功。) self.save_to_file() return True print(f未找到手机号为 {phone} 的联系人更新失败。) return False def display_all(self, group_filterNone): 显示所有联系人可按分组过滤 filtered_contacts self.contacts if group_filter: filtered_contacts [c for c in self.contacts if c.group group_filter] if not filtered_contacts: print(通讯录为空或没有符合条件的联系人。) return print(f\n 通讯录列表 ({所有分组 if not group_filter else 分组: group_filter}) ) for idx, contact in enumerate(filtered_contacts, 1): print(f{idx}. {contact}) print( * 50) def save_to_file(self): 将通讯录保存到JSON文件 try: # 将所有Contact对象转换为字典列表 data [contact.to_dict() for contact in self.contacts] with open(self.filename, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent4) # indent使JSON文件格式化便于阅读 # print(数据已保存。) # 可注释掉避免每次保存都输出 except IOError as e: print(f保存文件时出错: {e}) def load_from_file(self): 从JSON文件加载通讯录 try: with open(self.filename, r, encodingutf-8) as f: data json.load(f) # 将字典列表转换回Contact对象列表 self.contacts [Contact.from_dict(item) for item in data] print(f已从 {self.filename} 加载 {len(self.contacts)} 个联系人。) except FileNotFoundError: # 如果文件不存在则创建一个空列表这是第一次运行 print(未找到数据文件将创建新的通讯录。) self.contacts [] except json.JSONDecodeError: # 如果文件内容不是有效的JSON可能是文件损坏 print(警告数据文件格式错误将初始化空通讯录。) self.contacts []关键点解析与避坑指南自动加载与保存在__init__中调用load_from_file()使得AddressBook对象一旦创建就自动载入历史数据。在add_contact,delete_contact,update_contact等方法中调用save_to_file()实现了数据的实时持久化。你也可以设计一个“手动保存”模式但自动保存对用户更友好。查找与删除的逻辑delete_contact和update_contact都依赖于遍历查找手机号。这里使用enumerate来同时获取索引和元素找到后通过索引pop删除效率更高O(n)查找O(1)删除。如果联系人非常多可以考虑用字典以手机号为键来存储实现O(1)的查找和删除。模糊搜索的实现find_contact方法将关键词和姓名都转换为小写进行匹配实现了不区分大小写的模糊搜索。这是一个提升用户体验的小细节。灵活的更新方法update_contact使用了**kwargs关键字参数来接收要更新的字段。hasattr和setattr这两个内置函数允许我们动态地检查和设置对象的属性使得这个方法非常通用无需为每个字段单独写更新逻辑。健壮的文件操作save_to_file中ensure_asciiFalse保证了中文等非ASCII字符能正确保存indent4让生成的JSON文件有缩进易于阅读。load_from_file中处理了FileNotFoundError首次运行和json.JSONDecodeError文件损坏两种异常。这是非常重要的错误处理如果没有这些处理程序遇到损坏的文件就会直接崩溃用户体验极差。我们的处理方式是降级初始化一个空列表让程序可以继续运行。3.3 用户交互界面 (CLI Menu) 的实现这是面向用户的终端界面提供一个循环菜单根据用户输入调用AddressBook的相应方法。# main.py from address_book import AddressBook from contact import Contact def display_menu(): 显示主菜单 print(\n *30) print( 通讯录管理系统) print(*30) print( 1. 添加联系人) print( 2. 删除联系人) print( 3. 查找联系人) print( 4. 修改联系人) print( 5. 显示所有联系人) print( 6. 按分组显示) print( 7. 退出系统) print(*30) def get_contact_info_from_input(): 从用户输入收集联系人信息 name input(请输入姓名: ).strip() while not name: # 简单的非空校验 name input(姓名不能为空请重新输入: ).strip() phone input(请输入手机号: ).strip() # 这里可以添加更复杂的手机号格式校验正则表达式 while not phone: phone input(手机号不能为空请重新输入: ).strip() email input(请输入邮箱 (可选直接回车跳过): ).strip() address input(请输入地址 (可选直接回车跳过): ).strip() group input(请输入分组 (可选默认为‘默认’): ).strip() if not group: group 默认 return name, phone, email, address, group def main(): 主程序入口 book AddressBook() # 初始化通讯录会自动加载数据 while True: display_menu() choice input(请选择操作 (1-7): ).strip() if choice 1: # 添加联系人 name, phone, email, address, group get_contact_info_from_input() new_contact Contact(name, phone, email, address, group) book.add_contact(new_contact) elif choice 2: # 删除联系人 phone input(请输入要删除联系人的手机号: ).strip() if phone: # 确认输入非空 confirm input(f确定要删除手机号为 {phone} 的联系人吗(y/n): ).strip().lower() if confirm y: book.delete_contact(phone) else: print(输入不能为空。) elif choice 3: # 查找联系人 keyword input(请输入要查找的姓名或手机号支持模糊搜索: ).strip() if keyword: results book.find_contact(keyword) if results: print(f\n找到 {len(results)} 个结果:) for contact in results: print(f - {contact}) else: print(未找到匹配的联系人。) else: print(搜索关键词不能为空。) elif choice 4: # 修改联系人 phone input(请输入要修改联系人的手机号: ).strip() if not phone: print(手机号不能为空。) continue # 先查找是否存在 results book.find_contact(phone) if not results: print(f未找到手机号为 {phone} 的联系人。) continue print(f\n当前信息: {results[0]}) print(\n请输入要修改的字段和新值留空表示不修改:) updates {} name input(f姓名 [{results[0].name}]: ).strip() if name: updates[name] name # 注意手机号作为唯一标识通常不允许修改。若要修改需先删除旧记录再添加新记录。 # 这里我们选择不允许修改手机号。 # phone_new input(f手机号 [{results[0].phone}]: ).strip() # if phone_new: # updates[phone] phone_new email input(f邮箱 [{results[0].email}]: ).strip() if email: updates[email] email address input(f地址 [{results[0].address}]: ).strip() if address: updates[address] address group input(f分组 [{results[0].group}]: ).strip() if group: updates[group] group if updates: book.update_contact(phone, **updates) else: print(未作任何修改。) elif choice 5: # 显示所有联系人 book.display_all() elif choice 6: # 按分组显示 group_name input(请输入要筛选的分组名 (直接回车显示所有): ).strip() book.display_all(group_filtergroup_name if group_name else None) elif choice 7: # 退出系统 print(感谢使用通讯录管理系统再见) # AddressBook采用自动保存此处无需手动保存 break else: print(无效的选择请输入 1 到 7 之间的数字。) input(\n按回车键继续...) # 暂停让用户看清结果 if __name__ __main__: main()关键点解析与避坑指南输入校验与用户体验在get_contact_info_from_input中对姓名和手机号做了简单的非空校验。在实际项目中应该用正则表达式对手机号、邮箱格式做更严格的校验。对于修改操作提供了当前值作为提示[当前值]留空则保持原样这是非常人性化的设计。关键操作确认对于删除这种危险操作增加了二次确认y/n环节防止误操作。手机号作为唯一标识在修改功能中我刻意禁止了修改手机号。因为在这个简单设计中手机号是查找和识别联系人的关键字段。如果允许修改逻辑会变得复杂需要先按旧手机号找到记录删除再用新手机号创建一条新记录。在更复杂的设计中可以为每个联系人分配一个唯一的ID如UUID这样手机号就可以像其他字段一样随意修改了。if __name__ __main__:的作用这是一个Python的惯用法。它保证了当你直接运行main.py时main()函数会被执行而当main.py被其他文件作为模块导入时main()不会自动执行。这对于代码的复用和测试非常重要。清晰的交互流程每个操作后使用input(\n按回车键继续...)暂停让用户有时间阅读输出信息然后清屏通过菜单重绘继续避免了信息一闪而过的问题。4. 项目运行、打包与扩展思路4.1 如何运行这个项目环境准备确保你的电脑安装了Python3.6或以上版本。可以在命令行输入python --version或python3 --version检查。解压与运行将“通讯录系统.zip”解压到一个文件夹。你会看到contact.py,address_book.py,main.py三个核心文件。在命令行中进入该文件夹直接运行python main.py即可启动系统。首次运行首次运行会提示“未找到数据文件将创建新的通讯录。”并在当前目录生成一个空的contacts.json文件。之后的所有操作数据都会自动保存到这个文件中。4.2 从源码到可执行文件使用PyInstaller打包如果你想把这个Python脚本分享给没有安装Python的朋友可以将其打包成一个独立的.exeWindows或可执行文件macOS/Linux。这里推荐使用PyInstaller。安装PyInstaller在命令行中运行pip install pyinstaller。执行打包在项目根目录main.py所在目录下运行pyinstaller -F -w -i icon.ico main.py-F: 打包成单个可执行文件。-w: 运行时不显示命令行窗口对于GUI程序常用我们这是控制台程序可加可不加。如果加了打印信息会看不到调试时建议去掉。-i icon.ico: 为可执行文件指定一个图标需要准备一个.ico格式的图标文件。最简单的打包命令是pyinstaller -F main.py。获取成品打包完成后在项目目录下会生成一个dist文件夹里面的main.exeWindows就是打包好的程序。你可以把这个.exe文件和contacts.json数据文件一起发给别人使用。注意PyInstaller打包时有时会因为动态导入或路径问题导致打包后的程序找不到模块。如果遇到问题可以尝试在main.py开头显式导入所有依赖模块虽然我们已经做了或者使用--hidden-import参数。对于我们的简单项目通常直接打包就能成功。4.3 功能扩展与优化方向这个基础版本已经可用但还有很多可以打磨和扩展的地方这也是你深入学习的好机会数据校验强化使用re模块正则表达式对手机号、邮箱进行格式校验。添加姓名长度限制、去空格处理等。搜索功能增强支持多条件组合搜索如同时按姓名和分组查找。实现更高效的搜索例如预先建立姓名的拼音索引支持拼音首字母搜索。数据备份与恢复增加“导出通讯录”功能可以将数据另存为CSV或Excel格式方便用其他软件打开。增加“导入通讯录”功能从CSV文件批量导入联系人。图形化界面 (GUI)使用tkinterPython标准库、PyQt5或Kivy等库为通讯录开发一个图形窗口界面用户体验会大幅提升。网络化与多端同步将数据存储从本地文件改为网络数据库如SQLite配合Web API或直接使用云数据库服务。开发一个简单的Flask或FastAPI后端配合前端网页或手机App实现多端数据同步。这就能从一个本地工具升级为一个真正的“系统”。5. 常见问题与排查技巧实录在实际编写和运行这个项目的过程中你可能会遇到以下问题。这里我把自己踩过的坑和解决方法记录下来Q1: 运行程序后添加了联系人但关闭程序再打开数据没了排查首先检查程序所在目录下是否生成了contacts.json文件。如果没有说明保存失败。可能原因与解决文件写入权限问题确保程序对当前目录有写权限。可以尝试以管理员身份运行或换一个目录。代码中的保存逻辑未执行检查add_contact、delete_contact等方法中是否调用了self.save_to_file()。确保在__init__中正确调用了load_from_file()。异常导致保存中断在save_to_file方法中如果json.dump之前发生错误如对象无法序列化可能导致保存失败。确保所有要保存的对象都有to_dict方法。可以在save_to_file里加更详细的try...except打印错误信息。Q2: 错误提示ModuleNotFoundError: No module named contact排查这通常是因为Python找不到你写的模块。解决确保contact.py、address_book.py、main.py三个文件在同一个目录下。确保你是在包含这三个文件的目录下运行的python main.py。如果文件结构更复杂比如有子文件夹需要在子文件夹中创建__init__.py文件可以是空的并使用相对导入如from .contact import Contact。Q3: JSON文件里中文显示为Unicode码如\u4e2d\u6587原因json.dump()时没有设置ensure_asciiFalse。解决在address_book.py的save_to_file方法中确保json.dump(data, f, ensure_asciiFalse, indent4)。Q4: 想修改手机号但程序里不允许怎么改临时方案直接打开contacts.json文件找到对应的联系人记录手动修改phone字段的值。这就是使用JSON格式存储的另一个好处——可手动编辑。永久方案修改update_contact方法的逻辑。这需要分两步1) 用旧手机号找到联系人获取其全部信息2) 用新手机号创建一个新的Contact对象其他信息不变删除旧对象添加新对象。注意要处理好手机号唯一性的校验。Q5: 联系人很多时查找和删除感觉有点慢。原因当前实现是线性查找遍历列表时间复杂度是O(n)。当数据量上千时性能瓶颈会显现。优化思路使用字典dict作为主要存储结构以手机号或一个自增ID作为键Contact对象作为值。这样查找和删除的时间复杂度可以降到O(1)。代价是显示所有联系人时需要遍历字典的值和保持插入顺序Python 3.7的dict已保持插入顺序可能略有不同但通常利大于弊。你可以尝试重构AddressBook类将self.contacts []改为self.contacts {}并相应修改所有方法。这是一个非常好的进阶练习。本文还有配套的精品资源点击获取
返回列表