手动反爬虫:
原博地址 https://blog.csdn.net/lys_828/article/details/124343207
知识梳理不易,请尊重劳动成果,文章仅发布在CSDN网站上,在其他网站看到该博文均属于未经作者授权的恶意爬取信息
1 运行预览图
点击中间的账单下拉菜单,选择对应的月份账单,右侧的版面信息会进行相对应的变化。项目的全部代码可以直接翻到第四部分下载。
2 项目架构
从这个项目开始,代码量就相对较多了,为了避免所有的代码都在一个执行文件下(显着很冗肿,代码可读性较差),在敲代码之前首先需要规划好整个项目的架构布局,代码采用模块化的方式进行设计,具体的项目安排如下:
3 模块介绍
其中dash-vs-personal-app-today是指整个项目的名称(自定义,一般起个较为详细,便于明确项目主题的名称即可),剩下的文件及文件夹就是项目中的具体的内容。
3.1 assets和data文件夹介绍
assets文件夹中就是加载的样式和图片,样式这里可以使用之前已经下载好的bootstrap.min.css样式(也可以按照项目6.2bootstrap组件中的介绍下载其它的样式模板),图片就是网页左侧上方的一个猫图,对应如下 data文件夹中为1-12月份的账单数据,对应如下(Excel中的数据只截取了部分)
3.2 app.py和index.py文件完善
主框架就是app.py项目初始化文件和index.py主程序运行文件,其中app.py文件中的信息较为简单,就是创建一个dash的应用,代码如下 index.py文件中引入初始化后的应用,然后进行布局及运行初始化设置,代码如下 主框架文件设置后之后,重点放在布局上,即是设置app.layout等号后面的具体信息。观察页面中的结构,分为左右两部分,比例约为1:3,为了方便查看绘制的两侧结果,可以把左侧的背景颜色设置为黑色,填充面积为100%。代码如下(注意网格布局的结构总和要为12,这里左右比例为1:3,所以左侧的宽度width=3,右侧的width=9)
from app import app
import dash_bootstrap_components as dbc
app.layout = dbc.Row(
[
dbc.Col('Good Morning',className='bg-dark',width=3),dbc.Col(width=9)
],className='vh-100'
)
app.run_server(debug=True)
代码输入无误后保存运行index.py文件,结果如下:(由于左侧的背景是黑色,字体样式默认也是黑色,所以需要滑动鼠标选中左上方的文字,才可以看到。className='vh-100’表示填充区域长度与整个浏览器是一致,可以通过删除这个参数进行输出的比对)
3.3 sidebar.py文件完善
事情由简单到困难,先把最左侧的少量元素的排版给搞定。打开sidebar.py文件,将所有的信息都放在SideBar()函数中,返回的就是一个html.Div对象(这个操作在第一个项目中就有用到)。代码如下
from dash import html
def SideBar():
return html.Div(
[
html.Img(
src="assets/shocked.jpg",
width=120,
height=120,
),
html.H3("Admin", className="mt-4"),
html.P("Good morning")
]
)
将设置好的模块导入到index.py文件中(将’Good morning’替换成为[SideBar()]即可)
from sidebar import SideBar
dbc.Col([SideBar()],className='bg-dark',width=3),
修改保存后重新运行index.py文件,输出如下 最后就是调整一下图片(形状调整成圆形)和文字居中,具体的方式就是通过className参数,可以参照属性设置网站进行调整,sidebar.py全文代码如下。rounded-circle属性很容易在给的链接中找到,主要是倒数第二行中的代码解释,其中每一个空格就代表着一个属性,text-white文字显示白色,d-flex flex-column align-items-center这三个可以连起来看,表示采用flex布局,在行方向上进行所有元素居中,pt-4表示内容(这里就是图片html.Img)距离最上边(html.Div)要有4个rem。注意这里的pt-4对应的位置是在Div中的,表示[]中的内容与前面的Div的距离。
from dash import html
def SideBar():
return html.Div(
[
html.Img(
src="assets/shocked.jpg",
width=120,
height=120,
className="rounded-circle",
),
html.H3("Admin", className="mt-4"),
html.P("Good morning"),
],
className="text-white d-flex flex-column align-items-center pt-4",
)
index.py全文代码如下
from app import app
import dash_bootstrap_components as dbc
from sidebar import SideBar
app.layout = dbc.Row(
[
dbc.Col([SideBar()],className='bg-dark',width=3),
dbc.Col(width=9)
],className='vh-100'
)
app.run_server(debug=True)
app.py文件代码不变,此时重新运行index.py文件后,刷新网址,输出结果如下(很完美达到了预期效果)
3.4 table.py文件完善
接着就是把简单的表格功能添加进来,打开table.py空文件。初始化创建dash_table的代码,可以参考上一个宝可梦项目中关于dash_table的详细介绍,里面需要指定data和columns两个核心的参数,这里先使用None进行替代。
import dash_table
from dash import html
def PersonalTable():
return html.Div(
[dash_table.DataTable(id='table',data=None,page_size=12,columns=None)]
)
data参数要赋值的变量就是要读取data文件夹中的数据,比如这里以一月份账单读取为例,读入数据后指定data和columns参数,代码完善如下(里面的代码基本上就和上一个项目一致,只需要修改读入的文件的路径)
from dash import dash_table
from dash import html
import pandas as pd
def PersonalTable():
df = pd.read_excel("data/1月账单.xlsx")
return html.Div(
[dash_table.DataTable(id='table',
data=df.to_dict("records"),
page_size=12,
columns=[{"id": i, "name": i} for i in df.columns])]
)
table.py文件代码输入完毕后,将此模块中的函数引入到主程序中,代码如下
from table import PersonalTable
dbc.Col([PersonalTable()],width=9)
此时保存后重新运行index.py,刷新网页后出现的结果如下(表格数据正常加载到网页右侧,一定要注意引入函数放置的位置) 接着就可以把项目5实现动态图表中下拉菜单的设置给引用过来,实现通过手动选取下拉菜单的选项,表格中出现相对应的账单数据。此时回到table.py文件中,设置下拉菜单Dropdown,设置默认显示值和候选值。需要使用到文件遍历的操作os.listdir(’data‘),从而获取data文件夹中的所有的文件路径,方便进行显示后读取(label就是下拉菜单中显示的内容,value就是点击这个下拉选项返回的信息)。交互信息设置,输入的Input就是选择的下拉选项选择后返回的信息,为了避免出现None值,可以通过指定默认value,这里就是指定为“1月账单.xlsx”,然后输出Output就是进入到了表格中的data中,具体传递的v就是下拉菜单options中对应的“value”返回信息
from dash import dcc
from app import app
from dash.dependencies import Output,Input
import os
dcc.Dropdown(
id="dpd",
options=[{"label": f[:-5], "value": f} for f in os.listdir("data")],
value="1月账单.xlsx",
)
@app.callback(Output('table','data'),[Input('dpd','value')])
def update(v):
return pd.read_excel(f"data/{v}").to_dict("records")
保存table.py后,重新运行index.py文件,刷新网页后结果如下(默认是1月份账单,为了下拉菜单中不显示后面.xlsx文件后缀,进行字符串数据的切片,上面f[:-5]功能就是进行后缀的去除) 选择下拉选项为3月份的账单,看看输出结果是否能够完成跳转,输出如下(可以跳转,具体的数据发生了变化) 注意: 如果是windows系统,运行到此处选择下拉选项可能会打声网页窗口抖动的现象,解决的方式就是在assets文件夹中新建一个style.css文件,输入一下代码后再次刷新网页即可。
html,body{ overflow-y:scroll;}
关于下拉菜单和表格数据的加载就完成了,为了方便读者逐步复现,给出目前已经完成的三个文件,分别是app.py,table.py,和index.py文件全代码,如下。
app.py中的全代码
import dash
app = dash.Dash(__name__)
table.py中的全代码(最后添加一个表格阴影的参数)
from dash import dash_table
from dash import html
import pandas as pd
from dash import dcc
from app import app
from dash.dependencies import Output,Input
import os
def PersonalTable():
df = pd.read_excel("data/1月账单.xlsx")
return html.Div(
[
dcc.Dropdown(
id="dpd",
options=[{"label": f[:-5], "value": f} for f in os.listdir("data")],
value="1月账单.xlsx",
),
dash_table.DataTable(id='table',
data=df.to_dict("records"),
page_size=12,
columns=[{"id": i, "name": i} for i in df.columns]
)
],className='shadow'
)
@app.callback(Output('table','data'),[Input('dpd','value')])
def update(v):
return pd.read_excel(f"data/{v}").to_dict("records")
index.py中的全代码
from app import app
import dash_bootstrap_components as dbc
from table import PersonalTable
app.layout = dbc.Row(
[
dbc.Col('Good Morning',className='bg-dark',width=3),
dbc.Col([PersonalTable()],width=9)
],className='vh-100'
)
app.run_server(debug=True)
添加表格阴影后,再次测试运行index.py刷新网页后,输出结果如下(网格下方和左侧明显有一种层次感)
3.5 barchart.py文件完善
打开barchart.py文件,设置一个柱状图。在项目4中,复制的官网的示例就是下拉菜单结合着柱状图,基本的逻辑就是在页面布局的时候设置dcc.Graph()这一步就是为了获得id,然后通过callback回调函数,获得每次点击后的数据据更新图形。由于已经存在一个下拉菜单和表格数据联动,这里就没有必要再次重新设置数据的文件读入,直接使用表格数据就可以。barchart.py全部代码如下
from dash import dcc
from dash.dependencies import Output, Input
import plotly.express as px
import pandas as pd
from app import app
def BarChart():
fig = px.bar()
return dcc.Graph(figure=fig, id="chart")
@app.callback(Output("chart", "figure"), [Input("table", "data")])
def update(data):
df = pd.DataFrame(data)
data = df.sum().to_list()
return px.bar(x=df.columns, y=data, range_y=[0, 4000])
然后把写好的这个模块导入到index.py文件中,代码如下
from barchart import BarChart
dbc.Col([BarChart(),PersonalTable()],width=9)
重新运行index.py文件后,刷新页面如下 index.py全部代码如下
from app import app
import dash_bootstrap_components as dbc
from sidebar import SideBar
from table import PersonalTable
from barchart import BarChart
app.layout = dbc.Row(
[
dbc.Col([SideBar()],className='bg-dark',width=3),
dbc.Col([BarChart(),PersonalTable()],width=9)
],className='vh-100'
)
app.run_server(debug=True)
3.6 tabarchart.py文件完善
通过预览图中可以发现,存在着tab卡片选项控制着月花费账单和年花费账单的柱状图显示。可以参考官网的示例:tab卡片的设置 把代码直接复制粘贴到tabarchart.py文件中,只需要将callback后面的return内容换成自己想要的信息就行,这里为了进行分别,第一个tab选项卡起名为月消费,第二个为年消费,全部代码如下
import dash_bootstrap_components as dbc
from dash import html
from dash.dependencies import Input, Output
from app import app
def TabCharts():
return html.Div(
[
dbc.Tabs(
[
dbc.Tab(label="月消费", tab_id="tab-1"),
dbc.Tab(label="年消费", tab_id="tab-2"),
],
id="tabs",
active_tab="tab-1",
),
html.Div(id="content"),
]
)
@app.callback(Output("content", "children"), [Input("tabs", "active_tab")])
def switch_tab(at):
if at == "tab-1":
return html.H1('月消费')
elif at == "tab-2":
return html.H1('年消费')
return html.P("This shouldn't ever be displayed...")
将写好的tabarchart.py模块导入到index.py文件中,代码如下
from tabarchart import TabCharts
dbc.Col([TabCharts(),BarChart(),PersonalTable()],width=9)
保存index.py文件,重新运行后刷新网页,输出结果如下 鼠标放在第二个tab标签上时,可以正常进行跳转,显示年消费,如下 可以实现tabs选项卡跳转的功能,那么接下来就是把图形和跳转的后内容结合起来,也就是把图形放在callback对应的return后面,点击第一个月消费选项卡,对应的应该是每个月消费的情况,点击年消费就是年总计情况。首先解决一下第一个选项卡的关联问题,就是把之前写好的barchart.py文件中的代码可以复制过来直接用,如下
import plotly.express as px
from dash import dcc
def MonthChart():
fig = px.bar()
return dcc.Graph(figure=fig, id="chart")
@app.callback(Output("chart", "figure"), [Input("table", "data")])
def update(data):
df = pd.DataFrame(data)
data = df.sum().to_list()
return px.bar(x=df.columns, y=data, range_y=[0, 4000])
@app.callback(Output("content", "children"), [Input("tabs", "active_tab")])
def switch_tab(at):
if at == "tab-1":
return MonthChart()
elif at == "tab-2":
return html.H1('年消费')
return html.P("This shouldn't ever be displayed...")
此时修改完毕后,保存,重新回到index.py文件中,由于已经把barchart.py中的代码给复制到tabarchart.py文件中,所以就存在着两个相同的callback,这时候就会造成资源的争抢,这里已经将月份的柱状图和第一个tab选项绑定了,因此也就不需要再导入barchart.py模块了,修改index.py代码如下
dbc.Col([TabCharts(),PersonalTable()],width=9)
保存index.py文件,重新运行后刷新网页,输出结果如下(默认月消费对应的每月的订单都可以显示出来) 但是此时注意右下角有一个红色的报错提醒,打开后可以看一下(提示id=’chart‘没有被定义) 问题其实就是忽略了tabs选项卡中的设定,只看到了dbc.Tab()括号中的信息了,其实后面还有一个html.Div(id=“content”), 这个id="content"就是后面进行callback返回的信息,就要放在这个地方。如果没有指定具体的children信息就是默认为空,所以这里就报错了id = 'chart’未定义。解决问题的方式就是在这里添加上MonthChart()函数即可,如下
html.Div(MonthChart(),id="content"),
此时再刷新网页就不见刚刚的报错了(右下方的红点不见了,此外换成了9月账单,图形依然显示,说明这里的第一个选项卡关联图形的操作已经成功)。 接下来就是处理第二个选项卡关联的图形,命令一个YearChart()函数,具体操作就是读取所有的文件后进行每月消费数据的汇总,代码如下
def YearChart():
total = []
for f in os.listdir("data/"):
df = pd.read_excel(f"data/{f}")
month_total = sum(df.sum().to_list())
total.append(month_total)
fig = px.bar(x=[f"{i}月" for i in range(1, 13)], y=total, range_y=[0, 9000])
return dcc.Graph(figure=fig, id="year-chart")
@app.callback(Output("content", "children"), [Input("tabs", "active_tab")])
def switch_tab(at):
if at == "tab-1":
return MonthChart()
elif at == "tab-2":
return YearChart()
return html.P("This shouldn't ever be displayed...")
修改完毕后进行保存,运行index.py文件后,刷新网址如下(点击年消费选项卡,可以正常输出年统计的柱状图) 此时tabarchart.py文件中的全部代码如下
import dash_bootstrap_components as dbc
from dash import html
from dash.dependencies import Input, Output
from app import app
import plotly.express as px
from dash import dcc
import pandas as pd
import os
def TabCharts():
return html.Div(
[
dbc.Tabs(
[
dbc.Tab(label="月消费", tab_id="tab-1"),
dbc.Tab(label="年消费", tab_id="tab-2"),
],
id="tabs",
active_tab="tab-1",
),
html.Div(MonthChart(),id="content"),
]
)
def MonthChart():
fig = px.bar()
return dcc.Graph(figure=fig, id="chart")
@app.callback(Output("chart", "figure"), [Input("table", "data")])
def update(data):
df = pd.DataFrame(data)
data = df.sum().to_list()
return px.bar(x=df.columns, y=data, range_y=[0, 4000])
def YearChart():
total = []
for f in os.listdir("data/"):
df = pd.read_excel(f"data/{f}")
month_total = sum(df.sum().to_list())
total.append(month_total)
fig = px.bar(x=[f"{i}月" for i in range(1, 13)], y=total, range_y=[0, 9000])
return dcc.Graph(figure=fig, id="year-chart")
@app.callback(Output("content", "children"), [Input("tabs", "active_tab")])
def switch_tab(at):
if at == "tab-1":
return MonthChart()
elif at == "tab-2":
return YearChart()
return html.P("This shouldn't ever be displayed...")
index.py文件中的全部代码如下
from app import app
import dash_bootstrap_components as dbc
from sidebar import SideBar
from table import PersonalTable
from tabarchart import TabCharts
app.layout = dbc.Row(
[
dbc.Col([SideBar()],className='bg-dark',width=3),
dbc.Col([TabCharts(),PersonalTable()],width=9)
],className='vh-100'
)
app.run_server(debug=True)
3.7 infocards.py文件完善
最后一部分,就是打开infocards.py文件。尝试常见一个卡片的样式,代码如下
import dash_bootstrap_components as dbc
from dash import html
from dash.dependencies import Input, Output
from app import app
def InfoCards():
return dbc.Row(
[
dbc.Col(
[
html.H6('月度消费'),
html.Hr(),
html.H1('---')
],className='bg-warning'
)
]
)
然后将这个文件导入到index.py文件中,代码如下
from infocards import InfoCards
dbc.Col([InfoCards(),TabCharts(),PersonalTable()],width=9)
保存index.py文件后,重新运行并刷新网页如下(实现了卡片的单个布局) 从预览图中可以发现,三个卡片的布局基本一致,就是显示内容和背景颜色不同,为了简化代码,可以直接设置一个函数,专门处理卡片的样式,后续直接调用函数即可,代码封装以及调用如下
def Card(name,cost,color='bg-warning'):
return html.Div([
html.H6(name),
html.Hr(),
html.H1(cost)
],className=f'{color}'
)
def InfoCards():
return dbc.Row(
[
dbc.Col(Card('月度消费','---')),
dbc.Col(Card('季度消费','---',color='bg-primary')),
dbc.Col(Card('年度消费','---',color='bg-info'))
]
)
保存后重新运行index.py文件后,刷新网址,输出结果如下(想要的结果是出来了,对于具体的样式属性可以通过className进行设置) 就是修改className=f’{color}'这里的内容,添加一些属性,代码如下(设置阴影,文本颜色,间距和倒圆角)
className=f"{color} shadow text-white p-3 rounded"
刷新网址后,输出的结果对应如下(还有一点点小瑕疵,就是卡片直接顶到最上面了) 同样也是调整className参数,不过这一次调整的是dbc.Row中的参数,比如指定4个相对于距离(之前的毛的图片也是4个相对的距离)
className='py-4'
关于距离的设置,可以查看官方很详细的解释,其中最后面的一个值就是在1-5共5个level 然后点击网址打开后上图中的蓝色的spacing utilities链接,就可以知道具体的距离可以搭配的参数设置,如下(这里指定的参数就是下图红框中的三个组合,表示卡片位于上下的边缘各相当于4个level的距离) 修改代码完成后,运行index.py文件后,刷新网址如下(还是比较满意现在的这个卡片布局) 接下来就是要解决数据联动的问题了。这里有一个问题,由于数据联动,是不是又要重新读入一遍数据呢?感觉也是可以,直接再重新读入数据后,再分别求出对应的月,季度和年消费的数据。这个想法也是此时最常见的想法,除此之外,还有就是利用网页中已经加载的数据,这里就是可以通过年消费对应的图形中,就有每一月的消费数据,获取到这个图形中的数据,那么自然也就不用再读取一遍本地的数据。
想法已经构成了,但是这里有一个难点就是,系统默认通过选项卡获取到的是月消费数据,年消费数据只用通过tab选项卡点击年消费才会加载出来图形,由此就必须解决下拉菜单有选项时候年份数据就已经被加载出来了。尝试将默认tab选项卡设定为第二个,然后调用的就是年消费数据图,接着就是把年消费数据图形(获取每月的数据)和下拉菜单选项(获取选择的月份)作为输入,最后输出三个卡片的花费金额,代码如下
active_tab="tab-2",
html.Div([YearChart()],id="content")
html.H1(cost,id=name)
@app.callback(
output=[
Output("月度消费", "children"),
Output("季度消费", "children"),
Output("年度消费", "children"),
],
inputs=[Input("year-chart", "figure"), Input("dpd", "value")])
def update(fig,cur_table):
data = fig["data"][0]["y"]
print(data)
cur_month = int(cur_table[:-8])
cur_month_index = cur_month - 1
cur_month_cost = data[cur_month_index]
cur_season_cost = None
cur_year_cost = sum(data)
season_table = {
"第一季度": [1, 2, 3],
"第二季度": [4, 5, 6],
"第三季度": [7, 8, 9],
"第四季度": [10, 11, 12],
}
for s in season_table.values():
if cur_month in s:
a, b, c = s
cur_season_cost = data[a - 1] + data[b - 1] + data[c - 1]
return cur_month_cost, cur_season_cost, cur_year_cost
此时保存各文件代码后,重新运行index.py文件,刷新网址如下(此时可以显示出卡片中各自的消费,但是细心可以发现此时右下角是有红点报错) 原因就是在于把tab选项卡默认设定为第二个年消费数据,导致月消费数据没有加载,绘月消费制图形中指定了id=‘chart’,由于没有使用该图形,所以就一直报如下错误(和之前一样就是因为在html.Div中没有调用) 因此就想到在加载表格数据时候同时两个数据图同时都加载出来,只是有一个默认显示,一个默认隐藏,这样就会解决一个图形没有初始化绘制的问题,也就解决了id不存在的报错。具体的方式就是借助style参数,修改tabarchart.py文件中全部内容如下
import dash_bootstrap_components as dbc
from dash import html
from dash.dependencies import Input, Output
from app import app
import plotly.express as px
from dash import dcc
import pandas as pd
import os
def TabCharts():
return html.Div(
[
dbc.Tabs(
[
dbc.Tab(label="月消费", tab_id="tab-1"),
dbc.Tab(label="年消费", tab_id="tab-2"),
],
id="tabs",
active_tab="tab-1",
),
html.Div([YearChart(), MonthChart(hidden=True)],id="content"),
]
)
def MonthChart(hidden=False):
style = {"display": "none"} if hidden else {}
fig = px.bar()
return dcc.Graph(figure=fig, id="chart",style = style)
@app.callback(Output("chart", "figure"), [Input("table", "data")])
def update(data):
df = pd.DataFrame(data)
data = df.sum().to_list()
return px.bar(x=df.columns, y=data, range_y=[0, 4000])
def YearChart(hidden=False):
style = {"display": "none"} if hidden else {}
total = []
for f in os.listdir("data/"):
df = pd.read_excel(f"data/{f}")
month_total = sum(df.sum().to_list())
total.append(month_total)
fig = px.bar(x=[f"{i}月" for i in range(1, 13)], y=total, range_y=[0, 9000])
return dcc.Graph(figure=fig, id="year-chart",style = style)
@app.callback(Output("content", "children"), [Input("tabs", "active_tab")])
def switch_tab(at):
if at == "tab-1":
return html.Div([MonthChart(), YearChart(hidden=True)])
elif at == "tab-2":
return html.Div([YearChart(), MonthChart(hidden=True)])
return html.P("This shouldn't ever be displayed...")
此时保存文件后,重新运行index.py文件,刷新界面如下(右下方的红点不见了) 至此就完成了项目中全部的内容。最后面的数据联动,也可以尝试进行重新读取数据的方式进行设置,相当于又多了一层操作。如果再大胆一点,甚至可以单独创建一个data.py文件,将每次需要用到的各类数据都封装成函数,哪个模块使用到的时候直接进行调用即可。
4 全部代码
为了方便读者复现项目,提供全部的文件和文件代码:dash-vs-personal-app-today 百度网盘提取码:6666
|