电子词典阶段性小项目
电子词典阶段性项目
项目需求介绍
登录注册
用户可以登录和注册
用户名 密码
用户名不能重复
用户信息可以长期保存
网络连接
程序分为服务端,客户端
服务端负责数据处理
启动服务端后应满足多个客户端同时操作
图形二级界面
通过基本的图形界面 print 提示客户端输入
启动后进入一级界面
一级界面功能
登录 注册 退出
登陆: 成功进入二级界面
注册: 成功返回一级界面,或者直接使用注册用户进入二级界面
退出: 退出软件
二级界面功能
查单词 历史记录 注销
查单词: 循环输入单词.得到单词解释,输入特殊符号退出查询状态
直接使用单词本查询.(文本操作)
现将单词本存入数据库查询.通过数据库查询
历史记录: 查询当前用户的查词记录 要求记录包含 name word time可以查看所有记录,或者只显示前10条
注销: 返回上级页面
单词库
特点
文本形式
每个单词占一行
单词按照从小到大顺序排列
单词和解释之间一定有空格
形式如下的一个 e_dict.txt 文件
a indef art one abacus n.frame with beads that slide along parallel rods, used for teaching numbers to children, and (in some countries) for counting abandon v. go away from (a person or thing or place) not intending to return; forsake; desert abandonment n. abandoning abase v. ~ oneself/sb lower oneself/sb in dignity; degrade oneself/sb ; abash to destroy the self-possession or self-confidence of:disconcert abashed adj. ~ embarrassed; ashamed abate v. make or become less abattoir n. = slaughterhouse (slaughter) abbess n. woman who is head of a convent or nunnery abbey n. buildingin which monks or nuns live as a community under an abbot or abbess abbot n. man who is head of a monastery or abbey abbreviate v. ~ sth shorten (a word, phrase, etc), esp by omitting letters
项目目的
练以下的技能
socket, 协程, MySQL, pymysql,
项目结果展示
服务端启动
服务端执行启动后等待客户端连接
客户端启动
客户端连接后显示一级界面
注册功能展示
用户名输入,密码,重复密码输入,都无法展示.注册成功后返回一级界面
登录功能展示
登录输入验证成功后进入二级界面
查询功能展示
查询单词后返回查询结果,输入## 退出查看 返回二级目录
历史记录功能查看
返回当前用户最近10条历史查询单词,然后返回二级菜单
注销功能
注销当前用户返回一级菜单
退出程序展示
项目需求分析整理
大白话思路
首先想好要用哪些技术.
数据库这边选择 mysql. 然后使用 pymysql 连接数据库这定死了.
然后就是数据库表结构设计
然后就是网络部分选择 tcp_socket 分为服务端客户端两套来做.各自处理各自的逻辑也没得说
然后关于高并发问题用 多进/线/协程来处理,这里选择最简单的协程最方便
然后就是具体的逻辑代码问题了.
逻辑方面:
登录验证这块就和数据库的用户表打交道了.
查词这块是和字典表来查询.查询这块的选择方式为了知识点覆盖我们不通过数据库来查询.基于文件来查询
历史记录这块重点就是sql语句的问题了,要排序要限制取数据条数,这块为了技术覆盖面我们要基于数据库来
那整体差不多就是这样的分析了.首先设计表结构把
mysql 表结构设计
首先这里我们可以使用外键,这样设计是可以的.但是鉴于我们仅仅是练习还是特么这么简单的结构设计就懒得这么折腾了
table user ----------------------- id | username | pwd | ----------------------- table user_hist -------------------------- id | user_id | hist_id | -------------------------- table user_history ----------------------------- id | use_id | dict_id | time ----------------------------- table dict_hist --------------------------- id | dict_id | hist_id | --------------------------- table e_dict.txt ----------------- id | word | val -----------------
所以我们使用更简单的结构设计,直接三张表解决吧
其次还需要做一件事就是要把 e_dict.txt 文件中的 数据添加到数据库中
这里存在着优化点.
文件中有 2w条数据,本以为不算特别大居然也用了接近5分钟.这个效率不敢恭维
因此可以使用 将数据整合在列表中,然后用 excutemany 来实现批量的添加更加高效.
""" 一次性的执行脚本文件 目的 实现将文件中的 数据插入在数据库中 """ import pymysql f = open("e_dict.txt") db = pymysql.connect("127.0.0.1", "root", "123456", "dict") cursor = db.cursor() for line in f: # 循环读取文件内容 tmp = line.split(" ") word = tmp[0] mean = " ".join(tmp[1:]).strip() sql = 'insert into words (word, mean) values ("%s","%s")' % (word, mean) try: cursor.execute(sql) db.commit() except: db.rollback() f.close()
执行完毕后,数据库的相关操作就全部完成了
业务逻辑
服务端和客户端彼此的职能是完全不同的,但是需要数据的交互
业务逻辑也全部都是彼此分割
总框架
服务端
前面使用了 sys.argv, 此方法用与获取用户命令行操作的参数.这样可以实现用户不通过修改源码的方式实现自定义属性的传入
此方法在 django ,scarpy 中都有体现
服务端这需要创建tcp_socket 以及并发的相关操作.以及一些其他的善后处理.当然还有与 DB 的交互也要完成
并且定义了 相关的业务逻辑操作码.以及 基于协程的 并发模式处理
def main(): # 连接数据库 db = pymysql.connect("127.0.0.1", "root", "123456", "dict") # 创建套接字 s = socket() # s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 保证端口复用的 s.bind(ADDR) s.listen(5) # 僵尸进程的处理 # signal.signal(signal.SIGCHLD, signal.SIG_IGN) # 循环等待连接 while True: try: c, addr = s.accept() print("Connect from", addr) except KeyboardInterrupt: s.close() sys.exit("服务器退出") continue # 创建子进程 gevent.spawn(do_child, c, db) # 如果在 linux 下直接这样也可以 # pid = os.fork() # if pid == 0: # s.close() # do_child(c, db) # sys.exit() # else: # c.close() def do_child(c, db): while True: data = c.recv(1024).decode() print(c.getpeername(), ":", data) if not data or data[0] == "E": c.close() sys.exit() elif data[0] == "R": do_register(c, db, data) elif data[0] == "L": do_login(c, db, data) elif data[0] == "Q": do_query(c, db, data) elif data[0] == "H": do_history(c, db, data) return
客户端
客户端要实现一个图形界面的展示以及用户的操作指引.顺便需要对用户的操作指定 操作码
比如在框架中可以提现 退出程序 的操作码为 " E "
同样和服务端一样, 客户端也需要在命令行进行定制
def main(): if len(sys.argv) < 3: print(""" argv is error""") return HOST = sys.argv[1] PORT = int(sys.argv[2]) s = socket() try: s.connect((HOST, PORT)) except Exception as e: print(e) return while True: print(""" =====================Welcome====================== -- 1. 注册 2.登录 3 退出 -- ================================================== """) try: cmd = int(input("输入选项:")) except Exception as e: print("命令错误") continue if cmd not in [1, 2, 3]: print("请输入正确选项") continue elif cmd == 1: do_register(s) elif cmd == 2: do_login(s) elif cmd == 3: s.send(b'E') sys.exit("谢谢使用")
登录业务逻辑
服务端
服务端对业务逻辑 为登录的时候定义 操作码为 " L ", 然后定义通过 do_login函数具体操作
do_login 函数具体取到 客户端的发送数据然后格式化处理后在数据库查询用户验证
判断基于状态返回
def do_login(c, db, data): l = data.split(" ") name = l[1] passwd = l[2] sql = "select * from user where name = '%s' and pwd = '%s'" % (name, passwd) # 查找用户 cursor = db.cursor() cursor.execute(sql) r = cursor.fetchone() if r == None: c.send(b'FAIL') else: c.send(b"OK") return
客户端
客户端的操作主要是获取用户数据然后发送 与服务端约定的格式和操作码.然后等待服务端的回传消息
在基于回传消息进行对用户的信息展示.
如果 登录成功会进一步的 通过 login 函数进入二级界面.
def do_login(s): name = input("User:") passwd = getpass.getpass() msg = "L %s %s" % (name, passwd) s.send(msg.encode()) data = s.recv(128).decode() print(data) if data == "OK": print("登陆成功") login(s, name) else: print("登录失败") return
注册业务逻辑
服务端
依旧和登录类似,数据结构处理后对数据库的操作
中间有一步对用户名存在的判断
def do_register(c, db, data): l = data.split(" ") name = l[1] passwd = l[2] cursor = db.cursor() sql = "select * from user where name = '%s' " % name cursor.execute(sql) r = cursor.fetchone() if r != None: c.send(b"EXISTS") return # 插入用户 sql = "insert into user (name, pwd) value ('%s','%s')" % (name, passwd) try: cursor.execute(sql) db.commit() c.send(b"OK") except: db.rollback() c.send(b"FAIL") return
客户端
客户端依旧是获取用户数据以及状态消息回传. 密码部分的显示用了 getpass 来隐藏输入显示
def do_register(s): while True: name = input("User:") passwd = getpass.getpass() passwd1 = getpass.getpass("Again:") if (" " in name) or (" " in passwd): print("用户名密码不能有空格") continue if passwd != passwd1: print("两次密码不一致") continue msg = "R %s %s" % (name, passwd) # 发送请求 s.send(msg.encode()) # 等待回应 data = s.recv(128).decode() if data == "OK": print("注册成功") # login(s,name) # 注册成功进入二级界面 return elif data == "EXISYS": print("用户已存在") else: print("注册失败") return
查询业务逻辑
服务端
服务端的查询需要拿到单词的解释以及分区是否可以查询到,
未查询到的时候要返回具体的错误消息(因为客户端不好判断,这里直接返回具体消息更方便)
因为这里使用过的是基于文件的查询,在判断对比 查询字段的时候 基于 字符串的排序,因为文件内的字段都是排序过的.直接这样对比可以更加快速
当然也需要考虑 "zzzz" 这样的单词到死也查询不到的可能性,因此需要注意判断的嵌套,最外层要又一次表示
def do_query(c, db, data): l = data.split(" ") name = l[1] word = l[2] # 插入历史记录 cursor = db.cursor() tm = time.ctime() sql = "insert into hist (name, word, time) value ('%s','%s','%s')" % (name, word, tm) try: cursor.execute(sql) db.commit() except: db.rollback() f = open(DICT_TEXT) for line in f: tmp = line.split(" ")[0] # 获取单词 if tmp > word: break elif tmp == word: c.send(line.encode()) f.close() return c.send("没有该单词".encode()) f.close()
客户端
客户端的查询逻辑,获取用户输入.基于结束码结束
这里需要注意的是不确定是否拿到的是单词解释还是因为找不到单词而返回的结果.
但是不论是什么这里不好区分我们由后端来区分,这里直接全部展示即可
def do_query(s, name): while True: word = input("请输出查询单词:") if word == "##": break msg = "Q %s %s" % (name, word) s.send(msg.encode()) # 单词解释 / 找不到 data = s.recv(2048).decode() print(data)
访问历史业务逻辑
服务端
主要还是 mysql 的使用 sql 语句,要用到排序和限制.
需要注意的是数据库创建的时候图方便没使用 time 相关的字段而是用了字符串
因此这里排序不能按照时间,要按照自增的 id 倒叙来保证最近时间的数据可靠性
def do_history(c, db, data): name = data.split(" ")[1] cursor = db.cursor() sql = "select * from hist where name='%s' order by id desc limit 10" % name cursor.execute(sql) r = cursor.fetchall() if not r: c.send(b"FAIL") else: c.send(b"OK") time.sleep(0.1) for i in r: msg = "%s %s %s" % (i[1], i[2], i[3]) c.send(msg.encode()) time.sleep(0.1) c.send(b"##")
客户端
客户端这块就没什么好说的了,唯一需要注意的是因为你不知道要查询的字段到底有多少数据,
因此直接选择用 死循环不停接收即可,也因为死循环,需要定义一个结束标识, 即 "##"
def do_history(s,name): msg = "H %s" %name s.send(msg.encode()) data = s.recv(128).decode() if data == "OK": while True: data = s.recv(1024).decode() if data == "##": break print(data) else: print("没有历史记录")
总结
整合了网络编程,并发编程.mysql,以及前期阶段的集成项目.非常适合练手.
项目虽然完成但是中间有大量的优化空间,
尤其是大量的冗余代码.比如 DB 操作可以整合为一个 工具类这样 服务端中的数据库操作会更加舒适
其次全程使用 函数式编程,整合成 面向对象方式更加清晰.
还有为了避免沾包问题,这里用了很蠢的 sleep 来处理也是优化空间
窝草.通篇忘了使用 pymysql 的防注入了.全程铁头自己拼字符串还行...醉了....回过头来看问题真的多
以上差不多就是这样吧,,,算了算了...心放宽...宽...
全部代码
客户端
from socket import * import sys import getpass # 创建网络连接 def main(): if len(sys.argv) < 3: print(""" argv is error""") return HOST = sys.argv[1] PORT = int(sys.argv[2]) s = socket() try: s.connect((HOST, PORT)) except Exception as e: print(e) return while True: print(""" =====================Welcome====================== -- 1. 注册 2.登录 3 退出 -- ================================================== """) try: cmd = int(input("输入选项:")) except Exception as e: print("命令错误") continue if cmd not in [1, 2, 3]: print("请输入正确选项") continue elif cmd == 1: do_register(s) elif cmd == 2: do_login(s) elif cmd == 3: s.send(b'E') sys.exit("谢谢使用") def do_register(s): while True: name = input("User:") passwd = getpass.getpass() passwd1 = getpass.getpass("Again:") if (" " in name) or (" " in passwd): print("用户名密码不能有空格") continue if passwd != passwd1: print("两次密码不一致") continue msg = "R %s %s" % (name, passwd) # 发送请求 s.send(msg.encode()) # 等待回应 data = s.recv(128).decode() if data == "OK": print("注册成功") # login(s,name) # 注册成功进入二级界面 return elif data == "EXISYS": print("用户已存在") else: print("注册失败") return def do_login(s): name = input("User:") passwd = getpass.getpass() msg = "L %s %s" % (name, passwd) s.send(msg.encode()) data = s.recv(128).decode() print(data) if data == "OK": print("登陆成功") login(s, name) else: print("登录失败") return def login(s, name): while True: print(""" =====================Welcome====================== -- 1. 查词 2.历史记录 3 注销 -- ================================================== """) try: cmd = int(input("输入选项:")) except Exception as e: print("命令错误") continue if cmd not in [1, 2, 3]: print("请输入正确选项") continue elif cmd == 1: do_query(s, name) elif cmd == 2: do_history(s,name) elif cmd == 3: return def do_query(s, name): while True: word = input("请输出查询单词:") if word == "##": break msg = "Q %s %s" % (name, word) s.send(msg.encode()) # 单词解释 / 找不到 data = s.recv(2048).decode() print(data) def do_history(s,name): msg = "H %s" %name s.send(msg.encode()) data = s.recv(128).decode() if data == "OK": while True: data = s.recv(1024).decode() if data == "##": break print(data) else: print("没有历史记录") if __name__ == '__main__': main()
服务端
from gevent import monkey monkey.patch_all() from socket import * import pymysql import sys import time import gevent import os # 制定输入格式,提供用户输入指引 if len(sys.argv) < 3: print(""" Start as: python3 dict_server.py 0.0.0.0 8000 """) sys.exit() # 定义全局变量 HOST = sys.argv[1] # sys.argv 获取命令行参数 PORT = int(sys.argv[2]) ADDR = (HOST, PORT) DICT_TEXT = "e_dict.txt" # 搭建网络连接 def main(): # l连接数据库 db = pymysql.connect("127.0.0.1", "root", "123456", "dict") # 创建套接字 s = socket() # s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) # 保证端口复用的 s.bind(ADDR) s.listen(5) # 僵尸进程的处理 # signal.signal(signal.SIGCHLD, signal.SIG_IGN) # 循环等待连接 while True: try: c, addr = s.accept() print("Connect from", addr) except KeyboardInterrupt: s.close() sys.exit("服务器退出") continue # 创建子进程 gevent.spawn(do_child, c, db) # 如果在 linux 下直接这样也可以 # pid = os.fork() # if pid == 0: # s.close() # do_child(c, db) # sys.exit() # else: # c.close() # 处理客户端请求 def do_child(c, db): while True: data = c.recv(1024).decode() print(c.getpeername(), ":", data) if not data or data[0] == "E": c.close() sys.exit() elif data[0] == "R": do_register(c, db, data) elif data[0] == "L": do_login(c, db, data) elif data[0] == "Q": do_query(c, db, data) elif data[0] == "H": do_history(c, db, data) return def do_register(c, db, data): l = data.split(" ") name = l[1] passwd = l[2] cursor = db.cursor() sql = "select * from user where name = '%s' " % name cursor.execute(sql) r = cursor.fetchone() if r != None: c.send(b"EXISTS") return # 插入用户 sql = "insert into user (name, pwd) value ('%s','%s')" % (name, passwd) try: cursor.execute(sql) db.commit() c.send(b"OK") except: db.rollback() c.send(b"FAIL") return def do_login(c, db, data): l = data.split(" ") name = l[1] passwd = l[2] sql = "select * from user where name = '%s' and pwd = '%s'" % (name, passwd) # 查找用户 cursor = db.cursor() cursor.execute(sql) r = cursor.fetchone() if r == None: c.send(b'FAIL') else: c.send(b"OK") return def do_query(c, db, data): l = data.split(" ") name = l[1] word = l[2] # 插入历史记录 cursor = db.cursor() tm = time.ctime() sql = "insert into hist (name, word, time) value ('%s','%s','%s')" % (name, word, tm) try: cursor.execute(sql) db.commit() except: db.rollback() f = open(DICT_TEXT) for line in f: tmp = line.split(" ")[0] # 获取单词 if tmp > word: break elif tmp == word: c.send(line.encode()) f.close() return c.send("没有该单词".encode()) f.close() def do_history(c, db, data): name = data.split(" ")[1] cursor = db.cursor() sql = "select * from hist where name='%s' order by id desc limit 10" % name cursor.execute(sql) r = cursor.fetchall() if not r: c.send(b"FAIL") else: c.send(b"OK") time.sleep(0.1) for i in r: msg = "%s %s %s" % (i[1], i[2], i[3]) c.send(msg.encode()) time.sleep(0.1) c.send(b"##") if __name__ == '__main__': main()
本文来自博客园,作者:羊驼之歌,转载请注明原文链接:https://www.cnblogs.com/shijieli/p/10439989.html