用 Python 撸一个 Web 服务器-第3章:使用 MVC 构建程序

Todo List 程序介绍

我们将要编写的 Todo List 程序包含四个页面,分别是注册页面、登录页面、首页、编辑页面。以下分别为四个页面的截图。

注册页面:

注册

注册

登录页面:

登录

登录

首页:

首页

首页

编辑页面:

编辑

编辑

程序页面非常简洁,甚至有些 Low。但这足够我们学习开发 Web 服务器程序原理,页面样式的问题并不是我们本次学习的重点,所以读者不必纠结于此。

Todo List 程序功能大概分为两个部分,一部分是 todo 管理,包含增删改查基础功能;另一部分是用户管理,包含注册和登录功能。

初识 MVC

介绍了 Todo List 程序的页面和功能,接下来我们就要思考如何设计程序。

以客户端通过浏览器向服务器发送一个获取应用首页的请求为例,来分析下服务器在收到这个请求后都需要做哪些事情:

  1. 首先服务器需要对请求数据进行解析,发现客户端是要获取应用首页。
  2. 然后找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  3. 最后将 HTML 内容组装成符合 HTTP 规范的数据进行返回。

这是一个较为理想的情况,因为 HTML 页面内容是固定的,我们不需要对其进行其他处理,直接返回给浏览器即可。通常我们管这种页面叫静态页面。

但实际情况中,Todo List 程序首页内容并不是一成不变的,而是动态变化的。首页 HTML 文件中只定义基础结构,具体的 todo 数据需要动态填充进去。所以一个更加完整的服务器处理请求的过程应该像下面这样:

  1. 首先服务器需要对请求数据进行解析,发现客户端是要获取应用首页。
  2. 然后从数据库中读取 todo 数据。
  3. 接着找到代表首页的 HTML 文件,读取 HTML 文件中的内容。
  4. 再将 todo 数据动态添加到 HTML 内容中。
  5. 最后将处理好的 HTML 内容组装成符合 HTTP 规范的数据进行返回。

现在已经知道了服务器处理请求的完整过程,我们就可以设计服务器程序了。试想一下,如果 Todo List 程序都像 Hello World 程序一样把代码都写在一个 Python 文件中也不是不可以。但这样的代码显然不具备良好的扩展性和可维护性。那么更好的设计模式是什么呢?

其实对于 Web 服务器程序的设计,业界早已达成了一个普遍的共识,那就是 MVC 模式:

M(Model):模型,用来存储和处理 Web 应用数据。

V(View):视图,格式化显示 Web 应用页面。

C(Controller):控制器,负责基础逻辑,如从模型层读取数据并将数据填充到视图层,然后返回响应。

通过 MVC 的分层结构,能够让 Web 应用设计更加清晰,可以很容易的构建可扩展、易维护的代码。模型层,说直白些其实就是用来读写数据库的 Python 代码,新增 todo 的时候,可以通过模型层的代码将数据保存到数据库中,访问首页时需要展示所有已保存的 todo,这时可以通过模型层的代码从数据库中读取所有 todo。视图层,可以将其简单的理解为 HTML 模板文件的集合。控制器起到粘合的作用,它将从模型层读取过来的数据填充到视图层并返回给浏览器,或者将浏览器通过 HTML 页面提交过来的数据解析出来再通过模型层写入数据库中。

我画了一个示例图,来帮助你理解 MVC 模式。图中标注了浏览器发起一个请求到获得响应,中间经历的完整过程。

MVC

还是以客户端请求 Todo List 程序首页为例,一个完整的请求过程如下:

  1. 浏览器发起请求。
  2. 请求到达服务器后首先进入控制器,然后控制器从模型获取 todo 数据。
  3. 模型操作数据库,查询 todo 数据。
  4. 数据库返回 todo 数据。
  5. 模型将从数据库中查询的 todo 数据返回给控制器,控制器暂时将数据保存在内存中。
  6. 控制器从视图中获取首页 HTML 模板。
  7. 控制器将从模型查出来的 todo 数据填充到首页 HTML 模板中,并组装成符合 HTTP 规范的数据。
  8. 服务器返回响应。

其实 MVC 是一个宏观上的分层,具体细节部分还需要根据我们设计程序的粒度来进行处理,比如有些逻辑既可以写在控制器层,也可以写在模型层,甚至我们还可以在 MVC 的基础上扩展更多的分层。这些都需要结合具体的业务逻辑来决定。

构建 Todo List 程序

学习了 MVC 模式,我们就可以根据 MVC 模式来试着构建 Todo List 程序了。

Todo List 程序分两部分:todo 管理、用户管理。在项目初期,我们肯定不会考虑的太过全面。所以可以先不考虑用户管理功能部分的实现,先只考虑如何实现 todo 管理功能。

Todo List 程序目录结构设计如下:

todo_list  
├── server.py  
└── todo  ├── \_\_init\_\_.py  ├── config.py  ├── controllers.py  ├── db  │   └── todo.json  ├── models.py  ├── templates  │   ├── edit.html  │   └── index.html  └── utils.py

这里以 todo_list/ 作为程序的根目录,根目录下包含 server.py 文件和 todo/ 目录。其中 server.py 主要功能就是作为一个 Web Server 来接收请求和返回响应,它是 Todo List 程序的入口和出口。而 todo/ 目录则是 Todo List 程序处理业务逻辑的核心。

todo/ 目录下的 __init__.pytodo/ 文件夹标记为一个 Python 包。 config.py 用于存储一些项目的基础配置。utils.py 是一个工具集,里面可以定义一些供其他模块调用的类和方法。db/ 目录作为 Todo List 程序存储数据的目录,db/todo.json 用来存储所有的 todo 内容。剩下还有两个 .py 文件和一个目录没有介绍,相信你已经猜到了 models.pytemplates/controllers.py 分别对应了 MVC 模式中的模型、视图、控制器。models.py 中编写操作 todo 数据的代码,templates/ 目录用来存放 HTML 模板文件,templates/index.html 是首页,templates/edit.html 是编辑页面,controllers.py 编写负责程序控制的基础逻辑代码。

我们对项目的目录结构有了一个概览,这里我要强调一下 db/ 目录的作用。我们在开发整个 Todo List 程序的过程中都不会使用实际的数据库程序,项目中所有需要存储的数据都保存在 db/ 目录下的文件中。在开发 Web 程序时,需要用到数据库的目的就是为了存储数据,对于 Todo List 程序来说使用文件同样能满足需求,同时能够照顾到对数据库不了解的读者。

Todo List 首页开发

Todo List 程序目录结构构建完成后就可以动手开发程序了,我们可以从一个请求经历的过程来着手。

一个请求发送到服务器,首先服务器需要有一个能够接收请求的入口,在程序根目录 todo_list/ 下的 server.py 就是这个入口。server.py 文件代码如下:

\# todo_list/server.pyimport socket  
import threadingfrom todo.config import HOST, PORT, BUFFER_SIZEdef process_connection(client):  """处理客户端请求"""  \# 接收请求报文数据  request_bytes = b''  while True:  chunk = client.recv(BUFFER_SIZE)  request_bytes += chunk  if len(chunk) < BUFFER_SIZE:  break\# 请求报文  request_message = request_bytes.decode('utf-8')  print(f'request_message: {request_message}')\# TODO: 解析请求  \# TODO: 返回响应\# 关闭连接  client.close()def main():  """入口函数"""  with socket.socket() as s:  s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  s.bind((HOST, PORT))  s.listen(5)  print(f'running on http://{HOST}:{PORT}')while True:  client, address = s.accept()  print(f'client address: {address}')  \# 创建新的线程来处理客户端连接  t = threading.Thread(target=process_connection, args=(client,))  t.start()

server.py 程序几乎就是之前实现的多线程版 Hello World 服务器程序照搬过来的。为了程序代码更加清晰,这里将服务器的 IP 地址、端口、接收请求的缓冲区大小定义为变量写在了配置文件 todo/config.py 中,所以需要在 server.py 文件顶部从配置文件中导入 HOSTPORTBUFFER_SIZE。在 main 函数中,实例化 socket 对象部分的代码也有所改变,这里采用了 with 语句来实例化 socket 对象,这样能够保证任何情况下退出程序时 socket 都能够被正确关闭。此处 with 语句的用法可以类比文件操作时的 with 语句。处理客户端连接请求的 process_connection 函数内部基本逻辑没有改变,其中有两个 TODO 注释表示解析请求和返回响应的功能暂未实现。

server.py 入口程序接收到客户端的请求以后,需要解析请求报文,并根据解析出来的请求报文来决定如何处理请求并返回响应。所以接下来我们需要编写解析请求的代码。

不过在这之前,我先给出 todo/config.py 配置文件的代码,毕竟之后还会用到:

\# todo_list/todo/config.pyimport os\# todo/ 目录绝对路径  
BASE_DIR = os.path.dirname(os.path.abspath(\_\_file\_\_))\# IP  
HOST = '127.0.0.1'  
\# 端口  
PORT = 8000\# 缓冲大小  
BUFFER_SIZE = 1024

配置文件中除了包含前面介绍过的表示 IP 地址、端口、接收请求的缓冲区大小的几个变量,还有一个 BASE_DIR 变量用来表示 todo/ 目录的绝对路径,方便在程序中获取项目路径。

现在来看下如何解析请求,我们可以定义一个 Request 类用来专门解析请求报文,代码写在 todo/utils.py 文件中:

\# todo_list/todo/utils.pyclass Request(object):  """请求类"""def \_\_init\_\_(self, request_message):  method, path, headers = self.parse_data(request_message)  self.method = method \# 请求方法 GET、POST  self.path = path \# 请求路径 /index  self.headers = headers \# 请求头 {'Host': '127.0.0.1:8000'}def parse_data(self, data):  """解析请求报文数据"""  \# 用请求报文中的第一个 '\\r\\n\\r\\n' 做分割,将得到请求头和请求体  \# 请求体暂时用不到先不处理  header, body = data.split('\\r\\n\\r\\n', 1)  method, path, headers = self.\_parse_header(header)  return method, path, headersdef \_parse_header(self, data):  """解析请求头"""  \# 拆分请求行和请求首部  request_line, request_header = data.split('\\r\\n', 1)\# 请求行拆包 'GET /index HTTP/1.1' -> \['GET', '/index', 'HTTP/1.1'\]  \# 因为 HTTP 版本号没什么用,所以用一个下划线 _ 变量来接收  method, path, _ = request_line.split()\# 解析请求首部所有的键值对,组装成字典  headers = {}  for header in request_header.split('\\r\\n'):  k, v = header.split(': ', 1)  headers\[k\] = vreturn method, path, headers

Request 类的初始化方法 __init__ 接收请求报文字符串作为参数。在其内部调用 parse_data 方法将请求报文字符串解析成我们需要的结构化数据。

解析完请求报文,我们需要根据请求报文信息来判断如何返回响应。基础逻辑判断部分的代码可以写在 todo/controllers.py 中:

\# todo_list/todo/controllers.pyfrom todo.utils import render_templatedef index():  """首页视图函数"""  return render_template('index.html')

定义在控制器层的函数也叫视图函数,因为它们通常返回视图层的 HTML 内容。index 视图函数用来处理请求首页的逻辑,它返回 render_template 函数的调用结果,render_template 函数的作用是将 HTML 内容读取成字符串并返回,其定义如下:

\# todo_list/todo/utils.pyimport osfrom todo.config import BASE_DIRdef render_template(template):  """读取 HTML 内容"""  \# 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容  template_dir = os.path.join(BASE_DIR, 'templates')  path = os.path.join(template_dir, template)  with open(path, 'r', encoding='utf-8') as f:  html = f.read()  return html

todo/controllers.py 文件底部还定义了一个 routes 字典,字典的键为请求路径,值为一个元组,元组的第一个元素作为处理请求的函数,第二个元素是一个列表,里面定义处理请求的函数所允许的请求方法。index 视图函数能够同时匹配两个路径://index,因为这两个路径通常都代表首页。

\# todo_list/todo/controllers.pyroutes = {  '/': (index, \['GET'\]),  '/index': (index, \['GET'\]),  
}

读取出 HTML 内容以后,我们就可以构造响应报文并返回给浏览器了。在 utils.py 文件下,编写一个 Response 类用来构造响应:

\# todo_list/todo/utils.pyclass Response(object):  """响应类"""\# 根据状态码获取原因短语  reason_phrase = {  200: 'OK',  405: 'METHOD NOT ALLOWED',  }def \_\_init\_\_(self, body, headers=None, status=200):  \# 默认响应首部字段,指定响应内容的类型为 HTML  \_headers = {  'Content-Type': 'text/html; charset=utf-8',  }if headers is not None:  \_headers.update(headers)  self.headers = \_headers \# 响应头  self.body = body \# 响应体  self.status = status \# 状态码def \_\_bytes\_\_(self):  """构造响应报文"""  \# 状态行 'HTTP/1.1 200 OK\\r\\n'  header = f'HTTP/1.1 {self.status} {self.reason_phrase.get(self.status, "")}\\r\\n'  \# 响应首部  header += ''.join(f'{k}: {v}\\r\\n' for k, v in self.headers.items())  \# 空行  blank_line = '\\r\\n'  \# 响应体  body = self.bodyresponse_message = header + blank_line + body  return response_message.encode('utf-8')

Response 类的初始化方法 __init__ 接收三个参数,分别为响应体、响应首部字段、状态码。其中响应体为 str 类型,首页的响应体实际上就是 index.html 文件内容。响应首部字段为 dict 类型,在构造响应报文时,所有的响应首部字段最终按照 HTTP 规范拼接到一起作为响应首部。状态码为数值类型,目前只考虑了状态码为 200 正常响应和 405 请求方法不被允许。

需要注意的是,Response 类定义了 __bytes__ 魔法方法作为构造响应报文的方法。当使用 Python 内置的 bytes 方法转换 Response 实例对象时(bytes(Response())),会自动调用 __bytes__ 魔法方法。

从解析请求到构造响应报文的代码现在已经基本编写完成。接下来我们将整个处理请求的流程串联起来,回到 server.py 文件,继续完善代码:

\# todo_list/server.pyimport socket  
import threadingfrom todo.config import HOST, PORT, BUFFER_SIZE  
from todo.utils import Request, Response  
from todo.controllers import routesdef process_connection(client):  """处理客户端请求"""  \# 接收请求报文数据  request_bytes = b''  while True:  chunk = client.recv(BUFFER_SIZE)  request_bytes += chunk  if len(chunk) < BUFFER_SIZE:  break\# 请求报文  request_message = request_bytes.decode('utf-8')  print(f'request_message: {request_message}')\# 解析请求报文,构造请求对象  request = Request(request_message)  \# 根据请求对象构造响应报文  response_bytes = make_response(request)  \# 返回响应  client.sendall(response_bytes)\# 关闭连接  client.close()def make_response(request, headers=None):  """构造响应报文"""  \# 默认状态码为 200  status = 200  \# 获取匹配当前请求路径的处理函数和函数所接收的请求方法  \# request.path 等于 '/''/index' 时,routes.get(request.path) 将返回 (index, \['GET'\])  route, methods = routes.get(request.path)\# 如果请求方法不被允许,返回 405 状态码  if request.method not in methods:  status = 405  data = 'Method Not Allowed'  else:  \# 请求首页时 route 实际上就是我们在 controllers.py 中定义的 index 视图函数  data = route()\# 获取响应报文  response = Response(data, headers=headers, status=status)  response_bytes = bytes(response)  print(f'response_bytes: {response_bytes}')return response_bytesdef main():  """入口函数"""  with socket.socket() as s:  s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  s.bind((HOST, PORT))  s.listen(5)  print(f'running on http://{HOST}:{PORT}')while True:  client, address = s.accept()  print(f'client address: {address}')  \# 创建新的线程来处理客户端连接  t = threading.Thread(target=process_connection, args=(client,))  t.start()if \_\_name\_\_ == '\_\_main\_\_':  main()

首先完成之前未写完的 process_connection 函数。将原来标记 TODO 注释的地方替换成了如下代码:

\# 解析请求报文,构造请求对象  
request = Request(request_message)  
\# 根据请求对象构造响应报文  
response_bytes = make_response(request)  
\# 返回响应  
client.sendall(response_bytes)

新增了一个 make_response 函数,方便用来根据请求对象构造响应报文。函数中我写了比较详细的注释,你可以根据注释内容读懂代码逻辑。

最后给出首页 todo/templates/index.html 的 HTML 代码:

<!--todo_list/todo/templates/index.html--><!DOCTYPE html>
<html>  
<head>  <meta charset="UTF-8">  <title>Todo List</title>  <style>  \* {  margin: 0;  padding: 0;  }  ul {  list-style: none;  }  a {  text-decoration: none;  outline: none;  color: #000000;  }  h1 {  margin: 20px auto;  }  .container {  display: flex;  justify-content: center;  align-items: center;  }  .container ul {  width: 100%;  max-width: 600px;  }  .container ul li {  height: 40px;  line-height: 40px;  margin-bottom: 4px;  padding: 0 6px;  display: flex;  justify-content: space-between;  background-color: #d2d2d2;  }  </style>  
</head>  
<body>  
<h1 class="container">Todo List</h1>  
<div class="container">  <ul>  <li>  <div>Hello World</div>  </li>  </ul>  
</div>  
</body>  
</html>

HTML 代码比较简单,其中顶部写了一些基础的 CSS 样式,都很容易看懂,这里不再讲解。

接下来在终端中,进入 todo_list/目录下,使用 Python 运行 server.py 文件,看到如下打印结果说明程序已经正常启动:

运行 Todo List 服务器

运行 Todo List 服务器

打开浏览器,地址栏输入 http://127.0.0.1:8000/ 或者 http://127.0.0.1:8000/index,你将看到 Todo List 程序首页:

Todo List 首页

Todo List 首页

至此,Todo List 程序首页初步完成。不过我想很多读者看到这里会产生疑惑,说好的 MVC 呢,目前为止我们并没有编写一行模型层的代码,并且首页的 HTML 内容也不是动态填充的。没错,为了能够尽快让 Todo List 程序跑起来,我有意的避开了这两个问题,下一章我们再来解决这两个问题。

本章源码:chapter3

原文出处: https://jianghushinian.cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/24215.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】—— 线程控制的基本介绍

目录 &#xff08;一&#xff09;POSIX线程库 &#xff08;二&#xff09;创建线程 2.1 线程ID及进程地址空间布局 &#xff08;三&#xff09;线程终止 &#xff08;四&#xff09;分离线程 &#xff08;一&#xff09;POSIX线程库 POSIX线程库&#xff08;POSIX Thread…

Node.js后端构建指南:MongoDB与Express的集成

安装express 安装 Express 并将其保存到依赖列表中&#xff1a; $ cnpm install express --save 以上命令会将 Express 框架安装在当前目录的 node_modules 目录中&#xff0c; node_modules 目录下会自动创建 express 目录。以下几个重要的模块是需要与 express 框架一起安…

nss刷题(4)

1、[SWPUCTF 2021 新生赛]easyrce <?php error_reporting(0); highlight_file(__FILE__); if(isset($_GET[url])) { eval($_GET[url]); } ?> if(isset($_GET[url])) isset函数用来检测url变量是否存在&#xff1b;$_GET函数获取变量数据 eval($_GET[url]); eval函数用…

【GIS矢量切片】tippecanoe在Windows和CentOS中的安装

组件安装记录 背景介绍Windows下安装1、下载工具2、存放安装包3、进入DOS终端4、在终端执行命令5、下载程序6、放置源码7、修改配置信息8、编译9、测试10、参数说明瓦片输出瓦片描述和权属信息输入文件和图层名输入文件的并行处理输入文件的投影缩放级别瓦片分辨率CentOS 7安装…

第三篇——大数据思维的科学基础

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么&#xff1f; 四、总结五、升华 一、背景介绍 大数据时代&#xff0c;大数据思维的重要性不言而喻&#xff1b;而信息在…

6月26~28日,2024北京国际消防展即将开幕!

随着社会的快速发展&#xff0c;消防安全日益受到广大民众的高度关注。为了进一步推动消防科技的创新与发展&#xff0c;提升全民消防安全意识&#xff0c;2024年北京消防展将于6月26日在北京国家会议中心盛大开展。目前:观众预登记已全面启动&#xff0c;广大市民和业界人士可…

SQLite3(1):介绍安装与测试

目录 1、SQLite3介绍 2、SQLite3的优势和特性 3、SQLite3安装与测试 3.1 SQLite3安装 3.2 SQLite3测试 4、SQLite3简单使用 4.1 连接数据库文件 4.2 创建信息表 4.3 插入三个学生信息 4.4 确认信息 5、总结 1、SQLite3介绍 SQLite3是一种轻量级的关系型数据库管理系…

论文阅读 A Distributional Framework for Data Valuation

本论文解决的问题 量化数据价值&#xff08;机器学习模型训练中各个数据点的贡献&#xff09; 避免数据价值受到其所处数据集的影响&#xff0c;使数据点的估值更加稳定、一致 变量假设 假设 D 表示一个在全集 Z 上的数据分布。对于监督学习问题&#xff0c;我们通常认为 Z…

jvm学习笔记(一) ----- JAVA 内存

JAVA 内存 一、程序计数器二、虚拟机栈三、本地方法栈四、堆五、非JAVA内存(堆外内存)1.元空间(Metaspace)2.直接内存 链接: jvm学习笔记(二) ----- 垃圾回收 链接: jvm学习笔记(三) ----- 垃圾回收器 一、程序计数器 虚拟机需要通过『程序计数器』记录指令执行到哪了。线程要…

代码随想录算法训练营day43

题目&#xff1a;1049. 最后一块石头的重量 II 、494. 目标和、474.一和零 参考链接&#xff1a;代码随想录 1049. 最后一块石头的重量 II 思路&#xff1a;本题石头是相互粉碎&#xff0c;粉碎后剩下的重量就是两块石头之差&#xff0c;我们可以想到&#xff0c;把石头分成…

使用智谱 GLM-4-9B 和 SiliconCloud 云服务快速构建一个编码类智能体应用

本篇文章我将介绍使用智谱 AI 最新开源的 GLM-4-9B 模型和 GenAI 云服务 SiliconCloud 快速构建一个 RAG 应用&#xff0c;首先我会详细介绍下 GLM-4-9B 模型的能力情况和开源限制&#xff0c;以及 SiliconCloud 的使用介绍&#xff0c;最后构建一个编码类智能体应用作为测试。…

数据结构和算法之数组和链表

一、数组 数组是一种线性数据结构&#xff0c;它是由一组连续的内存单元组成的&#xff0c;用于存储相同类型的数据。在JavaScript中&#xff0c;数组可以包含任意类型的数据&#xff0c;不只限于基本数据类型。 1.存储方式 在内存中&#xff0c;数组的元素是连续存储的&…

【Vue】组件的存放目录问题

注意&#xff1a; .vue文件 本质无区别 组件分类 .vue文件分为2类&#xff0c;都是 .vue文件&#xff08;本质无区别&#xff09; 页面组件 &#xff08;配置路由规则时使用的组件&#xff09;复用组件&#xff08;多个组件中都使用到的组件&#xff09; 存放目录 分类开来的…

Llama模型家族之拒绝抽样(Rejection Sampling)(二)均匀分布简介

LlaMA 3 系列博客 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;一&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;二&#xff09; 基于 LlaMA 3 LangGraph 在windows本地部署大模型 &#xff08;三&#xff09; 基于 LlaMA…

ssti模板注入

一、Flask应用 1、介绍 定义 Flask&#xff1a;是一个使用Python编写的轻量级web应用框架。Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。 特点 良好的文档、丰富的插件、包含开发服务器和调试器、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。 …

手机短信删除怎么恢复?快速找回的3个秘密武器

手机&#xff0c;这个我们每天离不开的小玩意儿&#xff0c;有时候也会让我们头疼不已。比如&#xff0c;你一不小心&#xff0c;或者为了清理点空间&#xff0c;就把那些重要的短信给删了。这些短信可能是你和好友的深夜聊天&#xff0c;或者是重要的工作信息。一旦删除&#…

哪款开放式耳机佩戴最舒服?2024五款备受推崇产品分享!

​在现今耳机市场&#xff0c;开放式耳机凭借其舒适的佩戴体验和独特的不入耳设计&#xff0c;备受消费者追捧。它们不仅让你在享受音乐时&#xff0c;仍能察觉周围的声音&#xff0c;确保与人交流无障碍&#xff0c;而且有利于耳朵的卫生与健康。对于运动爱好者和耳机发烧友而…

GIGE 协议摘录 —— 引导寄存器(四)

系列文章目录 GIGE 学习笔记 GIGE 协议摘录 —— 设备发现&#xff08;一&#xff09; GIGE 协议摘录 —— GVCP 协议&#xff08;二&#xff09; GIGE 协议摘录 —— GVSP 协议&#xff08;三&#xff09; GIGE 协议摘录 —— 引导寄存器&#xff08;四&#xff09; GIGE 协议…

前后端实现文件上传进度条-实时进度

后端接口代码&#xff1a; PostMapping("/upload")public ResponseEntity<String> handleFileUpload(RequestParam("file") MultipartFile file) {try {// 获取文件名String fileName file.getOriginalFilename();// 创建上传目标路径Path targetPa…

基于简单Agent对医疗数据进行分析

数据表 供应商资格审核规定.pdf 医生名录.xlsx 历史就诊记录.xlsx 患者信息名录.xlsx 药品.xlsx 药品库存管理.xlsx 采购单位基本信息.xlsx Agent测试 模型基于ChatGPT-3.5 问题&#xff1a;帮我找出不达标的供应商 Agent分析过程 [Thought: 0] Key Concepts: - 不达标的供…