简介:这份资源是一套基于Python的宠物信息管理系统完整项目资料,面向正在学习Python开发、数据库应用或软件工程实践的初学者与进阶者,可用于课程设计、毕业设计参考或自学练手。压缩包共4个文件,约6MB,包含sql数据库脚本、zip源码包、pptx演示文稿与docx设计文档,分别对应数据建表、系统实现、答辩展示与需求设计说明。系统围绕宠物管理、主人登记、健康记录、疫苗提醒、搜索查询及报表统计等模块展开,采用面向对象方式定义宠物、主人、疫苗等类,并通过sqlite3或PyMySQL完成增删改查操作,界面部分可借助Tkinter、PyQt等库实现。目前已有1391人学习下载,读者可从中获取可运行的源码结构、数据库设计思路、功能模块划分与测试文档,便于快速理解项目架构并迁移到自己的开发实践中。
1. 从一张 Excel 表说起:宠物信息管理系统到底要解决什么
很多人第一次接触「基于 Python 的宠物信息管理系统」,场景都差不多:宠物医院前台有一张越拉越长的 Excel,宠物名字、主人电话、疫苗时间、品种、绝育状态全塞在一起,查一只猫的接种记录要 Ctrl+F 翻半天,改错一行还容易覆盖别人的数据。这个标题要解决的,就是把这张表变成一个能增删改查、能按条件筛选、数据不会互相踩踏的小型系统。它适合两类人:一类是刚学完 Python 基础、想找一个真实项目练手的入门者;另一类是小诊所、宠物店、救助站的兼职技术负责人,想用最低成本把台账管起来。核心词「宠物信息管理系统」听着像课程设计,但真做起来,考的是数据建模、持久化和查询设计,而不是界面花哨。下面按「先想清楚数据长什么样,再动手写,最后避坑」的顺序讲透。
2. 先把数据模型定死:宠物、主人、疫苗三张表怎么切
2.1 为什么不能只用一张大表
新手最容易犯的错,是把所有字段堆进一张 pet 表:宠物名、品种、生日、主人姓名、主人电话、疫苗名称、接种日期、下次接种……字段一多,问题立刻暴露。同一只宠物打三次疫苗,就要写三行,宠物和主人的信息被重复三遍;主人换了电话,得改所有行,漏一行就出现两个号码。这就是典型的没做范式拆分。
合理的切法是三张表:owner(主人)、pet(宠物)、vaccine(疫苗记录)。owner 和 pet 是一对多,一个主人可以养多只宠物;pet 和 vaccine 也是一对多,一只宠物有多条接种记录。这样主人电话只存一份,宠物信息只存一份,疫苗记录通过外键挂上去。查询时用 JOIN 拼回来,改数据时只改一处。
选 SQLite 而不是 MySQL,是因为这个量级的系统(几千条记录)用单文件数据库足够,零配置、随项目走、备份就是复制一个 .db 文件。等数据量真上来了再迁 MySQL,SQL 语句几乎不用改。
2.2 建表语句与字段类型选择
import sqlite3 # 连接数据库,文件不存在会自动创建 conn = sqlite3.connect("pet_system.db") cursor = conn.cursor() # 主人表:一个主人对应多个宠物 cursor.execute(""" CREATE TABLE IF NOT EXISTS owner ( owner_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT NOT NULL UNIQUE, -- 电话唯一,防止重复录入 address TEXT ) """) # 宠物表:通过 owner_id 外键关联主人 cursor.execute(""" CREATE TABLE IF NOT EXISTS pet ( pet_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, species TEXT NOT NULL, -- 猫/狗/兔等 breed TEXT, birth_date TEXT, -- 存 ISO 格式 'YYYY-MM-DD' gender TEXT CHECK(gender IN ('公','母','未知')), owner_id INTEGER NOT NULL, FOREIGN KEY (owner_id) REFERENCES owner(owner_id) ) """) # 疫苗表:通过 pet_id 外键关联宠物 cursor.execute(""" CREATE TABLE IF NOT EXISTS vaccine ( vac_id INTEGER PRIMARY KEY AUTOINCREMENT, pet_id INTEGER NOT NULL, vac_name TEXT NOT NULL, vac_date TEXT NOT NULL, next_date TEXT, -- 下次接种提醒日期 FOREIGN KEY (pet_id) REFERENCES pet(pet_id) ) """) conn.commit()这段代码的逻辑是:先建主人表,再建宠物表并声明外键指向主人,最后建疫苗表指向宠物。参数上,AUTOINCREMENT让主键自增,不用手动维护 ID;UNIQUE加在 phone 上,从数据库层面挡住重复主人;CHECK约束把性别限定在三个值,避免有人填「男」「female」这种脏数据。日期统一用 TEXT 存 ISO 格式,是因为 SQLite 没有原生日期类型,ISO 字符串可以直接用字符串比较做范围查询,比如WHERE vac_date > '2025-01-01',排序也不会乱。
提示:外键约束在 SQLite 里默认是关闭的,每次连接后要执行
cursor.execute("PRAGMA foreign_keys = ON"),否则删主人时不会拦住还挂着的宠物。
2.3 用 Python 定义函数封装增删改查
热词里「python定义函数」出现频率很高,这里正好用上。把每个操作封成函数,主流程只调用函数,代码才管得住。
def add_owner(name, phone, address=""): """新增主人,电话重复时抛异常""" try: cursor.execute( "INSERT INTO owner (name, phone, address) VALUES (?, ?, ?)", (name, phone, address) ) conn.commit() return cursor.lastrowid # 返回新主人的 ID except sqlite3.IntegrityError: print(f"电话 {phone} 已存在,请勿重复录入") return None def add_pet(name, species, owner_id, breed="", birth_date="", gender="未知"): """新增宠物,owner_id 必须已存在""" cursor.execute( "INSERT INTO pet (name, species, breed, birth_date, gender, owner_id) " "VALUES (?, ?, ?, ?, ?, ?)", (name, species, breed, birth_date, gender, owner_id) ) conn.commit() return cursor.lastrowid def find_pets_by_owner(phone): """按主人电话查他名下所有宠物""" cursor.execute(""" SELECT p.pet_id, p.name, p.species, p.breed FROM pet p JOIN owner o ON p.owner_id = o.owner_id WHERE o.phone = ? """, (phone,)) return cursor.fetchall()三个函数的职责很清楚:add_owner负责写主人并处理电话冲突,add_pet负责写宠物,find_pets_by_owner负责联表查询。参数全部用?占位符传,这是防 SQL 注入的关键——千万不要用 f-string 把用户输入拼进 SQL,否则主人名字里带个引号就能把语句搞崩。cursor.lastrowid拿到刚插入行的自增 ID,方便紧接着给这只宠物加疫苗记录。
3. 查询与筛选:把「找一只猫」做成能用的功能
3.1 多条件组合查询的写法
真实使用中,前台不会只按一个条件查。常见组合是「品种是猫 + 主人电话尾号 + 最近三个月打过疫苗」。硬写死 SQL 不现实,得动态拼条件。
def search_pets(species=None, keyword=None, vaccinated_after=None): """按品种、关键字(宠物名或主人名)、接种时间组合筛选""" sql = """ SELECT p.name, p.species, p.breed, o.name AS owner_name, o.phone FROM pet p JOIN owner o ON p.owner_id = o.owner_id WHERE 1=1 """ params = [] if species: sql += " AND p.species = ?" params.append(species) if keyword: sql += " AND (p.name LIKE ? OR o.name LIKE ?)" params.append(f"%{keyword}%") params.append(f"%{keyword}%") if vaccinated_after: sql += """ AND p.pet_id IN ( SELECT pet_id FROM vaccine WHERE vac_date >= ? )""" params.append(vaccinated_after) cursor.execute(sql, params) return cursor.fetchall()这里用WHERE 1=1起头,是为了后面每个条件都能无脑接AND,不用判断是不是第一个条件。LIKE配合%关键字%做模糊匹配,宠物名和主人名任一命中都算。接种时间用子查询IN过滤,比 JOIN 再 DISTINCT 更直观。参数列表和 SQL 里的?顺序必须严格对应,这是最容易翻车的地方——加了一个条件忘了 append 参数,运行时就报参数数量不匹配。
3.2 用 pandas 做统计报表
热词里「python dataframe」「python写入excel」指向一个真实需求:月底要出一张「各品种宠物数量」「本月新增接种数」的报表。查询结果直接丢给 pandas,几行就能出表。
import pandas as pd def export_species_report(output="species_report.xlsx"): """统计各品种数量并导出 Excel""" df = pd.read_sql_query(""" SELECT species AS 品种, COUNT(*) AS 数量 FROM pet GROUP BY species ORDER BY 数量 DESC """, conn) # 追加一行合计 total = pd.DataFrame([{"品种": "合计", "数量": df["数量"].sum()}]) df = pd.concat([df, total], ignore_index=True) df.to_excel(output, index=False) print(f"报表已生成:{output}") return dfpd.read_sql_query直接把 SQL 结果转成 DataFrame,列名用 SQL 里的别名,导出时表头就是中文。GROUP BY species做分组计数,ORDER BY 数量 DESC让多的排前面。to_excel需要 openpyxl 支持,没装的话先pip install openpyxl。index=False是为了不把 pandas 的行号写进 Excel 第一列,否则表头会错位。
注意:
read_sql_query传的是连接对象 conn,如果前面 commit 后连接被关过,这里会报错,保持连接常开或用上下文管理器重新打开。
3.3 疫苗到期提醒怎么算
系统真正有价值的地方,是能主动告诉你「哪只宠物该打疫苗了」。核心是拿next_date和今天比。
from datetime import date, timedelta def get_due_vaccines(days_ahead=7): """查出未来 N 天内需要接种的宠物""" today = date.today().isoformat() deadline = (date.today() + timedelta(days=days_ahead)).isoformat() cursor.execute(""" SELECT p.name, o.name, o.phone, v.vac_name, v.next_date FROM vaccine v JOIN pet p ON v.pet_id = p.pet_id JOIN owner o ON p.owner_id = o.owner_id WHERE v.next_date BETWEEN ? AND ? ORDER BY v.next_date """, (today, deadline)) return cursor.fetchall()date.today().isoformat()得到'2025-06-01'这种字符串,和数据库里存的格式一致,才能正确比较。timedelta(days=7)算出七天后的日期,BETWEEN取闭区间。返回结果按next_date升序,最急的排最前。这个函数可以挂到每天定时任务里,把结果发到前台。
4. 避坑与排查:那些让系统当场翻车的细节
4.1 中文乱码:现象是存进去变问号
现象:主人名字「张伟」存进数据库,查出来变成??或乱码。原因通常是连接时没指定编码,或者终端本身编码不是 UTF-8。解决:SQLite 默认就是 UTF-8,问题多出在读写文件或打印环节。Python 3 的字符串本身是 Unicode,写库不会乱;但如果用open()读 CSV 导入,要显式写encoding="utf-8"。Windows 命令行打印中文乱码,是终端代码页问题,chcp 65001切到 UTF-8 即可,跟数据库无关。
4.2 外键不生效:删了主人宠物还在
现象:删掉一个主人,他名下的宠物记录还挂在库里,成了孤儿数据。原因:SQLite 外键约束默认关闭,建表时写了FOREIGN KEY也没用。解决:每次建立连接后立刻执行PRAGMA foreign_keys = ON,并且建表时给外键加上ON DELETE CASCADE,这样删主人时宠物自动跟着删。但要注意,级联删除是双刃剑,删之前最好先查一遍名下有几只宠物,让用户确认。
4.3 日期比较失效:字符串格式不统一
现象:WHERE vac_date > '2025-1-1'查不到 2025 年 1 月的数据。原因:数据库里存的是'2025-01-01'(补零),查询条件写的是'2025-1-1'(没补零),字符串比较逐字符比,'01'和'1'第一位就不同。解决:所有日期入库前统一用isoformat()或strftime('%Y-%m-%d')格式化,查询条件也走同一个函数,绝不手写日期字符串。
4.4 参数数量不匹配:动态 SQL 的经典翻车
现象:调用search_pets时报Incorrect number of bindings supplied。原因:SQL 里加了新条件?,但忘了往 params 里 append 对应值,或者 append 顺序和?出现顺序不一致。解决:养成习惯,每写一个?就紧跟一行 append,条件块和参数块挨着写。调试时先把拼好的 sql 和 params 打印出来,肉眼对一遍再执行。
4.5 并发写入锁库:多个人同时点保存
现象:两个人同时录入,其中一个报database is locked。原因:SQLite 同一时刻只允许一个写操作,另一个会被阻塞,超时后抛错。解决:小团队场景下,给连接设置timeout,sqlite3.connect("pet_system.db", timeout=10),让第二个写等 10 秒而不是立刻失败。如果并发真的高,就该换 PostgreSQL 或 MySQL 了,SQLite 不适合多写场景。
5. 进阶:把系统从「能跑」推到「敢用」
前面四章把功能跑通了,但离「敢交给前台天天用」还差几步。第一是加一层输入校验,别指望用户自觉。宠物生日字段,用户可能填「去年」「2025.1.1」「20250101」,全得在入库前用datetime.strptime试解析,解析失败就提示重填,而不是把脏数据塞进库。第二是给关键操作加事务,比如「新增宠物 + 同时加一条疫苗记录」应该是一个原子操作,中途失败要整体回滚,否则会出现有宠物没疫苗的半截数据。
def add_pet_with_vaccine(pet_info, vac_info): """宠物和首针疫苗一起写入,任一失败则回滚""" try: cursor.execute("BEGIN") pet_id = add_pet(**pet_info) cursor.execute( "INSERT INTO vaccine (pet_id, vac_name, vac_date, next_date) " "VALUES (?, ?, ?, ?)", (pet_id, vac_info["name"], vac_info["date"], vac_info["next"]) ) conn.commit() return pet_id except Exception as e: conn.rollback() print(f"写入失败已回滚:{e}") return NoneBEGIN显式开启事务,两步都成功才commit,任何一步抛异常就rollback,数据库回到操作前的状态。这是「后悔药」,没有它,一次失败就可能留下查不到主人的孤儿宠物。
第三是备份习惯。SQLite 的备份简单到可以写进定时任务:sqlite3 pet_system.db ".backup backup_$(date +%F).db",每天一份,出问题直接换文件。别小看这一步,我见过太多人系统写得漂亮,硬盘一坏全没了。
验证系统是否真的可用,我一般做三件事:一是造 500 条假数据压一遍查询,看响应是否还在毫秒级;二是故意输错数据,看校验拦不拦得住;三是模拟两个人同时录入,看锁库报错有没有被 timeout 兜住。这三关过了,才敢让前台用。
最后说个我自己的教训:早期我图省事,把主人电话当主键用,结果一个主人换号,所有关联全断。后来老老实实加自增 ID 做代理键,业务字段只做唯一约束,再没出过这类问题。做管理系统,主键永远用无意义的自增 ID,别拿业务字段当主键,这条习惯帮我省了无数次返工。希望帮到你。
本文还有配套的精品资源,点击获取