01_mini-web
本章节学习目标
- 能够知道Web开发流程
- 能够知道使用web应用程序处理客户端的动态资源请求操作
- 能够知道路由列表的实现
- 能够知道装饰器方式的添加路由
- 能够知道logging日志的使用
本章节常用单词
- request 英 [rɪ'kwest] 请求;需要
- response 英 [rɪ'spɒns] 响应;反应;
- route 英 [ruːt] 按某路线发送 ; 路由
- template 英 ['templeɪt; -plɪt] 模板,样板
- source 英 [sɔːs] 来源;水源;
1.1 HTTP 协议
学习目标:
- 常握 HTTP 协议中 URL 地址、请求报文和响应报文的基本格式
1.1.1 HTTP 协议简介
Web应用程序开发主要是建立在 B/S 架构模式下,即通常基于浏览器作为客户端通过URL去请求访问Web服务端,Web服务端处理对应请求之后,给客户端返回响应数据,客户端根据响应数据进行页面的解析和数据的展示。
在Web客户端和服务端通信的过程中,其数据的传输和响应遵循HTTP协议。
HTTP协议是 Hyper Text Transfer Protocol【超文本传输协议】的缩写,是用于从万维网【WWW:World Wide Web 】服务器传输超文本到本地浏览器的传送协议。
HTTP是一个属于应用层的协议,于1990年被提出,基于TCP/IP通信协议来传递数据(HTML 文件、图片文件、查询结果等)。
(1) HTTP协议之URL
Web应用程序中,客户端需要通过URL来向Web服务器发送请求,下面我们来看一个完整的URL地址:
http://movie.douban.com:80/annual/2019?source=navigation
对于上面的这个URL地址,其主要分为以下几个部分:
URL部分 | 说明 |
---|---|
http | http是协议部分,表示遵循http协议的请求,常见的协议还有:https |
movie.douban.com | 域名部分,域名和IP是对应的,可以换成对应服务器的IP,只不过域名 更容易记忆 |
80 | 端口部分,可以省略,默认就是访问80端口,如果是https协议,默认 访问443端口 |
/annual/2019 | 资源路径部分,表明要请求的是服务主机上的什么资源 |
?source=navigation | 查询参数部分,传递一些请求参数,也叫查询字符串数据 |
(2) HTTP协议之请求报文
浏览器在发送http请求的过程中,其实给对应Web服务端发送的是HTTP请求报文,其具体协议格式如下:
下面是一个客户端请求时发送的HTTP请求报文:
GET /annual/2019?source=navigation HTTP/1.1\r\n
Host: movie.douban.com
Connection: keep-alive\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6)\r\n
Accept: text/html,application/xhtml+xml,application/xml\r\n
\r\n
一个HTTP请求报文分为以下4个部分:
-
请求行:用来说明请求方式、要访问的资源地址以及所使用的HTTP版本
GET /annual/2019?source=navigation HTTP/1.1\r\n
-
请求头:紧接着请求行之后的部分,用来说明服务器要使用的附加信息
Connection: keep-alive\r\n Pragma: no-cache\r\n Cache-Control: no-cache\r\n Upgrade-Insecure-Requests: 1\r\n User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6)\r\n Accept: text/html,application/xhtml+xml,application/xml\r\n
-
空行:请求头部后面的空行是必须的,即使第四部分的请求数据为空,也必须有空行
\r\n
-
请求体:可选部分,请求时所要向服务端传递的数据,上面的例子中没有请求数据
(3) HTTP协议之响应报文
Web服务端在处理完浏览器的请求之后,给浏览器返回的是HTTP响应报文,其具体协议格式如下:
下面是一个web服务端响应时的HTTP响应报文:
HTTP/1.1 200 OK\r\n
Date: Mon, 14 Sep 2020 07:24:14 GMT\r\n
Content-Type: text/html; charset=utf-8\r\n
Transfer-Encoding: chunked\r\n
Connection: keep-alive\r\n
Keep-Alive: timeout=30\r\n
Vary: Accept-Encoding\r\n
Content-Encoding: gzip\r\n
\r\n
<html>
<head>
...
</head>
<body>
...
</body>
</html>
一个HTTP响应报文分为以下4个部分:
-
响应行:主要包括使用的HTTP协议版本、响应状态码和状态码描述
HTTP/1.1 200 OK\r\n
-
响应头:紧接着响应行之后的部分,用来说明给客户端返回的一些附加信息
Date: Mon, 14 Sep 2020 07:24:14 GMT\r\n Content-Type: text/html; charset=utf-8\r\n Transfer-Encoding: chunked\r\n Connection: keep-alive\r\n Keep-Alive: timeout=30\r\n Vary: Accept-Encoding\r\n Content-Encoding: gzip\r\n
-
空行:响应头部后面的空行是必须的,即使第四部分的响应体数据为空,也必须有空行
\r\n
-
响应体:可选部分,web服务端给客户端返回的响应主体数据
<html> <head> ... </head> <body> ... </body> </html>
1.2 查看HTTP协议的通信过程
学习目标
- 能够使用谷歌浏览器的开发者工具查看HTTP协议的通信过程
1.2.1 谷歌浏览器开发者工具的使用
首先需要安装Google Chrome浏览器,然后Windows和Linux平台按F12调出开发者工具, mac OS选择 视图 -> 开发者 -> 开发者工具或者直接使用 alt+command+i 这个快捷键,还有一个多平台通用的操作就是在网页右击选择检查。
开发者工具的效果图:
开发者工具的标签选项说明:
- 元素(Elements):用于查看或修改HTML标签
- 控制台(Console):执行js代码
- 源代码(Sources):查看静态资源文件,断点调试JS代码
- 网络(Network):查看http协议的通信过程
开发者工具使用效果图:
开发者工具的使用说明:
- 点击Network标签选项
- 在浏览器的地址栏输入百度的网址,就能看到请求百度首页的http的通信过程
- 这里的每项记录都是请求+响应的一次过程
1.2.2 查看HTTP协议的通信过程
查看http请求信息效果图:
查看http响应信息效果图:
1.3 Web后端开发概述
学习目标:
- 知道 Web 服务器的作用,了解静态 Web 请求和动态 Web 请求
Web服务器
Web应用程序开发完成之后,如果项目想要运行起来,需要一个Web服务器才可以。所谓的Web服务器,其实就是一个能够解析HTTP请求报文,并且能够根据客户端的请求调用开发好的应用程序,最终组织HTTP响应报文的服务器。
对于客户端发送的HTTP请求来说,一般可以分为:静态web请求和动态web请求。
静态Web请求:客户端请求的内容是固定不变的,比如客户端请求一个固定的html页面、js文件或css文件,对于这类请求,Web服务器只需要读取对应文件的内容,组织响应报文返回即可。
动态Web请求:客户端请求的内容是动态变化的,比如客户端请求某个时段的热门新闻数据,这样的请求涉及到数据的动态查询,Web服务器需要调用动态web程序,动态web程序可能需要查询数据库,获取对应的数据,然后组织响应报文进行返回。
在Web应用程序的开发过程中,Web服务器是不需要我们开发的,Web框架基本都会提供测试的Web服务器供我们快速的将开发的Web项目运行起来。
1.4 搭建Python自带静态Web服务器
学习目标
- 能够知道搭建Python自带Web服务器
1.4.1 静态Web服务器是什么?
可以为发出请求的浏览器提供静态文件的程序。
1.4.2 如何搭建Python自带的静态Web服务器
搭建Python自带的静态Web服务器使用:
-
进入前端资源所在的文件夹(需改为自己的路径, 下面只是示例)
# 如果资源文件夹在C盘, 直接cd进文件夹 cd C:\Users\smart\Desktop\front> # 如果资源文件夹不在C盘, 使用cd时需要增加/d参数 cd /d E:\front
-
使用Python命令启动静态服务器(端口可选,不写默认是8000)
# Win系统 python -m http.server 端口号 # Mac系统(Mac系统自带python2, 因此需要指令来区分python版本) python3 -m http.server 端口号
-m选项说明:
-m表示运行包里面的模块,执行这个命令的时候,需要进入你自己指定静态文件的目录,然后通过浏览器就能访问对应的
html文件了,这样一个静态的web服务器就搭建好了。
1.4.3 访问搭建的静态Web服务器
通过浏览器访问搭建的静态Web服务器,效果图如下:
1.4.4 查看浏览器和搭建的静态Web服务器的通信过程
查看http的通信过程,效果图如下:
1.5 Web应用开发快速入门
学习目标
- 能够知道使用web应用程序处理客户端的动态资源请求操作
1.5.1 Web应用职责介绍
- 接收web服务器的动态资源请求,给web服务器提供处理动态资源请求的服务。
1.5.2 Web开发框架
Web 开发框架是用来进行 Web 应用开发的一个软件架构,主要用于动态 Web 应用程序的开发。各种编程语言中都会提供一些 Web 开发框架供我们使用,比如 Python 语言中的Django、Flask,Java语言中的 Spring 等。
具体来说,Web 开发框架在 Web 应用程序的开发中,主要有如下作用:
- 封装了开发中的一些重复性的工作,比如:Web 项目的搭建和一些基础的组件
- 降低开发难度,节省开发时间,提高应用程序的开发效率
- 总结一句话:避免重复造轮子,提高开发质量
每个 Web 框架都制定了自己的一些开发规则,学习 Web 框架,主要就是学习每个框架制定的规则。按照框架的规则去操作,就能很快的上手这个框架。等到对某个框架非常精通之后,我们甚至可以修改框架的规则,做一些定制化的操作,让使用框架的开发更加贴合实际的业务。
1.5.3 Flask框架
-
Flask诞生于2010年,是Armin ronacher(人名)用 Python 语言基于 Werkzeug 工具箱编写的轻量级Web开发框架。
-
Flask 本身相当于一个内核,其他几乎所有的功能都要用到扩展(邮件扩展Flask-Mail,用户认证Flask-Login,数据库Flask-SQLAlchemy),都需要用第三方的扩展来实现。比如可以用 Flask 扩展加入ORM、窗体验证工具,文件上传、身份验证等。Flask 没有默认使用的数据库,你可以选择 MySQL,也可以用 NoSQL。
-
Flask安装: 在PyCharm的Terminal中输入下面命令
pip install flask
1.5.4 HelloWorld程序
1)新建文件 main.py 文件,编写代码如下:
# 导入 Flask 类
from flask import Flask
# 创建 Flask 类的实例对象
app = Flask(__name__)
# 定义处理函数并设置URL地址
# @app.route的作用是将路由映射到视图函数 index
@app.route('/index')
def index():
return 'hello world!'
if __name__ == '__main__':
# 调试模式启动 flask 提供的 web 服务器(调试模式可以在更改代码后自动重新运行)
app.run(debug=True)
2)运行 main.py 文件,效果如下:
3)通过浏览器访问 http://127.0.0.1:5000/index 地址,页面如下:
什么是路由?
路由就是请求的URL到处理函数的映射,也就是说提前把请求的URL和处理函数关联好。
路由列表
这么多的路由如何管理呢, 可以使用一个路由列表进行管理,通过路由列表保存每一个路由。
请求路径 | 处理函数 |
---|---|
/login | login函数 |
/index | index函数 |
/detail | detail函数 |
1.6 首页数据加载
学习目标
- 能够参考课件实现首页图书数据的显示
1.6.1 图书 SQL 数据准备
添加数据库文件 ( 请确保数据库名和命令里的名字一致 )
1)在 cmd 窗口进入到 bookinfo.sql 文件所在的文件夹
2)登录 mysql 数据库,执行如下命令创建数据库
create database booksite charset=utf8;
3)创建好数据库后 exit 退出 mysql,然后再 cmd 窗口执行如下命令:
mysql -uroot -p booksite < bookinfo.sql
4)再次登录数据库, 查看是否有对应数据
1.6.2 首页图书数据的返回
1)先下载安装, 安装方式同 Flask
pip install pymysql
2)使用 pymysql 连接数据库, 获取图书数据并返回 JSON
import pymysql
import json
# 导入 Flask 类
from flask import Flask
from flask_cors import CORS
# 创建 Flask 类的实例对象
app = Flask(__name__)
# 设置后端服务器允许任何地址前端进行访问
CORS(app, origins='*')
# 定义处理函数并设置URL地址
@app.route('/index')
def index():
"""使用 pymysql 加载首页图书数据"""
# 1. 创建数据库连接对象
conn = pymysql.connect(host='127.0.0.1', port=3306,
database='booksite', user='root', password='mysql')
# 2. 创建游标对象
cursor = conn.cursor()
# 3. 准备查询图书数据的 SQL 语句
sql = 'select * from bookinfo;'
# 4. 通过游标对象执行 SQL 语句
cursor.execute(sql)
# 5. 获取执行 SQL 返回的所有数据
result = cursor.fetchall()
print(result)
# 6. 关闭游标对象和连接对象
cursor.close()
conn.close()
# 7. 将图书数据转换为列表套字典的形式
data_list = []
for data in result:
data_list.append({
'id': data[0],
'name': data[1],
'auth': data[2],
'img_url': data[3],
'rank': data[4]
})
print(data_list)
# 8. 将列表套字典数据转换为 JSON 字符串返回
json_str = json.dumps(data_list, ensure_ascii=False)
return json_str
1.6.3 处理跨域请求问题(了解)
-
考虑到安全性,浏览器是不允许跨域(不同地址)访问的。默认只有IP和端口完全一致时,才允许访问。由于咱们的静态服务器和动态服务器不在一个端口上,因此会涉及跨域访问问题。暂时的办法是设置允许跨域访问
-
下载flask_cors
pip install flask-cors
-
设置允许跨域访问
import json import pymysql from flask import Flask from flask_cors import CORS app = Flask(__name__) # 设置后端服务器允许任何地址前端进行访问 CORS(app, origins='*')
1.6.4 在前端 index.js 文件中,发送 ajax 请求获取数据进行渲染
- 确保url请求的端口号是Flask项目运行的端口号
- 确保url请求的路径index和Flask项目的路由地址一致
// 首页JS文件路径: front/js/index.js
$.get('http://127.0.0.1:5000/index',function(data,status){具体代码略}
1.7 详情数据数据
学习目标
- 能够参考课件实现详情页面数据的显示
1.7.1 指定图书数据返回
1)使用pymysql连接数据库,获取指定图书数据并返回 JSON
# 定义获取图书详情信息的处理函数
@app.route('/detail')
def detail():
"""使用 pymysql 加载图书详情数据"""
# 1. 获取指定的图书id
book_id = request.args.get('id', 1) # 获取不到 id 参数时,get函数默认返回1
# 2. 根据 id 查询数据库,获取对应图书的信息
# 创建数据库连接对象
conn = pymysql.connect(host='127.0.0.1', port=3306,
database='booksite', user='root', password='mysql')
# 创建游标对象
cursor = conn.cursor()
# 准备查询的 SQL 语句
sql = f'select * from bookinfo where id={book_id}'
# 通过游标对象执行 SQL 语句
cursor.execute(sql)
# 获取执行 SQL 结果中的一条数据
result = cursor.fetchone() # 注意:结果是元组
# 3. 将图书数据转换为字典的形式
data_dict = {
'id': result[0],
'name': result[1],
'auth': result[2],
'img_url': result[3],
'read': result[5],
'comment': result[6],
'score': result[8],
'content': result[7],
'synopsis': result[9]
}
print(data_dict)
# 4. 更新指定图书的阅读量
new_read = result[5] + 1
sql = f'update bookinfo set bread={new_read} where id={book_id}'
cursor.execute(sql)
conn.commit()
# 5. 关闭游标对象和数据库连接
cursor.close()
conn.close()
# 6. 将指定图书数据转化为JSON字符串并返回
json_str = json.dumps(data_dict, ensure_ascii=False)
return json_str
1.7.2 在前端 detail.js 文件中,发送 ajax 请求获取数据进行渲染
- 确保url请求的端口号是Flask项目运行的端口号
- 确保url请求的路径detail和Flask项目的路由地址一致
// 详情JS文件路径: front/js/detail.js
url = "http://127.0.0.1:5000/detail?"+str
1.8 logging日志
学习目标
- 能够知道logging日志的使用
1.8.1 logging日志的介绍
在现实生活中,记录日志非常重要,比如:银行转账时会有转账记录;飞机飞行过程中,会有个黑盒子(飞行数据记录器)记录着飞机的飞行过程,那在咱们 python 程序中想要记录程序在运行时所产生的信息,怎么做呢?
可以使用 logging 这个包来完成,logging 是 python 内置的一个用来记录程序运行日志的包,可以用来记录程序运行过程中产生的信息
记录程序日志信息的目的是:
(1)可以很方便的了解程序的运行情况
(2)可以分析用户的操作行为、喜好等信息
(3)方便开发人员检查bug
1.8.2 logging日志级别介绍
日志等级可以分为5个,从低到高分别是:
(1)DEBUG
(2)INFO
(3)WARNING
(4)ERROR
(5)CRITICAL
日志等级说明:
- DEBUG:程序调试bug时使用
- INFO:程序正常运行时使用
- WARNING:程序未按预期运行时使用,但并不是错误,如:用户登录密码错误
- ERROR:程序出错误时使用,如:IO操作失败
- CRITICAL:特别严重的问题,导致程序不能再继续运行时使用,如:磁盘空间为空,一般很少使用
- 默认的是WARNING等级,当在WARNING或WARNING之上等级的才记录日志信息
- 日志等级从低到高的顺序是: DEBUG < INFO < WARNING < ERROR < CRITICAL
1.8.3 logging日志的使用
在 logging 包中记录日志的方式有两种:
- 输出到控制台
- 保存到日志文件
日志信息输出到控制台的示例代码:
import logging
logging.debug('这是一个debug级别的日志信息')
logging.info('这是一个info级别的日志信息')
logging.warning('这是一个warning级别的日志信息')
logging.error('这是一个error级别的日志信息')
logging.critical('这是一个critical级别的日志信息')
运行结果:
WARNING:root:这是一个warning级别的日志信息
ERROR:root:这是一个error级别的日志信息
CRITICAL:root:这是一个critical级别的日志信息
说明:
- 日志信息只显示了大于等于WARNING级别的日志,这说明默认的日志级别设置为WARNING
logging日志等级和输出格式的设置:
import logging
# 设置日志等级和输出日志格式
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
logging.debug('这是一个debug级别的日志信息')
logging.info('这是一个info级别的日志信息')
logging.warning('这是一个warning级别的日志信息')
logging.error('这是一个error级别的日志信息')
logging.critical('这是一个critical级别的日志信息')
运行结果:
2021-08-28 17:06:32,605 - 01-logging日志记录.py[line:7] - DEBUG: 这是一个debug级别的日志信息
2021-08-28 17:06:32,605 - 01-logging日志记录.py[line:8] - INFO: 这是一个info级别的日志信息
2021-08-28 17:06:32,605 - 01-logging日志记录.py[line:9] - WARNING: 这是一个warning级别的日志信息
2021-08-28 17:06:32,605 - 01-logging日志记录.py[line:10] - ERROR: 这是一个error级别的日志信息
2021-08-28 17:06:32,605 - 01-logging日志记录.py[line:11] - CRITICAL: 这是一个critical级别的日志信息
代码说明:
- level 表示设置的日志等级
- format 表示日志的输出格式, 参数说明:
- %(levelname)s: 打印日志级别名称
- %(filename)s: 打印当前执行程序名
- %(lineno)d: 打印日志的当前行号
- %(asctime)s: 打印日志的时间
- %(message)s: 打印日志信息
日志信息保存到日志文件的示例代码:
import logging
# 设置日志级别和日志格式
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s',
filename='log.txt',
filemode='a')
logging.debug('这是一个debug级别的日志信息')
logging.info('这是一个info级别的日志信息')
logging.warning('这是一个warning级别的日志信息')
logging.error('这是一个error级别的日志信息')
logging.critical('这是一个critical级别的日志信息')
运行结果:
02_数据埋点
本章节学习目标
- 能够知道数据埋点的作用
- 能够知道数据埋点方案流程
- 能够知道数据埋点形式
- 能够知道埋点数据字段选择规范
本章节常用单词
- document英 [ˈdɒkjumənt , ˈdɒkjument]文件;公文;文献;
- requirements 英 [rɪˈkwaɪəmənts] 必要条件;必备的条件
- access英 [ˈækses] 访问,存取(计算机文件);
- product 英 [ˈprɒdʌkt] 产品;制品;
- designer英 [dɪˈzaɪnə(r)]设计者;构思者
- business 英 [ˈbɪznəs] 商业;商务;
- Intelligence英 [ɪnˈtelɪdʒəns] 智力;才智;智慧;
2.1 埋点介绍
在实际的业务问题中,难免会遇到这样的问题:分析时缺少关键的数据指标,或数据指标的维度和属性满足不了分析需求,这个时候你该怎么办呢?这时我们就要通过数据埋点获取我们想要的数据内容。
埋点是数据采集的专用术语,在数据驱动型业务中,如营销策略、产品迭代、业务分析、用户画像等,都依赖于数据提供决策支持,希望通过数据来捕捉特定的用户行为,如按钮点击量、阅读时长等统计信息。因此,数据埋点可以简单理解为:针对特定业务场景进行数据采集和上报的技术方案。
现在公司通常都会有数据产品经理或业务线数据分析师,结合版本迭代过程进行埋点规划。如果是代码埋点,还需要开发人员完成相应的埋点代码。
2.2 埋点形式
学习目标:
- 了解数据埋点的 3 种方式:代码埋点、可视化埋点、无埋点(全埋点)
2.2.1 代码埋点
为监测网站上/app上用户的行为,是需要在网页/app中加上一些代码的,当用户触发相应行为时,进行数据上报,也就是代码埋点。
- 优点:可以详细的设置某一个事件自定义属性;
- 缺点:时间、人力成本大,数据传输的时效性。
2.2.2 可视化埋点
利用可视化交互手段,数据产品/数据分析师可以通过可视化界面(管理后台连接设备) 配置事件,可视化埋点仍需要先配置相关事件,再采集。
- 优点:埋点只需业务人员接入,无需开发人员支持;
- 缺点:仅支持客户端(APP)行为。
2.2.3 无埋点(全埋点)
无埋点也被叫做全埋点,是指开发人员集成采集 SDK 后,SDK 便直接开始捕捉和监测用户在应用里的所有行为,并全部上报,不需要开发人员添加额外代码。数据分析师/数据产品 通过管理后台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后就可以结合时间属性、用户属性、事件进行分析了。所以无埋点并不是真的不用埋点了。
- 优点:无需开发,业务人员埋点即可;支持先上报数据,后进行埋点。
- 缺点:数据存储量大
无埋点和可视化埋点均不需要开发支持,仅数据业务人员进行设置即可。但两者数据上报埋点设置存在加大的差异:无埋点是无差别采集所有用户数据,可视化埋点是根据配置的要采集数据信息进行数据收集,因而无埋点上报的数据量远大于可视化埋点
2.3 代码埋点方案
学习目标:
- 了解代码埋点的流程规范
2.3.1 代码埋点的方式
在进行可视化埋点和无埋点(全埋点)是可以借助三方平台方案进行实现,如神策、友盟、腾讯移动分析、Talkingdata,GrowingIO等。如果对数据安全比较重视,业务又相对复杂,公司则通常是使用代码埋点方式采集数据,并搭建相应的数据产品实现其数据应用或是分析的诉求。
在埋点的技术方案中,首先要重视的,是用户唯一标识的建设。如果做不到对用户的唯一识别,那么基础的UV统计,都将是错误的。因此,在数据埋点方案中,有两个信息是一定要记录的,即设备ID+用户ID。
- 设备ID:代表用户使用哪个设备,如安卓的ANDROID_ID/IMEI,IOS中的IDFA/UDID,浏览器的Cookie,小程序的OpenID等。
- 用户ID:代表用户在产品中所注册的账号,通常是手机号,也可以是邮箱等其他格式。
- 当这两个信息能够获得时,不论是用户更换设备,或者是同一台设备不同账号登录,我们都能够根据这两个ID,来识别出谁在对设备做操作。
在Web开发过程中,Web端数据采集主要通过三种方式实现:服务器日志、URL解析及JS回传。
- 服务器日志:指Web服务器软件,例如Httpd、Nginx、Tomcat等自带的日志,例如Nginx的access.log日志
- URL解析:指访问服务器时,将URL信息及携带的参数进行解析后,上传服务器,例如在进行数据搜索时
http://www.meiduo.site?search=iphone
,我们可以获得本次用户搜索的内容search为"iphone"; - JS回传:指在Web页面上添加的各类统计插件,通过在页面嵌入自定义的Javascript代码来获取用户的访问行为(比如鼠标悬停的位置,点击的事件等),然后通过Ajax请求到后台记录日志。
这些数据的获取需要前端开发人员和后端开发人员配合,通过代码的形式采集到对应数据内容,还用一些特定数据需要单独在前端页面进行采集,页面浏览量,页面点击量等
2.3.2 埋点流程规范
流程规范会分成五个步骤:即规划评审、技术开发、埋点验证、发布上线、监测评估。
1)规划评审:
数据埋点的方案一旦确定,返工和排查问题的成本都很高,但数据埋点之后的分析工作,又涉及到了PD(product designer)、BI(Business Intelligence)、算法、数据等多个角色。因此非常有必要,将需求内容和数据口径统一,所有人在一套标准下,将需求定义出来,明确我们的分析的场景和目标,随后业务侧再介入,进行埋点方案的设计和开发。
通常情况下,我们需要记录用户在使用产品过程中的操作行为,通过4W1H模型可以比较好的保障信息是完备的。4W1H包括:
-
Who(谁):设备ID、用户ID、手机号、微信识别码等;
- When(在什么时间):记录日志的时间戳、日志上报的时间戳;
- Where(在什么位置):IP地址、GPS地址在哪;
- How(以什么方式):操作系统、设备型号、网络环境、APP版本、当前页面等信息;
-
What(做了什么事情):如果是搜索行为,则记录关联词;如果是内容点击,则记录内容ID、内容类型、列表位置;如果是交易动作,记录交易的商品ID、类型、数量;如果是支付过程,记录付款的方式与付款金额。
我们统计时,按照上述约定,统计用户在某个时间和地点中,看到了哪些信息,并完成了怎样的动作。上下游的相关人员,在使用这份数据时,产生的歧义或者是分歧会小很多。
2)技术开发:
当需求确定后,我们就可以开始开发动作了,这里基本上是对研发进行约束。埋点的开发,简单讲,是分成行为埋点(用户浏览什么内容、浏览时间)和事件(视频点击次数、视频下载次数)埋点两个大类,每一类根据端的不同进行相应的开发。
3)埋点验证:
埋点的验证很关键,如果上线后才发现问题,那么历史数据是无法追溯的。
验证有两种方式,一种是实时的功能验证,一种是离线的日志验证。
-
实时功能验证:指功能开发好后,在灰度环境上测试相应的埋点功能是否正常,比如点击相应的业务模块,日志是否会正确的打印出来。通常而言,我们需要验证如下三个类型的问题:
1)记录正确:APP发生相应的动作,检查日志是否打印正确,如:打开页面(行为埋点)、点击按钮(事件埋点)时,是否日志会记录;
2)位置正确:查看SPM、SCM码与平台申请的是否一致;
3)内容正确:设备ID、用户ID等必须记录的内容是否正确,行为、事件记录内容是否与页面实际发生的一致。
-
离线的日志验证:我们需要把日志写到测试环境中,查看数据上报的过程是否正确,以及对上报后的数据进行统计,侧面验证记录的准确性,如统计基本的PV、UV,行为、事件的发生数量。
很多时候,数据是需要多方验证的,存在一定的上下游信息不同步问题,比如对某个默认值的定义有歧义,日志统计会有效的发现这类问题。
4)发布上线:
应用的发布上线通常会有不同的周期,例如移动端会有统一的发版时间,而网页版只需要根据自己的节奏走,因此数据开始统计的时间是不同的。最后,应用应当对所有已发布的埋点数据,有统一的管理方法。
5)监测评估:
大多数时候,数据埋点的技术方案,只需要设计一次,但数据准确性的验证,却需要随着产品的生命周期持续下去,对于埋点收集的数据进行持续监测,评估数据质量如何,发现问题后提出优化方案,比如,是否在设计上改进,或者是否是工程上的 bug。确定方案的责任人来优化方案内容,在下一轮迭代中推进实施。
2.4 埋点实践
学习目标:
- 能够结合 mini-web 案例进行数据埋点操作
2.4.1 埋点的两大问题
通过埋点方案的学习,我们知道整个埋点实施的过程,在规划评审阶段,数据分析人员需要进行埋点数据设计,在进行设计过程中会遇到两大问题:
- 埋点数据:要什么样的数据,有什么字段属性
- 埋点位置:前端埋点? 后端埋点?
为了解决以上问题,分析人员需要编写一份数据需求文档:DRD(Data Requirements Document),通过这份规范的文档,我们便能够地与研发工程师进行沟通。
埋点数据
以短视频业务为例:
需求:分析视频的受欢迎程度
从视频显示内容中我们能直观获取到的字段属性数据为:
-
点赞量:2.3w
-
评论量:1921
- 转发量:7054
这些数据对应了不同的事件(用户行为)内容,我们在设计数据需求文档时要添加上这些字段属性
id | 事件(用户行为) | 数量 | ...... |
---|---|---|---|
1 | likes (点赞) | 2.3w | ...... |
2 | comment (评论) | 1921 | ...... |
3 | forward (转发) | 7054 | ...... |
4 | ...... | ...... | ...... |
单纯的从这些事件中,我们并不能有效的分析出视频的受欢迎程度。我们只是大概的从数据里知道点赞很高,这并不能反应视频受欢迎,有可能是用户误操作,也有可能是刷赞程序没有观看视频直接请求的点赞接口。
这时我们为了保证数据的有效性,就需要通过一些指标来真正的反应出我们视频的受欢迎程度,如点赞率(点赞人数/实际观看人数),转发率(转发量/实际观看人数)。
在计算的数据中我们就缺失一个关键的时间观看人数这个条数据,那我们就需要在数据需求文档中增加上这一条数据内容,同时要求开发人员增加统计实际观看人数的代码。
id | 事件(用户行为) | 数量 | ...... |
---|---|---|---|
1 | likes (点赞) | 2.3w | ...... |
2 | comment (评论) | 1921 | ...... |
3 | forward (转发) | 7054 | ...... |
4 | count(观看人数) | ??? | ...... |
...... | ...... | ...... | ...... |
在 受欢迎程度 的这需求中,我们还可以衍生出很多子需求,如:
-
哪些年龄段的用户喜欢看我的视频?
-
哪些地区的用户喜欢看我的视频?
要满足这些需求,我们就需要在原来的字段属性基础上再增加用户的年龄信息,用户所在位置信息等等
通过观察分析这样一组相关联的数据,我们可以对我们的用户群体进行精准定位,拍摄出用户喜欢的视频内容。
我们除了要根据需求选择的一组关联数据字段属性外,我们还有要对数据进行持续追踪,这就需要再增加一个时间维度的数据,比如通过观察半年的视频情况,发现在某个时间段的视频观看量有所下滑,那我们就可以定位到这一时间段的视频,分析这一时间内的视频为什么会出现下滑。
通过我们对数据字段属性的不断丰富 ,我们能够得到更加完整建立起用户画像。
由此我们也可以看出,数据分析的本质就是通过观察一组或多组关联数据连续变化的情况,分析出数据变化的原因和可能性。
以上的字段属性选取过程是凭经验选择埋点数据 ,以需求划分字段属性,这对于分析人员有以下要求
- 需要有相当的行业经验,能很快根据需求选定一组关联字段属性
- 在新需求出现时,频繁添加埋点字段属性,需要不断和开发人员沟通
作为一个小白,在没有行业经验的的加持我们该如何去进行字段属性的选取呢?
答案是:4W1H规范
WHO(什么人),When(在什么时间),Where(在什么地点),How(以什么方式),What(干了什么事情)
我们可以将所有事件按照对应的信息进行字段属性提取,然后然后根据需求将字段属性进行聚类划分,如前面提到的点赞、转发、评论这三个属性都是与视频受欢迎程度相关的,那我们就可以将它们放在一起。同时我们也可以将共用的字段属性部分进行抽取作为公共属性,如:Who(什么人),这就是前面提到用户唯一标识,用来确认用户身份,这是我们在进行需求分析时都会用到的字段数属性,我们就可以将该字段单独拿出作为公共属性交给某一个开发人员统一维护,这样我们就不必每次需求分析时都找开发去获取该字段属性
根据4W1H规范我们可以清晰的看到用户的行为路径,方便了我们进行数据字段属性的选取,做到了:
- 字段属性选择有迹可循
- 可以进行属性聚类划分和公共属性的抽取
埋点位置
前面我们知道了如何选取数据字段属性,那选取后的字段属性该交给前端还是后端开发人员进行埋点呢,这是我们需要遵循的原则就是:数据只能从前端获取的前端进行埋点,其它数据建议全部后端埋点
什么是只能前端获取的数据呢?比如用户在一个页面停留的时间,用户观看视频的时长,用户观看视频的次数等等,后端将视频数据返回前端了,那用户到底对这个视频看了多长时间及观看次数,只能是前端通过控制手机上的播放器插件才能获取到这个数据内容,而后端是无法获取这个数据内容。
除了这些只能是前端获取的数据外,其它数据都建议后端埋点。
在前端进行数据埋点存在如下缺陷:
1)数据实时性差
我们开发的手机端APP在进行数据采集时,如果频繁往后端发送请求,就会造成手机资源(内存,CPU)被严重消耗,影响手机性能。这时为了避免手机资源被过度消耗,前端APP一遍采用的策略是将数据统一收集后定时发送到后端,比如用户观看了10个视频,不在是每个视频观看时都发送数据,而是将用户观看的10个视频数据统一收集之后再在规定的时间点发送到后端,这样原来需要发送10个请求现在只需要发送1次。但是这样做的就会造成我们获取的数据是滞后的,同时还可能会因为前端代码的原因造成统一收集后每个视频观看的时间戳是一样的,影响我们分析结果的准确性。
不考虑性能,用户使用卡顿,体验差;注重性能,获取到数据可能会存在误差。鱼和熊掌要看自己公司的选择。
2)无法确认what完成情况
以订单支付为例,如果我们在前端进行支付数据获取,用户点击了支付按钮就算用户完成了支付,这样统计的数据是错误的。整个支付流程是在后端完成的,只有后端代码执行完成够才能知道支付是成功了还是失败了。前端的按钮点击事件是无法确认该事件是否执行成功的,所以我们需要后端去统计这个事件执行的结果
3)代码改动依赖于版本更新
前端APP在添加了埋点代码后是需要用户更新到自己的手机上,如果这时候用户不更新自己的APP,那我们是无法采集到对应的用户数据的;还有就是APP应用的上架更新也是需要进行相关审核,这个审核周期是不可控的。但是对于后端来说,添加完埋点代码后,只需要将自己公司内部的后端服务重启一下就可以实现服务更新,去执行新代码采集对应的用户数据。
综合以上几点,我们在选择埋点位置时尽量选择后端埋点。
2.4.2 mini-web埋点实战
接下来我们将以前面的mini-web为例,编写对应的需求文档,完成埋点代码。
我们以其中的一个事件(用户行为)为例,当用户点击进入详情页时我们进行相关数据统计
当用户进入详情页页后,对应的表示该用户阅读了该图书,我们就需要记录用户的阅读量
我们先定义出我们的事件(用户行为)内容
件id | 事件英文名称 | 时间显示名 | 事件定义描述 |
---|---|---|---|
1 | bookClick | 图书详情点击 | 1.pc端在点击页面上的元素(按钮等)的时候触发 2.阅读次数:点击元素的次数 3.触发用户数:点击元素的用户 4.触发的内容:点击的图书名称 5.触发路径位置:从哪个页面触发 |
...... | ...... | ...... | ...... |
根据事件(用户行为)我们按照规范选出我们的字段内容:
-
Who:用户标识信息,这里我们没有用户登录,那我们就以设备id作为标识
-
When:用户点击时间戳,有程序获取当前时间
-
Where:通过用户的IP地址确定其位置
-
How:获取用户设备的系统信息
-
What:用户的点击详情页
在这些属性字段中,我们可以吧用户标识,时间戳,用户IP地址,设备信息这些字段属性对于每个事件来说都会用到,我们可以设计为公共属性
公共属性
字段名称 | 类型 | 说明 | 上线版本 | 上线时间 | 备注 |
---|---|---|---|---|---|
identification | 字符串 | 用户标识 | V1.0 | 2021-01-01 | |
time | 日期 | 事件触发时间 | V1.0 | 2021-01-01 | 自动生成 |
ip | 字符串 | IP | V1.0 | 2021-01-01 | |
equipment | 字符串 | 设备信息 | V1.0 | 2021-01-01 | ... |
对于what部分就是我们用户的操作事件,在这部分我们通过对事件定义的描述信息中我们还需要指定的字段属性有阅读次数,图书名,从哪里进入的
属性变量名 | 事件属性显示名 | 属性值类型 | 属性定义 | 分析说明 | 埋点形式 |
---|---|---|---|---|---|
entrance | 入口 | 字符串 | 点立详情的上一步页面,pc端是页面地址,app端是文字形式 | 后端 | |
bookname | 图书名 | 字符串 | 点击时的图书书名 | 后端 | |
read | 阅读量 | 字符串 | 点击时阅读量+1 | 后端 |
最终会得到一份完整的数据需求文档
开发人员需要根据数据文档中的内容进行字段的收集保存,根据前面方案中提到的数据埋点形式,可以将数据存储到日志中,在原有的代码基础上,我们需要额外增加一段日志写入的代码
# 全局日志设置
import logging
# 设置日志级别和日志格式
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
# 定义获取图书详情信息的处理函数
@app.route('/detail')
def detail():
"""使用 pymysql 加载图书详情数据"""
# 1. 获取指定的图书id
book_id = request.args.get('id', 1) # 获取不到 id 参数时,get函数默认返回1
# 2. 根据 id 查询数据库,获取对应图书的信息
# 创建数据库连接对象
conn = pymysql.connect(host='127.0.0.1', port=3306,
database='booksite', user='root', password='mysql')
# 创建游标对象
cursor = conn.cursor()
# 准备查询的 SQL 语句
sql = f'select * from bookinfo where id={book_id}'
# 通过游标对象执行 SQL 语句
cursor.execute(sql)
# 获取执行 SQL 结果中的一条数据
result = cursor.fetchone() # 注意:结果是元组
# 3. 将图书数据转换为字典的形式
data_dict = {
'id': result[0],
'name': result[1],
'auth': result[2],
'img_url': result[3],
'read': result[5],
'comment': result[6],
'score': result[8],
'content': result[7],
'synopsis': result[9]
}
print(data_dict)
# 4. 更新指定图书的阅读量
new_read = result[5] + 1
sql = f'update bookinfo set bread={new_read} where id={book_id}'
cursor.execute(sql)
conn.commit()
# 5. 关闭游标对象和数据库连接
cursor.close()
conn.close()
# TODO:进行埋点统计
header = request.headers
print('请求头', header)
logging.info({
'bookClick': { # 事件名称
"identification": header['User-Agent'], # 获取设备号
"ip": request.remote_addr, # 用户ip
"equipment": header['User-Agent'], # 用户系统信息
"entrance": header['Referer'], # 用户跳转路径
"bookname": data_dict['name'], # 书名
"read": data_dict['read'], # 阅读量
}
})
# 6. 将指定图书数据转化为JSON字符串并返回
json_str = json.dumps(data_dict, ensure_ascii=False)
return json_str
查看日志信息:
后续还可以增加评论事件统计用户评论信息和评论量,还有用户打分事件,这些事件可以放在一起分析出图书的受欢迎程度是怎样的。
在日志中的数据可以通过搭建 ETL(数据仓库)进行抽取、准换、加载。数据分析人员可以从ETL中获取数据进行相关数据分析工作,这部分内容我们会在后续的课程中陆续进行学习。
03_闭包和装饰器
本章节学习目标
- 能够知道闭包的构成条件
- 能够知道定义闭包的语法格式
- 能够知道闭包的作用
- 能够知道定义装饰器的语法格式
- 能够知道装饰器的使用
- 能够写出通用的装饰器
- 能够写出带有参数的装饰器
本章节常用单词
- closure 英 ['kləʊʒə] 闭包, 使终止
- login 英 ['lɒɡɪn] 登录;
- decorator 英 ['dekəreɪtə] 装饰器 ,装饰者;
- register 英 ['redʒɪstə] 登记;注册
- wrapped 英 [ræpt] 包裹;
- allable 英 ['kɔːləb(ə)l] 可被调用的
3.1 闭包
学习目标
- 能够知道闭包的构成条件
- 能够知道定义闭包的语法格式
3.1.1 问题引入
假设我们现在有下面一个计算两个数之差的函数:
def sub(a, b):
"""计算两个数之差"""
return a - b
需求:累计 sub 函数的调用次数,在每次调用 sub 函数时,都打印出是第几次调用 sub 函数
解决方式:可以利用全局变量对 sub 函数的调用次数进行累计。
# 定义全局变量num,初始值为0,保存sub函数的累计调用次数
num = 0
def sub(a, b):
# 声明使用全局变量
global num
num += 1
print(f'第{num}次调用sub函数')
return a - b
# 测试进行函数调用
sub(2, 1)
sub(3, 2)
sub(5, 3)
执行结果:
第1次调用sub函数
第2次调用sub函数
第3次调用sub函数
注意:虽然通过 num 全局变量能够记录 sub 函数的调用次数,但是这个 num 全局变量和 sub 函数是分离的,其他函数也可以修改这个全局变量,如果其他函数不慎修改了这个 num 全局变量,会造成结果的不正确。
有没有更好的方式?闭包函数
3.1.2 闭包的介绍
闭包的定义:
在函数嵌套的前提下,内部函数使用了外部函数的变量,并且外部函数返回了内部函数,我们把这个使用外部函数变量的内部函数称为闭包。
通过闭包的定义,我们可以得知闭包的形成条件:
- 在函数嵌套(函数里面再定义函数)的前提下
- 内部函数使用了外部函数的变量(还包括外部函数的参数)
- 外部函数返回了内部函数
闭包实现函数调用计数:
# 外部函数定义
def func_outer():
# 记录函数调用的次数
num = 0
# 内部函数定义
def sub(a, b):
# nonlocal关键字声明使用外部函数的变量
nonlocal num
num += 1
print(f'该函数调用的第{num}次')
return a - b
# 外部函数返回内部函数
return sub
# 调用外部函数创建闭包
func = func_outer()
# 调用闭包函数
func(2, 1)
func(3, 2)
func(5, 3)
运行结果:
该函数调用的第1次
该函数调用的第2次
该函数调用的第3次
3.1.3 闭包的作用
-
闭包可以保存外部函数内的变量,不会随着外部函数调用完而销毁
-
闭包的好处是将外层函数的变量和内层函数进行了绑定,防止其他函数对变量的修改
3.2 装饰器
学习目标
- 能够知道定义装饰器的语法格式
3.2.1 问题引入
假设我们现在有如下两个函数:
def func1():
_sum = 0
for i in range(1000000):
_sum += i
return _sum
def func2():
_res = 0
for i in range(1000000):
_res *= i
return _res
需求:分别计算 func1 和 func2 函数调用时的执行时间
解决方式:分别在函数执行之前和执行之后,获取相应的时间戳,计算两个时间戳之差,即可获取函数调用时的执行时间
import time
# time.time():获取代码执行时的时间戳,单位是秒
s1 = time.time()
func1()
s2 = time.time()
print('执行时间:', s2 - s1)
s3 = time.time()
func2()
s4 = time.time()
print('执行时间:', s4 - s3)
执行结果:
执行时间: 0.03532052040100098
执行时间: 0.05095553398132324
思考:如果有 10 个函数都要计算执行时间,该如何操作?在这种情况下,上面的操作就会略显麻烦,接下来我们来介绍另外一种实现方式:装饰器
3.2.2 装饰器的定义
装饰器就是给已有函数增加额外功能的函数,它本质上就是一个闭包函数。
装饰器的功能特点:
- 不修改已有函数的源代码
- 不修改已有函数的调用方式
- 给已有函数增加额外的功能
示例代码:
import time
# 添加计算函数调用时间的功能
def get_time(func):
def wrapper():
s1 = time.time()
func()
s2 = time.time()
print('执行时间:', s2 - s1)
return wrapper
# 利用装饰器装饰函数
func1 = get_time(func1)
func1()
func2 = get_time(func2)
func2()
执行结果:
执行时间: 0.05128788948059082
执行时间: 0.04070568084716797
装饰器的基本雏形:
def decorator(fn):
"""fn:目标函数(被装饰函数)"""
def inner():
# TODO:执行函数之
fn() # 执行被装饰的函数
# TODO:执行函数之后
return inner
代码说明:
- 闭包函数有且只有一个参数,必须是函数类型,这样定义的函数才是装饰器
- 写代码要遵循开放封闭原则,它规定已经实现的功能代码不允许被修改,但可以被扩展
3.2.3 装饰器的语法糖写法
如果有多个函数都要添加计算调用执行时间的功能,每次都需要编写func = get_time(func)
这样代码对已有函数进行装饰,这种做法还是比较麻烦。
Python给提供了一个装饰函数更加简单的写法,那就是语法糖,语法糖的书写格式是: @装饰器名字,通过语法糖的方式也可以完成对已有函数的装饰
示例代码:
import time
# 添加计算函数调用时间的功能
def get_time(func):
def wrapper():
s1 = time.time()
func()
s2 = time.time()
print('执行时间:', s2 - s1)
return wrapper
# 本质:func1 = get_time(func1)
@get_time
def func1():
_sum = 0
for i in range(1000000):
_sum += i
return _sum
# 本质:func2 = get_time(func2)
@get_time
def func2():
_res = 0
for i in range(1000000):
_res *= i
return _res
# 调用func1
func1()
# 调用func2
func2()
说明:
- 装饰器的执行时间是加载模块时立即执行
执行结果:
执行时间: 0.04107022285461426
执行时间: 0.04064607620239258