项目更多开发细节参见:
- Flask+Vue+Scrapy+Pandas+Echarts商品数据分析与可视化(一) 项目概述、动图演示
- 商品数据分析与可视化之 Scrapy 数据爬取记录(二)
- 商品数据分析与可视化之 Vue 项目记录(四)
- 商品数据分析与可视化之 项目错误记录(五)
一、项目环境搭建
1、利用 pipenv 为当前项目创建虚拟环境 (参见文末 pipenv安装详情)
使用pip安装Pipenv:
pip install pipenv
为当前项目创建虚拟环境:
pipenv install
2、安装 flask (可能需要开代理)
pipenv install flask
3、安装 python-dotenv 用于配置环境变量 (参见文末 python-dotenv配置详情)
pipenv install python-dotenv
创建一个名称为 .flaskenv
的文件,文件内容如下:
# 开启调试模式(1: 开启 0: 关闭)
# 开启后,经测试发现修改代码后无需重启服务即可实现更新
FLASK_DEBUG = 1
4、创建 app.py 文件
在创建好虚拟环境的目录下新建一个 app.py
文件(建议用这个文件名,否则后续无法启动内置服务器,如若不使用此文件名,可在 .flaskenv
文件中将 FLASK_APP
变量设置成自己的文件名称 参见 python-dotenv配置详情) 文件内容如下:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'hello flask'
创建完虚拟环境后,记得选择对应的 Python 解释器(参见文末 vscode中的python解释器选择)
二、主程序
1、导入开发所需的第三方包
下载并导入:flask
、pymongo
、pandas
、flask_cors
from flask import Flask,request,jsonify
from pymongo import MongoClient
from pymongo.errors import ConnectionFailure,ServerSelectionTimeoutError
import pandas as pd
app = Flask(__name__)
CORS(app,cors_allowed_origins="*")
2、读取MongoDB数据库数据并去重
安装 pymongo
:
pipenv install pymongo
更新 app.py
文件的内容如下:
# 定义聚合函数 aggregate 的参数
PIPLINE_PARM = [
{
"$group": {
# 基于 商品名称(name) 进行分组
"_id": "$name",
# 数据出现重复时,只保留最后一次出现的数据
"doc": { "$last": "$$ROOT" }
}
},
{
"$replaceRoot": { "newRoot": "$doc" }
},
{
# 不包含 _id 字段
"$project": {
"_id": 0
}
}
]
# 获取数据库数据并去重
def getMongoDBData():
with MongoClient("localhost",27017) as client:
# smzdm: 为数据库名称
db = client.smzdm
# goods_info: 为数据库中的集合(相当于mysql中的表)
collection = db.goods_info
# 应用聚合函数 aggregate 过滤文档内容
data = collection.aggregate(PIPLINE_PARM)
result = list(data)
return result
2、实现模糊查询
在 app.py
文件中追加以下内容:
@app.route('/api/getGoodsByName',methods=['POST'])
def get_goods_by_name():
with MongoClient("localhost",27017) as client:
db = client.smzdm
collection = db.goods_info
# 直接查询不去重
# data = collection.find({"name": {'$regex': request.json['data']}},{"_id":0})
NEW_PIPLINE_PARM = PIPLINE_PARM.copy()
# 追加聚合函数的参数 此处根据 商品名称 进行模糊查询
NEW_PIPLINE_PARM.extend([
{"$match":
{"name": {'$regex': request.json['data']}}
}
])
data = collection.aggregate(NEW_PIPLINE_PARM)
data_list = []
for each in data:
data_list.append(each)
return data_list
代码说明:
$regex
:该参数为 MongoDB 中的查询运算符,作用为选择值与指定的正则表达式匹配的文档(数据)
关于request对象的其他属性:
-
request.method
:获取 HTTP 请求的方法 -
request.args
:获取 HTTP 请求中的查询参数 -
request.form
:获取 POST 请求中的表单数据 -
request.files
:获取文件上传数据 -
request.json
:将请求的主体解码为 json
更多 MongoDB 查询运算符参见:https://docs.mongoing.com/can-kao/yun-suan-fu/query-and-projection-operators/evaluation-query-operators
接口名称 | 模糊查询 |
---|---|
请求方法 | POST |
URL | http://localhost:5000/api/getGoodsByName |
前端请求(dev):http://localhost:8080/api/getGoodsByName
请求体携带参数(json):
{"data":"牛肉"}
返回值:
[{"name": "xxx","brand": "xxx",...},{...}]
3、翻页
在 app.py
文件中追加以下内容:
@app.route('/api/pageTurn/<int:pageNo>')
def page_up(pageNo):
with MongoClient("localhost",27017) as client:
db = client.smzdm
collection = db.goods_info
NEW_PIPLINE_PARM = PIPLINE_PARM.copy()
# 追加参数:每次获取 15 条数据
NEW_PIPLINE_PARM.extend([
{"$skip": 15 * pageNo},
{"$limit": 15}
])
data = collection.aggregate(NEW_PIPLINE_PARM)
# 获取数据总数目
pageCount = collection.count_documents({})
data_list = []
for each in data:
data_list.append(each)
pageInfo = {
# 当前页码
"pageNo": pageNo,
# 页码对应数据
"pageData": data_list,
# 商品总数目
"pageCount": pageCount
}
return pageInfo
接口名称 | 翻页 |
---|---|
请求方法 | GET |
URL | http://localhost:5000/api/pageTurn/1 |
前端请求(dev):http://localhost:8080/api/pageTurn/1
返回值:
{
"pageNo": 1,
"pageData": [{"name": "xxx","brand": "xxx",...},{...}],
"pageCount": 148
}
4、获取商品筛选对应的选项
在 app.py
文件中追加以下内容:
@app.route('/api/getFilterOptions')
def get_filter_options():
db_data = getMongoDBData()
df = pd.DataFrame(db_data)
filterObj = {
"brand": df['brand'].unique().tolist(),
"buy_channels": list(df['buy_channels'].unique()),
"goods_classification": list(df['goods_classification'].unique()),
}
return filterObj
代码说明:
获取数据库数据,再借助 pandas
将数据转换成 DataFrame
类型的数据后,调用 unique()
方法获取对应列的唯一值列表,再利用 tolist()
方法将其转换成列表类型的数据(也可使用 list
方法)。使用 nunique()
则可以获取其对应的唯一值个数。
接口名称 | 获取商品筛选对应的选项 |
---|---|
请求方法 | GET |
URL | http://localhost:5000/api/getFilterOptions |
前端请求(dev):http://localhost:8080/api/getFilterOptions
返回值:
{
"brand": ['xxx',...],
"buy_channels": ['xxx',...],
"goods_classification": ['xxx',...]
}
5、商品筛选
在 app.py
文件中追加以下内容:
@app.route('/api/handleForm',methods=['POST'])
def handle_form():
with MongoClient("localhost",27017) as client:
db = client.smzdm
collection = db.goods_info
query = {}
# 优惠券选项是否选中
if request.json['data']['coupons'] == 'yes':
query['coupons'] = {"$ne": []}
elif request.json['data']['coupons'] == 'no':
query['coupons'] = []
# 按时间进行排序
if request.json['data']['sortTime'] == 'rise_time':
sortTime = 1
elif request.json['data']['sortTime'] == 'drop_time':
sortTime = -1
else:
pass
# sortTime = 0
# 按点赞进行排序
if request.json['data']['sortLikes'] == 'rise_likes':
sortLikes = 1
elif request.json['data']['sortLikes'] == 'drop_likes':
sortLikes = -1
else:
pass
# sortLikes = 0
# 复选框不为空时
if request.json['data']['brand']:
query['brand'] = {"$in":request.json['data']['brand']}
if request.json['data']['buy_channels']:
query['buy_channels'] = {"$in":request.json['data']['buy_channels']}
if request.json['data']['goods_classification']:
query['goods_classification'] = {"$in":request.json['data']['goods_classification']}
# 原查询语句,无去重
# data = collection.find(query).sort({"update_time": sortTime,"likes":sortLikes,"collects":sortLikes})
# 去重升级版
NEW_PIPLINE_PARM = PIPLINE_PARM.copy()
NEW_PIPLINE_PARM.extend([
{"$match": query},
{"$sort":
{
"update_time": sortTime,
"likes":sortLikes,
"collects":sortLikes
}
}
])
data = collection.aggregate(NEW_PIPLINE_PARM)
data_list = []
for each in data:
data_list.append(each)
return data_list
接口名称 | 商品筛选 |
---|---|
请求方法 | POST |
URL | http://localhost:5000/api/handleForm |
前端请求(dev):http://localhost:8080/api/handleForm
请求体携带参数(json):
{"data":{"coupons":"yes","sortTime":"rise_time","sortLikes":"rise_likes","brand":["百里炙"],"buy_channels":["京东"],"goods_classification":["牛羊肉"]}}
返回值:
[{"name": "xxx","brand": "xxx",...},{...}]
6、获取柱状图数据
在 app.py
文件中追加以下内容:
@app.route('/api/getBarChartData/<string:barFieldName>')
def get_bar_chart_data(barFieldName):
db_data = getMongoDBData()
df = pd.DataFrame(db_data)
# 转换时间格式
date = pd.to_datetime(df['update_time'],format='%Y-%m-%d %H:%M')
# 获取日期格式 例:2023-1-1
day = date.dt.floor('d')
# 获取日期列表
date_list = df.groupby(day).sum().index.strftime('%Y-%m-%d').tolist()
# 获取总数
day_sum = len(df.groupby(day).sum())
# 控制截取日期的数量,此处只截取最近10天的数据
if day_sum > 10:
start = day_sum - 10
else:
start = 0
# 获取每一天当中(商品点赞/商品收藏/商品评论数)的最大值及对应的商品分类
goods_class_max_list = df.loc[df.groupby(day)[barFieldName].idxmax()][['goods_classification',barFieldName]].values.tolist()[start:]
goods_class_bar_obj = {}
goods_class_name_list = []
field_value_list = []
for i in goods_class_max_list:
goods_class_name_list.append(i[0])
field_value_list.append(i[1])
goods_class_bar_obj['goods_class_name_list'] = goods_class_name_list
goods_class_bar_obj['field_value_list'] = field_value_list
goods_class_bar_obj['date_list'] = date_list[start:]
return goods_class_bar_obj
接口名称 | 获取柱状图数据 |
---|---|
请求方法 | GET |
URL | http://localhost:5000/api/getBarChartData/likes |
说明 | 获取近10天(点赞/收藏/评论数)最多的商品分类 |
前端请求(dev):
- 获取点赞数据:
http://localhost:8080/api/getBarChartData/likes
- 获取收藏数据:
http://localhost:8080/api/getBarChartData/collects
- 获取评论数据:
http://localhost:8080/api/getBarChartData/comment_count
返回值:
{
// 日期列表(近10天)
"date_list": ['2023-1-1','xxx'],
// 点赞/收藏/评论数 对应的数值
"field_value_list": [1,2,...],
// 商品分类列表
"goods_class_name_list": ['xxx',...]
}
实现说明:
1、设置日期的格式
# 转换时间格式
date = pd.to_datetime(df['update_time'],format='%Y-%m-%d %H:%M')
运行结果:
0 2023-12-24 09:13:00
1 2023-12-21 16:40:00
2 2023-12-24 11:04:00
...
305 2023-12-24 10:19:00
306 2023-12-26 15:08:00
307 2023-12-25 01:06:00
2、针对日期的年月日数据进行分组,并转化成列表
# 获取日期的年月日 例:2023-12-24
day = date.dt.floor('d')
# 获取日期列表
date_list = df.groupby(day).sum().index.strftime('%Y-%m-%d').tolist()
运行结果:
['2023-12-14',
'2023-12-15',
...
'2024-01-10',
'2024-01-11']
3、再根据日期列表获取每一天 点赞/收藏/评论 数最多的商品的索引,后续再通过该索引找到对应的商品的商品分类
# 例如:此处获取每一天 点赞 数最多的商品的索引
df.groupby(day)['likes'].idxmax()
运行结果:
update_time
2023-12-14 163
...
2024-01-11 120
说明:后续的折线图、饼图等皆以类似的方法实现,不再说明。
7、导出数据
@app.route('/api/toExcel')
def data_to_excel():
db_data = getMongoDBData()
df = pd.DataFrame(db_data)
# 导出至桌面
df.to_excel(r'D:\桌面\smzdm.xlsx')
return '1'
8、数据库的插入、删除、修改操作
插入:数据库的插入操作,其他方法类似
@app.route('/api/insertDB',methods=['POST'])
def insert_db():
# print(request.json)
with MongoClient("localhost",27017) as client:
db = client.smzdm
collection = db.goods_info
collection.insert_many(request.json.get('data'))
return '1'
删除:
# 此处通过商品的名称进行删除
collection.delete_many({"name": "xxx"})
修改:
# 此处通过商品的名称查找到目标元素,再修改其对应的字段
collection.update_one({"name": "xxx"},{"$set": {"star": 1}})
9、获取数据
说明:爬取数据并实时传递至前端进行展示
准备工作:
1、下载 flask_socketio
pipenv install flask_socketio
2、导入需要使用到的模块
from flask_socketio import SocketIO,disconnect
import subprocess
socketio = SocketIO(app,cors_allowed_origins='*')
3、需要有一个由 scrapy
创建的爬虫项目。因为此处是通过执行 scrapy
命令来爬取数据的。
4、实现
# 爬取数据
@socketio.on('start_spider')
# 默认爬取的网站 quotes
def start_spider(scrapyCMD=['scrapy', 'crawl', 'quotes']):
# 向前端发送数据
# socketio.emit("console_output",{"data": "test"})
try:
# 创建子进程
process = subprocess.Popen(
# 此命令爬取 什么值得买 网站
# ['scrapy', 'crawl', 'smzdm'],
# 此命令爬取 quotes 网站
# ['scrapy', 'crawl', 'quotes'],
# 根据前端传递的 scrapy 命令,来选择爬取的网站(该参数由前端传递)
scrapyCMD,
# 目标文件夹
cwd='scrapy_smzdm',
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
# 该文件的 stdout 以文本流方式打开
universal_newlines=True
)
# 获取命令输出并实时推送到前端
for line in process.stdout:
socketio.emit('console_output', {'data': line.strip()})
# 等待命令执行完成
process.wait()
# 爬取完成后打印提示信息
socketio.emit('console_output', {'data': 'Scrapy crawl completed','finish': '1'})
except Exception as e:
socketio.emit('console_output', {'data': f'Error: {str(e)}','finish': '0'})
if __name__ == "__main__":
socketio.run(app,debug=True,host="0.0.0.0",port=5000)
说明: 参数 scrapyCMD
是由前端传递过来的 scrapy 命令,例如:scrapy crawl smzdm
命令可以执行爬虫获取数据。
三、其他
一、pipenv 的安装和环境变量的配置
1、安装 Pipenv
Pipenv 是基于pip的Python包管理工具,用于包安装、包依赖管理和虚拟环境管理。
使用pip安装Pipenv:
pip install pipenv
检查安装是否成功:
pipenv --version
2、创建虚拟环境
通过创建虚拟环境,可以为每一个项目创建独立的Python解释器环境,便于相关依赖包的管理。
在创建虚拟环境前,可在当前文件夹下创建一个 .venv
的文件夹,这样执行创建虚拟环境命令后所下载的依赖都会在这个 .venv
文件夹中。默认安装路径则是 C:\Users\Administrator\.virtualenvs\当前项目文件夹名称+随机字符
。
也可通过配置环境变量的方式将所下载的依赖放在当前执行命令的文件夹中:
配置环境变量:
变量名:WORKON_HOME
变量值:PIPENV_VENV_IN_PROJECT
配置完环境变量后,执行 pipenv install
命令创建虚拟环境后,文件目录如下:
查看项目对应的虚拟环境路径:
pipenv --venv
为当前的项目创建虚拟环境:
pipenv install
执行结果:
D:\桌面\GoodsDataAnalysis\GoodsFlask> pipenv install
......
Successfully created virtual environment!
显式地激活虚拟环境:(激活虚拟环境后才可以执行命令)
pipenv shell
激活虚拟环境后,查看当前环境所安装的包:
pip list
不显式激活虚拟环境,在当前项目的虚拟环境中执行命令:
pipenv run python index.py
3、安装 Flask
为当前虚拟环境安装 Flask:
pipenv install flask
说明:
Pipenv 会自动管理虚拟环境,所以在执行 pipenv install
安装Python包时,无论是否激活虚拟环境,包都会安装到虚拟环境中。
二、python-dotenv 安装及环境变量的配置
python-dotenv: Flask的自动发现程序实例机制如果发现安装了 python-dotenv
,则会在使用 flask run
或其他命令时自动从 .flaskenv
文件和 .env
文件中加载环境变量。
.flaskenv
文件(手动创建):用来存储和Flask相关的公开环境变量。
.env
文件(手动创建):用来存储包含敏感信息的环境变量。
当安装了python-dotenv时,Flask在加载环境变量的优先级是:手动设置的环境变量 > .env中设置的环境变量 > .flaskenv设置的环境变量。
① 安装 python-dotenv
:
pipenv install python-dotenv
② 在项目根目录下创建一个 .flaskenv
文件,文件内容如下:
# 使服务器外部可见
FLASK_RUN_HOST = 0.0.0.0
# 改变默认端
FLASK_RUN_PORT = 8000
③ 执行 flask run
命令再次启动服务
PS D:\桌面\FlaskBasic\Basic01> flask run
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://10.22.140.118:8000
Press CTRL+C to quit
更多环境变量补充,皆可放置在 .flaskenv
文件中
# 当用户修改了 `app.py` 文件名时
FLASK_APP = 新的文件名
# 设置运行环境为开发环境
FLASK_ENV=development
# 开启调试模式(1: 开启 0: 关闭)
# 开启后,经测试发现修改代码后无需重启服务即可实现更新
FLASK_DEBUG = 1
# 使服务器外部可见
FLASK_RUN_HOST = 0.0.0.0
# 改变默认端
FLASK_RUN_PORT = 8000
三、在 VsCode 中,创建完虚拟环境后,选择 Python 解释器
1、点击 VsCode 右下角的 Python 解释器:
2、选择以下目录中的 Python.exe
D:\桌面\GoodsDataAnalysis\GoodsFlask\PIPENV_VENV_IN_PROJECT\GoodsFlask-S_qAB8d7\Scripts
说明:GoodsFlask-S_qAB8d7
为项目文件夹名称+随机生成的字符,应根据自己实际选择。
动图详情: