(注:此文章为本人所参与的某学生团队培训大一新生时所用,面向零基础同学,因此没有提及较进阶的内容,以及内容可能不是那么专业。)
第一章 网络编程基础 当今的时代是互联网的时代,网络无处不在。而我们先前所写的程序,都是运行在本地计算机的。在许多情况下,我们需要写一个程序,能够为其他计算机服务。因此我们就需要使用网络编程 技术。
在正式进入Web开发前,我需要简单介绍一些计算机网络知识。
1.1 IP 协议 计算机为了联网,就必须约定通信协议。互联网协议包含上百协议标准,最重要的两个就是TCP协议和IP协议。
在通信时,通信双方必须知道对方的标识。互联网上每个计算机的唯一标识就是IP地址。IP地址由4个整数、3个小数点组成(称为IPv4),其中每个整数的范围都是0~255。
例如,以下几个都是正确的IP地址:
1 2 3 127.0.0.1 172.19.96.42 219.216.96.4
IP地址主要分为公网IP、私网IP、本地环回IP以及其他用途或者保留IP。
公网IP地址是可以在互联网上直接访问的地址,每个公网IP在全球范围内唯一。互联网上的所有人都可以通过公网IP访问这个设备。
私网IP:私有IP地址用于局域网内部的通信 ,不能直接访问互联网。私有IP地址被分配在特定的地址范围内,由网络管理员根据需要进行分配。私网IP的具体范围有:A类私有IP(10.0.0.0 到 10.255.255.255),B类私有IP(172.16.0.0 到 172.31.255.255),C类私有IP(192.168.0.0 到 192.168.255.255)。
回环地址:回环地址用于设备与自身通信,通常用于测试计算机的网络堆栈是否正常工作。回环地址是由IPv4协议规定的,所有回环地址都属于 127.0.0.0 到 127.255.255.255 的范围,但通常我们使用 127.0.0.1 作为标准回环地址。
简单总结:公网IP可以被互联网的其他设备访问到;私网IP只能被与自己处在相同的局域网的设备访问到;本地环回IP只能被自己访问到,通常用于开发测试。
1.2 TCP协议 TCP协议是建立在IP协议基础上的。TCP协议负责在两台计算机之间建立可靠的连接,保证数据包安装顺序到达。TCP协议会通过“3次握手”建立可靠连接。
许多更高级的协议都是建立在TCP协议上的,比如说用于浏览器的HTTP协议、用于邮箱的SMTP协议。一个TCP报文除了包含要传输的数据以外,还需要包含源IP和目标IP地址,以及对应的端口。
同一台计算机上可能跑着多个网络程序。可能同时跑着QQ,浏览器,微信等等,因此只知道客户的IP地址是不够的,每个应用程序还需要指定一个端口号,这样TCP报文就可以通过IP地址+端口号定位发送数据和接受数据的双方。
1.3 套接字 为了让两个程序进行网络通信,二者需要使用Socket套接字。“套接字”,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同计算机之间的通信。简单理解,就是程序可以通过Socket套接字,来访问互联网上的另一台计算机程序。
服务器上的每一个服务都需要打开一个Socket,并且绑定到一个端口上,不同的端口对应于不同的服务。
1.4 TCP编程 创建TCP连接时,主动发起连接的叫做客户端,被动响应连接的叫做服务器。例如,在访问本网站时,用户自己的计算机就是客户端,用户的浏览器会主动向本网站的服务器发起连接。如果网络通畅,本网站的服务器就会接受用户的连接,建立了TCP。后续的通信就是发送网页内容、接受用户请求。
1.4.1 创建TCP服务器 一个最基本的TCP服务器的流程为:
使用socket创建套接字
使用bind绑定IP和端口
使用listen使套接字变为被动连接
使用accept等等客户的连接
使用recv和send接受发送数据
比如,我们在本地写一个server.py,绑定为本机端口8080,向浏览器发送Hello World:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import sockethost = '127.0.0.1' port = 8080 web = socket.socket() web.bind((host, port)) web.listen(5 ) print ("Wait for connection..." )while True : conn, addr = web.accept() data = conn.recv(1024 ) print (data) conn.sendall(f"HTTP/1.1 200 OK\r\n\r\nHello World, {addr} !" .encode()) conn.close()
运行脚本,打开浏览器,输入http://127.0.0.1:8080,查看效果:
1.4.2 创建TCP客户端 TCP客户端比服务器简单的多,因为它是主动连接的。TCP客户端只需要connect和send方法就可以了。
1 2 3 4 5 6 7 8 9 10 11 import sockets = socket.socket() host = '127.0.0.1' port = 8080 s.connect((host, port)) data = input ("Please enter data: " ) s.send(data.encode()) recv = s.recv(1024 ) print (recv)s.close()
我们需要同时运行这两个脚本,可以一个在终端运行:
可以看到,服务器端可以成功接受我们的数据。
1.5 HTTP协议 HTTP(超文本传输协议)是互联网上应用最为广泛的一种网络协议。利用TCP在两台计算机(通常是Web服务器和客户端)之间传输信息。客户端使用Web浏览器发送HTTP请求给服务器,服务器发送被请求的信息给客户端。
当用户在浏览器中输入URL后,浏览器首先会去请求DNS服务器,获得这个URL对应的IP地址,然后给这个IP地址的主机发送HTTP请求。接着就会收到服务器返回的HTTP信息(响应,Response),浏览器经过渲染后,呈现给用户。
HTTP协议的请求方法有GET、POST、HEAD、PUT、DELETE、OPTIONS。其中最常用的为GET请求和POST请求。GET请求为指定的页面信息,并返回数据。POST请求则向指定资源提交数据(例如提交表单或者上传文件)。
服务器返回给客户端的状态码分为五种类型,由第一位数字表示大类。比如说2开头的状态码代表成功返回响应、4开头代表客户端请求错误、5开头的代表服务器内部错误。
最常见的有:200 OK,404 Not Found, 403 Forbidden, 500 Internal Error。
1.6 HTTPS协议 HTTPS,全称为安全超文本传输协议。与HTTP相比,多了一个安全(Secure),它是在 HTTP 的基础上加入了安全层,通常通过 SSL/TLS 加密技术来保护数据传输。数据在HTTP协议传输过程中是明文的,没有加密保护。意味着可能被第三方窃听甚至修改;而HTTPS使用 SSL/TLS 加密协议来加密数据,这样即使数据在传输过程中被截获,也无法轻易被解密或篡改,提供了更高的安全性。HTTPS的网站需要通过证书来证明其身份,浏览器会验证证书的有效性,这可以减少用户访问恶意网站的风险。现代网站大多推荐使用 HTTPS 来确保用户的隐私和安全。
1.7 Web服务开发 对于服务器来说,一个Web服务最重要的两部分就是:给用户呈现的网页以及后台的业务处理。因此,一个Web服务通常分为前端开发 与后端开发 。
前端开发是指开发用户直接交互的部分,也就是网页的界面和用户体验。前端开发主要关注页面的呈现和交互效果,涉及HTML(用来定义网页的结构和内容)、CSS(用来设置网页的样式,包括布局、颜色、字体等)、JavaScript(用来为网页添加交互功能,如表单验证、动态内容加载、动画效果等)。前端开发需要实现:
构建和优化网页界面
提升用户体验(UX)
响应式设计,使网站适应不同的屏幕尺寸(如手机、平板、桌面)
后端开发是指开发处理数据、逻辑和服务器操作的部分,也就是用户看不到的部分。后端主要负责处理前端传来的请求,操作数据库,处理业务逻辑等。后端需要使用一种编程语言的Web框架,最常用的有Python(Flask、Django),Java(SpringBoot),Go、Rust等。后端开发需要实现:
设计和管理数据库
处理和响应前端请求
设计和实现业务逻辑
确保数据的安全性和可靠性
第二章 前端基础 Web开发通常分为前端和后端。前端是指用户直接交互的部分,包括Web页面的结构,Web的外观视觉表现以及Web层面的交互实现。
前端开发最常使用的“三件套”为HTML、CSS和JavaScript 。
对于初学者来说,本节内容并不需要全部掌握,只需要对前端开发有一个最基础的印象,能够了解什么是HTML以及HTML的最常用的标签、分清CSS和JavaScript即可。
理论知识往往都是在实践过程中增强的。
2.1 HTML基础 HTML是用来描述网页的一种语言。HTML是指超文本标记语言(Hyper Text Markup Language)。严格来说它并不是变成语言,而是一种标记语言。HTML使用一套标签包裹各个元素。Web浏览器的作用就是读取HTML文档,然后按照规则解析显示HTML页面给用户。
2.1.1 HTML的Hello World! 几乎所有的IDE(集成开发环境)都支持编辑HTML文件,PyCharm也不例外。我们可以在Python项目中创建一个HTML文件:
PyCharm会默认给我们生成最基础的HTML框架,如下所示:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > </body > </html >
第一行<!DOCTYPE html>代表这是一个HTML5文档。然后所有的元素都被包裹在<html></html>这一组标签中。<head></head>标签内包含了文件的元数据,比如<title></title>定义了网站的标题为Title。
<body></body>中包含的元素就是HTML文档的可见页面内容。我们可以在里面写上字符串试一试:
1 2 3 4 5 6 7 8 9 10 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Title</title > </head > <body > Hello World! </body > </html >
然后我们右键这个文件,使用浏览器打开:
发现我们写在body里面的字符串被加载了出来。
2.1.2 HTML标签、元素、属性 HTML的标签有以下特点:
HTML标签是由尖括号包围的,比如<html>,<body>
HTML标签通常成对出现,代表这个标签的起始作用域和终止作用域,比如<body>其他东西</body>,<head>其他东西</head>
以下是HTML的比较常用的标签(初学肯定记不住这么多,往往都是在日后实践中体会到这些标签的作用):
标签
作用
定义注释
<a>
定义链接
<article>
定义文章区域
<b>
定义文本粗体
<body>
定义文档的主体
<br>
换行(这个没有对应的</br>,单独使用就有换行的功能)
<button>
定义按钮
<div>
文档的分区,常用作框架布局
<form>
用户填写的表单
<h1> to <h6>
定义标题,从1到6级字号越来越小
<head>
定义文档信息
<img>
定义图像
<input>
定义输入控件
<p>
定义段落
<script>
插入JavaScript脚本
<select>
定义选择列表
<style>
定义样式
<table>
定义表格
HTML元素就以开始标签起,以终止标签结束。元素的内容就是开始标签与结束标签之间的内容,以2.1.1的代码为例:
1 2 3 <body > <p > Hello World!</p > </body >
上面的代码就包含一个<body>元素和一个<p>元素。
每一个HTML元素都可以设置属性,属性放在开始标签中,总是以“属性=值”的形式出现。每种标签都有自己独特的属性,比如<a>标签可以添加href属性,设置单击文本时候跳转的链接:
1 <a href ="https://neuicere.cn" > I+CERE创新团队主页</a >
这会生成一个:
I+CERE创新团队主页
大多数HTML元素都支持class、id、style属性,为HTML元素定义一个所属类名(可以多个)、元素的唯一ID以及元素的样式。
HTML元素可以通过class属性赋予一个或多个类名,以便在CSS或JavaScript中引用和操作这些元素。我们在使用第三方框架时,可以为元素指定一个或多个框架提供的类名,从而让该元素应用框架中的预定义样式。
2.1.3 HTML的div元素 <div>元素是HTML中一个非常常见的容器元素,最主要的用途就是用于文档布局和分组内容。
分组内容:<div>用于将页面中的一部分内容组织成一个整体,使其在结构上更加清晰。例如,可以将一个页面的不同部分(如导航栏、侧边栏、主内容区域等)。
布局容器:在布局设计中,<div>经常用作容器,配合 CSS 来控制布局。
应用样式:通过给<div>元素添加class 或id属性,可以为这些容器应用样式。
实现响应式设计:在响应式设计中,<div>常常用于适配不同屏幕尺寸和设备。通过设置不同的CSS样式,<div>可以帮助页面在各种设备上正确显示,确保良好的用户体验。
2.1.4 HTML表单 为了实现浏览器和服务器的互动,可以使用HTML表单收集不同类型的用户输入,然后将输入的内容发送到服务器上,经过服务器后端成或许处理,再返回相应的信息,实现交互的效果。需要表单的地方有很多,如用户注册、登录、个人信息收集等等。
在HTML中,使用<form>表单,即可创建一个表单,表单常见的结构如下:
1 2 3 <form name ="form-name" method ="post" action ="/" > </form >
常见的属性为:
name:表单的名词
method:设置表单的提交方式,通常为GET和POST方法
action:指向处理该表单信息的URL。
enctype:设置表单内容的编码模式。
GET方法是将表单内容作为参数附加到URL后面,而POST方法是将表单的信息作为数据发送到服务器后端程序上进行处理。
2.1.4.1 表单元素 表单元素由输入标记<input>,选择标记<select>,文字标记<textarea>组成。
最常用的是输入标记<input>,它的type属性可以指定输入的类型,常用的类型有:
2.1.4.2 表单示例 我们团队主页的招新页面就是一个HTML表单,除掉样式元素之后,代码是这样的:
(通常表单信息应由后端提供信息,前端动态生成,而不是在前端代码中将表单信息固定写出)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 <form method ="POST" action ="" enctype ="multipart/form-data" id ="MyForm" > <input id ="csrf_token" name ="csrf_token" type ="hidden" value ="csrf_token_value" > <div > <label class ="form-label" for ="name" > 姓名:</label > <input class ="form-control" id ="name" maxlength ="10000" name ="name" required type ="text" value ="" > <label class ="form-label" for ="stuID" > 学号:</label > <input class ="form-control" id ="stuID" name ="stuID" required type ="number" value ="" > </div > <div > <label class ="form-label" for ="college" > 学院</label > <select class ="form-select" id ="college" name ="college" required > <option value ="" > </option > <option value ="1" > 计算机科学与工程学院</option > <option value ="2" > 软件学院</option > <option value ="3" > 信息科学与工程学院</option > <option value ="4" > 生命科学与健康学院</option > <option value ="5" > 冶金学院</option > <option value ="6" > 机器人科学与工程学院</option > <option value ="7" > 工商管理学院</option > <option value ="8" > 理学院</option > <option value ="9" > 资源与土木工程学院</option > <option value ="10" > 材料科学与工程学院</option > <option value ="11" > 机械工程与自动化学院</option > <option value ="12" > 生物医学与信息工程学院</option > <option value ="13" > 文法学院</option > <option value ="14" > 马克思主义学院</option > <option value ="15" > 外国语学院</option > <option value ="16" > 艺术学院</option > <option value ="17" > 江河建筑学院学院</option > <option value ="18" > 未来技术学院</option > <option value ="19" > 体育部</option > <option value ="20" > 继续教育学院</option > <option value ="21" > 国防教育学院</option > <option value ="100" > 其他(管理员忘记列举出来了,请及时反馈)</option > </select > <label class ="form-label" for ="major" > 专业名称:</label > <input class ="form-control" id ="major" maxlength ="10000" name ="major" required type ="text" value ="" > </div > <div > <label class ="form-label" > 校区:</label > <input id ="school-0" name ="school" required type ="radio" value ="1" > <label for ="school-0" > 南湖校区</label > <input id ="school-1" name ="school" required type ="radio" value ="2" > <label for ="school-1" > 浑南校区</label > <label class ="form-label" > 年级:</label > <input id ="grade-0" name ="grade" required type ="radio" value ="1" > <label for ="grade-0" > 大一</label > <input id ="grade-1" name ="grade" required type ="radio" value ="2" > <label for ="grade-1" > 大二</label > <input id ="grade-2" name ="grade" required type ="radio" value ="3" > <label for ="grade-2" > 大三</label > <input id ="grade-3" name ="grade" required type ="radio" value ="4" > <label for ="grade-3" > 大四</label > </div > <div > <label class ="form-label" > 性别:</label > <input id ="sex-0" name ="sex" required type ="radio" value ="0" > <label for ="sex-0" > 男</label > <input id ="sex-1" name ="sex" required type ="radio" value ="1" > <label for ="sex-1" > 女</label > <label class ="form-label" > 是否需要参加培训:</label > <input id ="train-0" name ="train" required type ="radio" value ="0" > <label for ="train-0" > 不需要</label > <input id ="train-1" name ="train" required type ="radio" value ="1" > <label for ="train-1" > 需要</label > </div > <div > <label class ="form-label" for ="job" > 意向方向:</label > <select class ="form-select" id ="job" multiple name ="job" required > <option value ="1" > 硬件组</option > <option value ="2" > 软件组</option > <option value ="3" > 结构组</option > <option value ="4" > 办公室</option > <option value ="5" > 资产管理部</option > <option value ="6" > 财务部</option > <option value ="7" > 外联部</option > <option value ="8" > 媒体宣传部</option > <option value ="9" > 竞赛部</option > </select > </div > <div > <label class ="form-label" for ="email" > 邮箱:</label > <input class ="form-control" id ="email" maxlength ="10000" name ="email" required type ="email" value ="" > <label class ="form-label" for ="phone" > 电话:</label > <input class ="form-control" id ="phone" minlength ="11" name ="phone" required type ="text" value ="" > </div > <div > <label class ="form-label" for ="qq" > QQ号:</label > <input class ="form-control" id ="qq" maxlength ="10000" name ="qq" required type ="text" value ="" > <label class ="form-label" for ="file" > 上传简历附件</label > <input class ="form-control" id ="file" name ="file" required type ="file" > </div > <div > <label class ="form-label" for ="supplyText" > 补充说明:</label > <textarea class ="form-control" id ="supplyText" maxlength ="10000" name ="supplyText" > </textarea > </div > <input class ="btn btn-primary" id ="submit" name ="submit" type ="submit" value ="提交" > </form >
渲染一下的效果为:
2.2 CSS基础 CSS是Cascading Style Sheet(层叠样式表)的缩写。CSS是一种标记语言,用于为HTML文档定义布局。例如,CSS涉及字体、颜色、边距、高度、宽度、背景等方面。使用CSS可以让页面更加美观。
2.2.1 CSS语法 CSS语句由两个主要部分组成:选择器以及一条或多条声明。
选择器通常是需要改变样式的HTML元素,每条声明由一个属性和一个值组成,属性和值使用冒号分开,声明以分号结束。
比如,我们可以选择所有p元素,并将它的文本设置为红色,然后居中显示:
1 2 3 4 5 p { color :red; text-align :center; }
通常在HTML中设置CSS样式,我们并不会去改变一类元素的属性,我们可以在HTML标签中设置id和class属性,然后在CSS中对指定的id或者class设置样式。
如果我们有一个元素的id属性为id1,那么我们可以在CSS中这么指定它的样式:
如果你想对所有具有class属性为myclass的元素设置样式,那么可以这么写:
1 2 3 4 .myclass { color :red; }
2.2.2 HTML文件中嵌入CSS样式的方法 在HTML文件中嵌入CSS有3种方法:内联样式表、内部样式表和外部样式表。
内联样式表就是使用HTML的style属性,在style属性种添加CSS语句。这种方法仅印象一个元素的样式。
1 <h1 style ="text-align:center;color:green" > Hello World</h1 >
效果为:
Hello World
内部样式表指的是在HTML文件的<head>标签内部定义CSS属性,以下代码为例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Hello World</title > <style > .myClass { text-align :center; color :green; } </style > </head > <body > <h1 class ="myClass" > Hello World</h1 > </body >
效果与刚才的是一样的。
外部样式表就是将CSS语句存储到.css文件中,在HTML的头部导入这个文件。这么写的好处是方便后期维护,以及供多个HTML文件同时使用。
在硬盘上创建一个css文件,然后将代码保存到css文件中:
1 2 3 4 .myClass { text-align :center; color :green; }
然后在<head>标签中导入css文件:
1 <link rel ="stylesheet" type ="text/css" href ="test.css" >
完整代码为:
1 2 3 4 5 6 7 8 9 10 11 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Hello World</title > <link rel ="stylesheet" type ="text/css" href ="test.css" > </head > <body > <h1 class ="myClass" > Hello World</h1 > </body > </html >
运行效果:
2.3 JavaScript基础 JavaScript是一种跨平台面向对象的脚本语言,它能使网页产生交互行为,例如拥有可点击的按钮等功能。
JavaScript可以嵌入HTML代码中由客户端浏览器运行的脚本语言。在网页中使用JavaScript代码,不仅可以实现网页特效,还可以响应用户请求,实现动态交互的功能。例如,在用户注册的时候检验手机号是否正确。
2.3.1 JavaScript的Hello World 在HTML中插入JavaScript,需要使用<script>标签。比如:
1 2 3 <script > alert ("Hello World!" ); </script >
JavaScript会在页面加载的时候执行,可以在HTML文档放入多个script标签,可以放在body部分也可以放在head部分。通常的做法是把函数放在head部分,或者放在body页面的最底部。
2.3.2 JavaScript常量和变量 和其他语言一样,JS具有数字、字符串、数组等数据类型,数字可以使用算术表达式。
1 2 3 4 5 6 [1 , 2 , 3 , 4 ] 'KanbeKotori' {name :'Kotori' , friend :null } function add (a, b ) { return a+b; }
Js中使用关键字var定义变量,使用等号为变量赋值。
1 2 3 var x, length;x = 5 ; length = 6 ;
(关键字let也可以声明变量,但作用域不同。var是函数作用域,let是块级作用域。)
2.3.3 JavaScript数据类型 值类型:字符串、数字、布尔(boolean)、空(null)、未定义(undefined)。
引用类型:数组(array)、对象(object)、函数(Function)
1 2 3 4 5 var course = new Array ();course[0 ] = "Python" ; course[1 ] = "C++" ; course[3 ] = "Java" ; alert ("course: " +course+"\nlength of course: " +course.length );
JS的对象由花扩号分隔,在括号内部对象的属性以名称和值的形式出现,多个属性由逗号分隔。形式类似于Python的字典。
但是在访问对象的元素,或者给对象的属性赋值的时候,则可以像使用类对象那样使用点号引出属性。同时也可以使用Python字典式获取Js对象的值。
1 2 3 4 5 6 var person = { name :"kotori" , age :18 }; person.hobby = "save coins" ; alert ("Name: " +person.name +" Age: " +person.age +" Hobby: " +person.hobby );
2.3.4 JavaScript的控制语句 JavaScript中,使用三等号(===)代表两个值绝对等于,即值和类型均相同。对应的,使用(!==)代表不绝对等于(值和类型至少有一个不相等)。
条件语句、循环语句等跟C语言是类似的,这里只给出简单结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 if (cond){ } else if (cond2){ } else { } switch (n){ case 0 : break ; case 1 : break ; default : break ; } var arr = [1 , 2 , 3 , 4 ]for (var i=0 ;i<arr.length ;i++){ } for (x in arr){ } while (cond){ break ; } do { continue ; } while (cond);
2.3.5 JavaScript函数 函数是由事件驱动的,被调用的时候才执行相应的代码。函数使用function关键字定义,如果有形参则在括号中定义出来。
比如说,我们设置点击按钮的时候弹出Hello World:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > Hello World</title > <script > function myFun ( ) { alert ("Hello World!" ); } </script > </head > <body > <button onclick ="myFun()" > 点击</button > </body > </html >
如果函数需要返回值,那么我们就可以使用return传递返回值,然后如果有需要则由调用者接受。
2.3.6 HTML插入外部js文件 我们刚才所测试的js代码都是直接插入HTML文档中的,然而这不便于日后维护或者供其他文件使用。与css文件一样,js代码也可以定义在html外部中。
我们使用以下结构导入js代码:
1 <script src ="url" > </script >
其中url可以是本地的文件目录,也可以是其他服务器提供的url链接。
比如我们把2.3.5的示例写在外部js文件中:
2.4 前端组件库Bootstrap的简单介绍 Bootstrap是一个比较流行的前端组件库,它定义了许多开发者常用的组件样式,对于新手来说十分好入门。
Bootstrap官网:https://v5.bootcss.com/
Bootstrap网站有许多组件的介绍以及HTML代码该如何写,可以直接查找自己所需要的控件然后插入自己的HTML代码中。
Bootstarp的使用方法很简单,只需要在<head>标签内插入两行代码即可:
1 2 <link href ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel ="stylesheet" integrity ="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin ="anonymous" > <script src ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity ="sha384-/mhDoLbDldZc3qpsJHpLogda//BVZbgYuw6kof4u2FrCedxOtgRZDTHgHUhOCVim" crossorigin ="anonymous" > </script >
比如想在自己的HTML页面插入一个导航栏,我们在官网上找到样例,然后把代码copy下来:
1 2 3 4 5 6 7 8 9 10 11 <div class ="container" > <header class ="d-flex justify-content-center py-3" > <ul class ="nav nav-pills" > <li class ="nav-item" > <a href ="#" class ="nav-link active" aria-current ="page" > Home</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > Features</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > Pricing</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > FAQs</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > About</a > </li > </ul > </header > </div >
然后将这个导航栏插到body里:
之后就可以根据你的具体需要,利用你所学的HTML知识,修改导航栏的文字以及链接,实现自己的Web页面。
第三章 Flask框架基础 Flask是一个轻量级的Web应用框架。作为初学者来说,Flask具有易上手且教程资源较多的特点。Flask很容易使用,因此本团队以Flask教程来引导新手掌握Web开发的一些基本技术。
(然而可惜的是,国内的互联网企业很少去使用Python,更别提Flask框架了。如果未来要从事软件开发相关的工作,最好还是选择去学习Java或Golang的一些框架;如果并不打算从事互联网行业,仅仅是为了大创项目需要或增长自己的知识,那么选择Flask来学习是一个合适的选择。)
安装Flask的方式为:
如果你使用PyCharm等集成开发环境,可以由IDE帮你安装。
3.1 Flask的Hello World 现在我们来开始编写第一个Flask程序,首先我们先新键一个项目,习惯上我们把Flask项目的运行入口文件命名为run.py
1 2 3 4 5 6 7 8 9 10 from flask import Flaskapp = Flask(__name__) @app.route('/' ) def index (): return 'Hello World!' if __name__ == '__main__' : app.run(debug=True )
运行代码,然后我们在浏览器中输入http://127.0.0.1:5000/,运行效果如图所示:
可以看到,浏览器加载出了Hello World字符串,PyCharm终端也输出了一些访问信息。
简单介绍一下这些代码的作用:
第一行,导入Flask类
第二行,实例化了一个app,Flask的构造函数接受一个字符串,代表app的名称,我们通常直接使用模块的名称__name__。
之后有一个@开头的语句以及函数定义,使用app.route(url)的格式,告诉Flask项目,当用户访问这个url的时候,执行什么函数。url为/时代表为这个网站的根路由。
之后执行app.run()方法,启动这个Flask项目。run方法常用两个参数,debug和port。前者是bool值,代表是否开启debug模式;后者是运行端口,默认是5000端口。请注意,端口号不能和计算机内其他正在运行的程序冲突。
3.2 Flask的路由 用户通过浏览器把请求发到发送到Web服务器后,Web服务器会把请求发送给Flask程序的实例,而程序需要直到对于每一个url请求,需要执行什么样的代码,所以保存一个url到Flask项目函数的映射关系。
我们在开发的时候,会把程序运行在本地,通常使用本地环回地址http://127.0.0.1/或者localhost来测试我们的程序。如果程序的端口号不是80则需要加上冒号引出端口号。比如3.1的代码,我们需要使用http://127.0.0.1:5000/或localhost:5000/。
在代码中使用app.route(url)的格式声明路由:
1 2 3 @app.route('/about' ) def about (): return '<h1 style="color:green">Kanbe Kotori</h1>'
在浏览器中访问/about,这个url,发现我们返回的HTTP代码能够正常解析:
然而有时,我们的函数对应的url是动态变化的。例如个人中心页面的用户名是变化的、商品详情页面的商品ID是动态变化的。很显然为每一个用户个体或者商品个体单独创建一个函数是不现实的。
Flask给url添加变量部分时,可以把这些特殊的字段标记为<变量名>的形式,它就会作为参数传递到函数。如果需要限定变量的类型,则可以使用<变量类型:变量名>指定接受变量的类型。
比如我们写以下函数:
1 2 3 4 5 6 7 @app.route('/user/<userName>' ) def showUser (userName ): return "Hello, <a style='color:red'>" + userName + "</a>" @app.route('/item/<int:id>' ) def showItem (id ): return "The " + str (id ) + "th item."
打开浏览器查看效果:
而对于限制了数据类型为int的item id,如果输入非数字:
就会报告404 Not Found错误。
请注意,路由路径的变量参数要和函数的形式参数对应。
3.3 构造url和重定向 Flask可以使用url_for()函数来生成指定函数名的url,它的第一个参数是函数名,其余参数就会自动添加到url末尾作为查询参数。
而redirect()函数,则用于跳转到指定的url页面。例如,用户登录成功后需要自动跳转到主页或者个人信息页面、用户点击某一个链接后需要跳转到某个地方等等。通常redirect和url_for需要结合使用。
例如,模拟用户登录成功,然后跳转到用户信息页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import Flask, redirect, url_forapp = Flask(__name__) @app.route('/user/<userName>' ) def showUser (userName ): return "Hello, <a style='color:red'>" + userName + "</a>" @app.route('/' ) def index (): return "Hello, World!" @app.route('/login' ) def login (): return redirect(url_for('showUser' , userName='Kotori' )) if __name__ == '__main__' : app.run(debug=True )
我们会发现访问login页面直接跳转到了user页面,而且终端有一条302状态码的记录(302是永久重定向的状态码):
url_for不仅可以构造跳转到其他函数的url,还可以构造静态文件的下载链接。默认情况下,在Flask项目的目录创建一个名为static的文件夹,存在这个文件夹的所有文件都可以被用户访问到。
在代码中,我们可以使用url_for来让用户下载这个文件:
1 redirect(url_for('static' , filename="filename" ))
我们在PyCharm工程上新键一个文件夹,名字为static,之后在这里创建一个文件,名字为233.txt,里面的内容任意,然后在run.py中写上代码:
1 2 3 @app.route('/getfile' ) def getFile (): return redirect(url_for('static' , filename="233.txt" ))
之后使用浏览器访问这个路径:
(一些浏览器能够展示的文件默认是不会下载的)
3.4 加载HTML页面、渲染模板 多数情况下,用户对某个url所做的请求都是GET请求,因此加载一个美观的页面十分重要。在第二章我们已经简单学习了前端基础,接下来简单介绍一下Flask框架如何给指定url加载HTML页面。
默认情况下,Flask程序会在templates(注意有复数s)寻找html文件,Flask使用render_template()方法调用html文件并返回。
最基本的使用方法为:
1 return render_template("filename.html" )
首先我们先创建templates文件夹,然后创建index.html,之后写上我们在第二章Bootstrap介绍所学的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!doctype html > <html lang ="en" > <head > <meta charset ="utf-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1" > <title > 我的Bootstrap测试</title > <link href ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel ="stylesheet" integrity ="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin ="anonymous" > <script src ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity ="sha384-/mhDoLbDldZc3qpsJHpLogda//BVZbgYuw6kof4u2FrCedxOtgRZDTHgHUhOCVim" crossorigin ="anonymous" > </script > </head > <body > <div class ="container" > <header class ="d-flex justify-content-center py-3" > <ul class ="nav nav-pills" > <li class ="nav-item" > <a href ="#" class ="nav-link active" aria-current ="page" > Home</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > Features</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > Pricing</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > FAQs</a > </li > <li class ="nav-item" > <a href ="#" class ="nav-link" > About</a > </li > </ul > </header > </div > </body > </html >
根路由函数改为:
1 2 3 @app.route('/' ) def index (): return render_template('index.html' )
之后浏览器运行:
发现正确的加载了HTML代码。
然而多数情况下,我们的网站内容是需要动态改变的,如果仅仅只需要静态网站,那么完全可以使用Nginx来代理。
Flask提供的render_template()使用的是Jinja2模板引擎,可以支持插入一些变量的值。
在HTML文件(模板文件)中,我们可以使用{{ variable }}的形式,使用两个花扩号,来把variable对应的值渲染到此处。与此同时,在调用render_template()的时候,需要在html文件名后以key=value的形式给出所有模板文件所需要的变量的值,可以为多组,使用逗号分隔。
比如,我们在index.html文件的<body>处添加:
1 2 <h1 style ="text-align:center" > Hello, <a style ="color:red" > {{ name }}</a > </h1 > <h3 style ="text-align:center" > Your age is {{ age }}</h3 >
并将跟路由函数改为:
1 2 3 @app.route('/' ) def index (): return render_template('index.html' , name="Kotori" , age=18 )
Jinja2能够识别Python的一些复杂数据类型,比如列表、字典以及对象。
比如我们将上例改为传递一个字典:
1 2 <h1 style ="text-align:center" > Hello, <a style ="color:red" > {{ user['name'] }}</a > </h1 > <h3 style ="text-align:center" > Your age is {{ user['age'] }}</h3 >
1 2 3 @app.route('/' ) def index (): return render_template('index.html' , user={'name' :'Kotori' , 'age' :18 })
运行效果与上例一样。
3.5 模板的控制结构 在很多情况下,网站的数据是动态生成的,比如网站的目录,在不断开发中网站的目录会越来越多,网页的元素由人工添加很不方便。Jinja2模板提供了多种控制结构,可以改变模板的渲染流程。最常用的就是{% if %}``和``{% for %}``。需要注意的是,需要使用``{% endif %}和{% endfor %}作为终止标准
语法结构为:
1 2 3 4 5 6 7 {% if cond %} <p > cond为真时显示这部分内容(与Python的真值一样)</p > {% elif cond2 %} <p > cond为假且cond2为真</p > {% else %} <p > cond和cond2均为假</p > {% endif %}
1 2 3 {% for each in all %} <p > 显示{{ each }}的内容</p > {% endfor %}
当然,Jinja2模板支持多层for或if的嵌套。
下面我们给出具体的例子。在index.html文件的body部分添加:
1 2 3 4 5 6 7 8 9 10 11 12 <h1 style ="text-align:center" > Hello, <a style ="color:red" > {{ person['name'] }}</a > </h1 > <h3 style ="text-align:center" > Your age is {{ person['age'] }}</h3 > {% if person['age']<18 %} <p style ="text-align:center;color:red" > 年龄小于18岁。</p > {% else %} <p style ="text-align:center;color:green" > 年龄大于18岁。</p > {% endif %} <div > {% for userdata in userdatas %} <p style ="text-align:center" > 用户:{{ userdata['name'] }},成绩:{{ userdata['score'] }}</p > {% endfor %} </div >
1 2 3 4 5 6 7 8 9 @app.route('/' ) def index (): return render_template('index.html' , person={'name' :'Kotori' , 'age' :18 }, userdatas=[ {'name' : 'Kotori' , 'score' :100 }, {'name' : 'Fuko' , 'score' :200 }, {'name' : 'Shinoa' , 'score' :300 } ])
3.6 HTTP方法和Web表单 Flask中,所有route构造的路由默认只接受GET请求。然而有时需要用户通过表单或其他方式通过POST上传一些数据,需要给route装饰器传递methods参数,改变这一行为。
1 2 3 4 5 6 @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if request.method == 'POST' : pass else : pass
在Flask项目如果想要处理表单,则需要安装WTForms包:
有一种网站攻击方式叫做CSRF(跨站请求伪造)。跨站请求伪造(CSRF,Cross-Site Request Forgery)是一种攻击方式,攻击者诱使用户在已认证的站点上执行不想执行的操作。简而言之,攻击者通过伪造用户请求,借助用户的登录状态,向目标网站发送恶意请求,利用网站的信任执行危险操作。
举例说明,假设用户已经登录了银行网站,在一个正常的会话中具有转账权限。攻击者可以通过发送一封电子邮件或发布一个恶意网站链接,诱使用户访问一个恶意页面,该页面可能包含一个伪造的请求(如提交转账请求)。当用户访问该页面时,银行网站可能会认为这是用户自己的请求并执行转账操作,造成损失。
Flask-WTF可以保护所有表单免受CSRF攻击,为了实现CSRF保护,Flask项目需要给程序设置一个密钥,通过密钥生成加密令牌,再利用令牌验证请求表单数据的真伪,设置密钥的方式如下:
1 2 app = Flask(__name__) app.config['SECRET_KEY' ] = 'password'
加密强度取决于变量值的机密成都,不同程序需要使用不同密钥,而且要保证除网站维护者外其他人不能直到你所使用的字符串。
为了提高安全性,你可以使用python的secrets库,随机生成一个密钥:
1 2 3 import secretsapp.config['SECRET_KEY' ] = secrets.token_urlsafe(64 )
使用Flask-WTF表单时,每一个Web表单都由一个继承自FlaskForm的类表示。这个类定义了表单中的一组字段,每个字段都用对象表示。字段对象可以负数一个或多个验证函数,以验证用户提交的输入值是否符合要求。
我们可以写一个简单的登录页面:
1 2 3 4 5 6 7 8 from flask_wtf import FlaskFormfrom wtforms import StringField, SubmitField, PasswordFieldfrom wtforms.validators import DataRequired class LoginForm (FlaskForm ): name = StringField('请输入姓名:' , validators=[DataRequired()]) password = PasswordField('请输入密码:' , validators=[DataRequired()]) submit = SubmitField("提交" )
这个表单的字段都定义为类变量,类变量的值是相应字段类型的对象。
WTFForms常用的HTML标准字段:
字段类型
说明
StringField
文本字段
TextAreaField
多行文本
PasswordField
密码字段
IntegerField
只能输入数字的字段
RadioField
一组单选按钮
SelectField
下拉列表
FileField
文件上传按钮
SubmitField
提交按钮
validators除了DataRequired以外,最常用的还有一个Length方法,比如我们限制用户名和密码的长度:
1 2 3 4 5 6 class LoginForm (FlaskForm ): name = StringField('请输入姓名:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) password = PasswordField('请输入密码:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) submit = SubmitField("提交" )
validators可以直接在前端判断用户的输入是否符合要求,使用validators可以免去一些后端的判断。
定义完表单类后,我们可以在对应的路由函数内部使用,并在函数内部对用户登录的信息进行验证,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): form = LoginForm() if form.validate_on_submit(): user = form.name.data password = form.password.data if user == "Kotori" and password == "Kanbe" : return redirect(url_for('index' )) else : return render_template("login.html" , form=form, fail=True ) return render_template("login.html" , form=form, fail=False )
在login函数中,我们使用form.validate_on_submit()代替request.method == 'POST'更加方便且更安全,它会帮我们自动处理CSRF防护,避免手动管理安全令牌。
同时,我们需要把form表单对象传递给HTML模板文件,让它在前端渲染出指定的界面,login.html代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 登录</title > <link href ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel ="stylesheet" integrity ="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin ="anonymous" > <script src ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity ="sha384-/mhDoLbDldZc3qpsJHpLogda//BVZbgYuw6kof4u2FrCedxOtgRZDTHgHUhOCVim" crossorigin ="anonymous" > </script > </head > <body > <form action ="" method ="post" > {{ form.hidden_tag() }} {{ form.name.label(class="form-label") }} {{ form.name(class="form-control") }} {% if form.name.errors %} {% for error in form.name.errors %} <p style ="color:red;" > {{ error }}</p > {% endfor %} {% endif %} {{ form.password.label(class="form-label") }} {{ form.password(class="form-control") }} {% if form.password.errors %} {% for error in form.password.errors %} <p style ="color:red;" > {{ error }}</p > {% endfor %} {% endif %} {{ form.submit(class="form-control") }} </form > </body > </html >
多数控件都是固定这样的格式的:
1 2 3 4 5 6 7 {{ form.name.label(class="form-label") }} {{ form.name(class="form-control") }} {% if form.name.errors %} {% for error in form.name.errors %} <p style ="color:red;" > {{ error }}</p > {% endfor %} {% endif %}
运行效果:
只有输入正确的账户密码才会跳转:
但实际开发中,把用户密码明文保存在代码中或者数据库中是一个十分低级且危险的错误。(可以搜索 中国互联网最大规模用户资料泄露事件——CSDN用户密码泄露 来感受一下)如何提高密码的安全性详见本章3.9节。
3.6.1 Web表单实例 以下是团队主页招新页面 的供Flask使用的模板代码,其中使用了几个Bootstrap容器来实现对齐:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 <div class ="container mt-5 custom-container" > <h1 class ="text-center" > 提交你的简历</h1 > <form method ="POST" action ="" enctype ="multipart/form-data" id ="MyForm" > {{ form.hidden_tag() }} <div class ="container mt-3" > {% if TODAY>DEADLINE %} <div class ="alert alert-info alert-dismissible fade show" role ="alert" > <span style ="color: red;" > 本轮招新已经截至。</span > <br > </div > {% endif %} </div > <div class ="row" > <div class ="col mb-3" > {{ form.name.label(class="form-label") }} {{ form.name(class="form-control") }} {% if form.name.errors %} <div class ="text-danger" > {% for error in form.name.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > <div class ="col mb-3" > {{ form.stuID.label(class="form-label") }} {{ form.stuID(class="form-control") }} {% if form.stuID.errors %} <div class ="text-danger" > {% for error in form.stuID.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > </div > <div class ="row" > <div class ="col mb-3" > {{ form.college.label(class="form-label") }} {{ form.college(class="form-select") }} {% if form.college.errors %} <div class ="text-danger" > {% for error in form.college.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > <div class ="col mb-3" > {{ form.major.label(class="form-label") }} {{ form.major(class="form-control") }} {% if form.major.errors %} <div class ="text-danger" > {% for error in form.major.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > </div > <div class ="row" > <div class ="col mb-3" > {{ form.school.label(class="form-label") }} {% for subfield in form.school %} <div > {{ subfield() }} {{ subfield.label }} </div > {% endfor %} {% for error in form.school.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > <div class ="col mb-3" > {{ form.grade.label(class="form-label") }} {% for subfield in form.grade %} <div > {{ subfield() }} {{ subfield.label }} </div > {% endfor %} {% for error in form.grade.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > </div > <div class ="row" > <div class ="col mb-3" > {{ form.sex.label(class="form-label") }} {% for subfield in form.sex %} <div > {{ subfield() }} {{ subfield.label }} </div > {% endfor %} {% for error in form.sex.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > <div class ="col mb-3" > {{ form.train.label(class="form-label") }} {% for subfield in form.train %} <div > {{ subfield() }} {{ subfield.label }} </div > {% endfor %} {% for error in form.train.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > </div > <div class ="mb-3" > {{ form.job.label(class="form-label") }} {{ form.job(class="form-select", multiple=True) }} {% if form.job.errors %} <div class ="text-danger" > {% for error in form.job.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > <div class ="row" > <div class ="col mb-3" > {{ form.email.label(class="form-label") }} {{ form.email(class="form-control") }} {% if form.email.errors %} <div class ="text-danger" > {% for error in form.email.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > <div class ="col mb-3" > {{ form.phone.label(class="form-label") }} {{ form.phone(class="form-control") }} {% if form.phone.errors %} <div class ="text-danger" > {% for error in form.phone.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > </div > <div class ="row" > <div class ="col mb-3" > {{ form.qq.label(class="form-label") }} {{ form.qq(class="form-control") }} {% if form.qq.errors %} <div class ="text-danger" > {% for error in form.qq.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > <div class ="col mb-3" > {{ form.file.label(class="form-label") }} {{ form.file(class="form-control") }} {% if form.file.errors %} <div class ="text-danger" > {% for error in form.file.errors %} <span class ="error" style ="color: red;" > {{ error }}</span > <br > {% endfor %} </div > {% endif %} </div > </div > <div class ="mb-3" > {{ form.supplyText.label(class="form-label") }} {{ form.supplyText(class="form-control") }} </div > <div class ="mb-3" > {{ form.submit(class="btn btn-primary") }} </div > </form > {% if TODAY>DEADLINE %} <script > const form = document .getElementById ('MyForm' ); Array .from (form.elements ).forEach (element => { element.disabled = true ; }); </script > {% endif %} </div >
3.7 Flask请求 用户在通过请求url资源的时候,可以带一些参数,通常是以问号(?)开头,使用(key=value)的形式指定参数,比如以下url:
1 http://127.0.0.1:5000/info?name=kotori&age=18
这样的话,用户将访问http://127.0.0.1:5000/info链接,并给服务器传递两个参数:name和age,且值分别为kotori和18。
注意,用户通过url传参,值是字符串类型。
那么Flask项目如何获取用户通过GET请求传递的参数呢?Flask项目使用request.args.get(key)的方法来获取变量名为key的值。
例如,我们有以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 from flask import Flaskapp = Flask(__name__) @app.route('/info' ) def info (): name = request.args.get("name" ) age = request.args.get("age" ) return (f"<h1>Hello, <a style='color:green'>{name} </a>!</h1>" f"<h3>You are {age} years old.</h3>" ) if __name__ == '__main__' : app.run(debug=True )
我们在浏览器访问http://127.0.0.1:5000/info?name=kotori&age=18
如果用户没有写变量的值,那么会返回一个None值:
注意url中不需要对值打引号,如果打引号则引号也会被作为值的一部分:
用户使用POST提交信息的时候,我们可以使用request.args.post(key)的方法获取变量的值。对于表单,我们可以使用request.form[key]来获取值。不过在Flask项目中,我们更推荐使用前面所讲的Flask-WTF表单,因为即方便又提高了安全性。
3.8 文件上传 在使用Web表单的时候,经常会使用到上传文件的功能。我们通常使用Flask-WTF表单处理文件上传。
一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 from flask import Flask, render_templatefrom flask_wtf.file import FileFieldfrom flask_wtf import FlaskFormfrom wtforms import SubmitFieldfrom wtforms.validators import DataRequiredimport os, uuidapp = Flask(__name__) class UploadForm (FlaskForm ): file = FileField("上传文件" , validators=[DataRequired()]) submit = SubmitField("提交" ) app.config['UPLOAD_FOLDER' ] = 'upload' @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload (): os.makedirs(app.config['UPLOAD_FOLDER' ], exist_ok=True ) form = UploadForm() if form.validate_on_submit(): file = form.file.data fileName = uuid.uuid4().hex extName = file.filename.split('.' )[-1 ] fileName += "." +extName file.save(os.path.join(app.config['UPLOAD_FOLDER' ], fileName)) return "<h1>save success!</h1>" return render_template("upload.html" , form=form) if __name__ == '__main__' : app.run(debug=True )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 上传图片</title > <link href ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel ="stylesheet" integrity ="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin ="anonymous" > <script src ="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity ="sha384-/mhDoLbDldZc3qpsJHpLogda//BVZbgYuw6kof4u2FrCedxOtgRZDTHgHUhOCVim" crossorigin ="anonymous" > </script > </head > <body > <form action ="" method ="post" enctype ="multipart/form-data" > {{ form.hidden_tag() }} {{ form.file.label(class="form-label") }} {{ form.file(class="form-control") }} {% if form.file.errors %} {% for error in form.file.errors %} <p style ="color:red;" > {{ error }}</p > {% endfor %} {% endif %} {{ form.submit(class="form-control") }} </form > </body > </html >
逻辑就是使用Flask-WTF创建一个含有上传文件的表单,然后使用app.config['UPLOAD_FOLDER'] = 'upload'指定文件的上传路径,之后获取form表单的文件内容:
1 2 3 form = UploadForm() if form.validate_on_submit(): file = form.file.data
但需要注意的是,我们在保存用户上传的文件时,需要生成一个随机文件名,然后再保存,这是为什么呢?
1 2 3 4 fileName = uuid.uuid4().hex extName = file.filename.split('.' )[-1 ] fileName += "." +extName file.save(os.path.join(app.config['UPLOAD_FOLDER' ], fileName))
可以看到,我们在调用文件保存的时候,使用的是os.path.join拼接文件路径,因为Linux和Windows系统的文件路径分隔不一样。但是在两个操作系统都有一个特殊的目录:..,代表是上一级目录。举个例子,我们的Web程序跑在/home/kotori/web目录(这个是Linux目录)下,那么我们保存用户上传文件的地方就在/home/kotori/web/upload,如果某个不良用户上传了一个名为../../../../etc/passwd的文件,如果直接使用路径拼接,我们将文件保存到了/home/kotori/web/upload/../../../../etc/passwd,那么文件就实际上保存到了/etc/passwd,这个是服务器系统的root密码文件,不良用户就这么破坏了系统的密码文件。因此在处理用户上传的文件,一定要随机生成一个文件名,通常使用uuid.uuid4().hex生成,可以通过用户上传的文件获取扩展名,但不能直接使用用户上传的文件。
我们上例的效果为:
3.9 Cookie与Session 3.9.1 Cookie HTTP是无状态协议,一次请求响应结束后服务器不会留下任何关于对方状态的信息,这对于多数需要用户登录的Web来说是十分不方便的,为了解决这个问题,就有了Cookie技术。Cookie指的是Web服务器为了存储某些数据而保留在浏览器的小型文本数据。浏览器在一定时间内保存,并在下一次访问服务器的时候附带这些数据。
在Flask,可以使用response.set_cookie(key, value, [max_age, expires])的方法让浏览器保存一个Cookie数据,max_age为cookie被保存的时间,单位为秒。response则由make_response()生成。如果不设置则在关闭浏览器的时候过期。expires为具体的过期时间,通常为一个datetime对象,也可以直接赋值0代表立马过期,用于删除Cookie。
在获取某个Cookie的值的时候,使用request.cookies.get(key)的方法。
下面以用户登录为例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from flask import Flask, redirect, url_for, render_templatefrom flask_wtf import FlaskFormfrom wtforms import StringField, SubmitField, PasswordFieldfrom wtforms.validators import DataRequired, Lengthimport secretapp = Flask(__name__) app.config['SECRET_KEY' ] = secrets.token_hex(32 ) class LoginForm (FlaskForm ): name = StringField('请输入姓名:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) password = PasswordField('请输入密码:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) submit = SubmitField("提交" ) @app.route('/' ) def index (): if request.cookies.get('userName' ): return "<h1>Hello, <a style='color:green'>" + request.cookies.get('userName' ) + "</a></h1>" else : return redirect(url_for('login' )) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): form = LoginForm() if form.validate_on_submit(): user = form.name.data password = form.password.data if user == "Kotori" and password == "Kanbe" : response = make_response("<h1>登录成功!</h1>" ) response.set_cookie('userName' , user) return response else : return render_template("login.html" , form=form, fail=True ) return render_template("login.html" , form=form, fail=False ) @app.route('/logout' ) def logout (): response = make_response("<h1>退出登录!</h1>" ) response.set_cookie('userName' , "" , expires=0 ) return response
login.html文件与3.6是一样的。
在访问主页的时候,因为cookie中不存在username,所以直接302重定向到了登录页面:
这个时候我们再访问主页:
直接关闭浏览器,发现直接进入主页也需要登录:
访问logout也是一样的。
3.9.2 Session Cookie有个问题,它是保存在浏览器中的,用户完全可以手动修改。一些认证信息保留在Cookie中,很容易被不良用户恶意修改,来伪造请求。为了避免这个问题,Flask提供Session对象以对Cookie数据加密储存。
Session是指用户会话,是一种持久网络协议。在Flask中,Session对象用来加密Cookie。因此需要设置一个密钥,密钥字符串越随机越安全,我们可以使用secrets模块生成:
1 app.secret_key = secrets.token_urlsafe(64 )
使用Session对象则需要
1 from flask import session
session是一个字典对象,写入session则直接使用session[key]=value,读取session则需要使用session.get(key)的形式,删除Session则使用session.pop(key)的形式。3.9.1的代码可以改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 from flask import session, Flask, redirect, url_for, render_templatefrom flask_wtf import FlaskFormfrom wtforms import StringField, SubmitField, PasswordFieldfrom wtforms.validators import DataRequired, Lengthimport secretapp = Flask(__name__) app.config['SECRET_KEY' ] = secrets.token_hex(32 ) app.secret_key = secrets.token_hex(32 ) class LoginForm (FlaskForm ): name = StringField('请输入姓名:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) password = PasswordField('请输入密码:' , validators=[DataRequired(), Length(max =10 , min =3 ,message="长度为3到10" )]) submit = SubmitField("提交" ) @app.route('/' ) def index (): if session.get('userName' ): return "<h1>Hello, <a style='color:green'>" + session.get('userName' ) + "</a></h1>" else : return redirect(url_for('login' )) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): form = LoginForm() if form.validate_on_submit(): user = form.name.data password = form.password.data if user == "Kotori" and password == "Kanbe" : session['userName' ] = 'Kotori' return "<h1>登录成功!</h1>" else : return render_template("login.html" , form=form, fail=True ) return render_template("login.html" , form=form, fail=False ) @app.route('/logout' ) def logout (): session.pop('userName' ) return "<h1>退出登录!</h1>"
效果有3.9.1一样。
3.10 更安全的登录验证 在Web表单一节,我们说过不能直接明文存储密码,因为如果数据库被攻击者获取,所有用户的密码都会泄露。为了避免这种情况,应该采取加密或哈希(散列)方法存储密码。
一种解决办法是服务器端只保存密码的哈希值,在判断用户密码是否正确的时候只去比对两个字符串的哈希值是否相等。我们可以使用Werkzeug库中的generate_password_hash函数,它可以安全地生成密码的哈希值。generate_password_hash会根据密码生成哈希,并自动为每个密码加上一个“盐值”(salt),以增强密码存储的安全性。
在用户提交注册申请的时候,我们将用户设置的密码通过generate_password_hash生成密码的哈希值,然后只在数据库中保存哈希值;在用户登录的时候,我们使用Werkzeug库中的check_password_hash(hashed_password, user_input_password)来检验用户输入的密码是否正确。
使用哈希保存密码有许多重要的安全好处,主要是为了保护用户密码的安全,防止密码泄露带来的风险。哈希算法是单向的,也就是说,一旦密码被哈希后,无法从哈希值逆推回原始密码。因此,即使攻击者获得了存储的哈希值,他们也无法轻易恢复出原始密码。
二是,我们可以通过Session设置用户登录尝试了多少次,如果用户尝试超过了一定次数,我们可以冻结用户的操作,这样避免了暴力破解。
在这里为了方便演示,我们直接将正确密码的Hash值保存在了代码中,实际上,我们应该将这个数据保存到数据库中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 def login_attempts (): current_time = time() attempts = session.get('attempts' , 0 ) last_attempt_time = session.get('last_attempt_time' , current_time) if current_time - last_attempt_time > 60 : attempts = 0 session['attempts' ] = attempts session['last_attempt_time' ] = current_time session['last_attempt_time' ] = current_time if attempts >= 5 : return False session['attempts' ] = attempts + 1 return True PASSWORD_HASH = generate_password_hash("Kanbe" ) @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): form = LoginForm() if form.validate_on_submit(): if not login_attempts(): return '<h1 style="color:red">尝试次数过多,禁止登录!</h1>' input_password = form.password.data if check_password_hash(PASSWORD_HASH, input_password): session['userName' ] = form.name.data session['attempts' ] = 0 return redirect(url_for('index' )) else : return redirect(url_for('login' )) return render_template('login.html' , form=form)
尝试超过了五次:
用户输入了正确的密码: