翻译中…
一个通过猜测api绕过支付的案例
背景
案例已去除所有敏感信息.
前段时间考研时接到的一个需求, 客户是抓取自己的网站, 可能是用来做测试?
该网站是售卖现在比较火的龙傲天短剧, 通常是那种每集2分钟, 动辄100+集的龙傲天短剧。通常采用前xx集免费,等消费者看上头了,后面剧集便需要通过支付购买方可解锁观看,由此实现变现。
支付这个功能实际上是对安全的要求十分高的,通常使用大厂的接口会靠谱的多。然而,支付接口也只是支付这一大块功能中的一个组成部分。即便使用了靠谱的支付接口,其他方面的逻辑漏洞也又可能导致
本次的案例就是支付接口之外的漏洞。该网站在消费者支付后,会在后端验证该消费者的消费信息后,给该主机后台发送所购买的视频的链接。在前端并不能看到这个链接,也看不出什么异常。
分析
在多次的抓包分析后,发现了这样的问题:对付费视频的api做请求时,请求中并没有任何用于检验身份的token。这就意味着,未付费的消费者如果碰巧猜测到了付费视频的api,那么便可以不用付费即可获得视频数据。
本次案例中的视频api有这样的结构:xxxx.com/video_id/num_id/base.m3u8. 其中当集数增加1后,base以36进制也增加1。由此,我们可以通过前一段免费的视频来猜测后面收费视频的api。
问题的关键在base+1的实现。该base是一个字符串,每个位上的字符取值集合为[0-9a-z],按照顺序在数值上主次+1,也就是4+1=5,d+1=e,当z+1后会变回0,并向高位进位。数学上与36进制数字同构。
实现
关键实现一个36进制数字的类:
1 | class ABNum: |
该类并没有完全实现作为一个数字该实现的所有方法,这里仅满足使用即可。算法使用常规的短除法,按照数字定义,从10进制与36进制之间转换。类的内部使用10进制保存数值以及处理算术运算,在需要时再转换为36进制即可。
需要注意的点有:
1. 注意原始36进制数据的位数,最终求出来的36进制数要记得填充0.
2. 注意10进制与36进制转换时,需要做reverse.
总结
作为渗透测试人员、爬虫工程人员,要多留意数据之间的潜在关系。看出关系后,要能联系到可以实现的数学表达。遇到该类网站,未了解全貌后不要擅自爬取。本质上这个已经有些超过合法爬虫的范围,已经触犯法律了。可以提醒站长漏洞。
作为网站运营,这种出售链接的方式,应当使用完全随机的链接,以防止攻击者猜测出规律,绕过支付。更稳妥的方式还是验证浏览者身份信息,以及即时生成随机的视频链接。
理解pandas架构
前言
Pandas 是数据科学中常用的数据处理库;然而初学者并没有学习到Pandas的思想,仍然用数组的思维将Pandas仅仅作为数据的容器,总使用迭代的方式处理数据,这样既不优雅,又容易犯错;此外Pandas的API众多,初学者很容易迷失在茫茫的API文档中,不得要领;
数据模型
首先需要牢记的是,Pandas采用计算式;也就是说绝大多数的操作都是计算出一个结果,而并不修改原数据;如果你需要对原数据作出修改,通常需要手动重新赋值,或者加上inplace参数;
初识数据
这是一个常规数据表的结构示意图:
在Pandas中,数据由Series与DataFrame表示;其中DataFrame可以视为Series的集合;
索引
列名
特征(列)
观测(行)
选择
如果是选择某列,可以直接采用下面的方式选取:
1 | col = df["col_name"] |
此时得到的是一个Series对象;
若选择多列,则采用下面的方式选取:
1 | cols = df[["col_name_1", "col_name_2", "col_name_3"]] |
此时得到的cols是一个DataFrame对象。
若选择涉及到行,则需要使用数据框的loc/iloc接口;
loc
loc 暴露了数据框根据index对数据进行选择的接口:
1 | sub_df = df.loc[] |
iloc
iloc 暴露了数据框根据位置对数据进行选择的接口:
1 | sub_df = df.iloc[2:8, 1:9] |
筛选
筛选的过程分成两步:
根据筛选条件生成掩码;
根据掩码对数据进行筛选。
掩码
以行方向举例,每行都是一个独立的观测
用筛选条件,对所有观测做一个映射,满足筛选条件则映射到True,否则映射到False;
由此获得一个与索引长度相同的掩码数组;
根据该掩码数组,对应为True的观测被保留;对应为False的观测被舍弃。
上述为通用的筛选逻辑,其中前文介绍的loc/iloc接口均可以使用,例如:
1 | df.loc[lambda s: s['shield'] == 8, :] |
处理
数据处理常用的三大函数类:apply、map、reduce
这三大函数类都接受一个处理函数func、一个序列seq、以及其他的定制参数作为参数,但它们在数据处理的逻辑上会有一些差别。
apply
apply类函数会将序列整体作为一个参数传递给处理函数,该处理函数会将序列整体作为参数进行处理,并返回一个值作为结果;
从效果上来看比较类似:
1 | df['col_name'].apply(func) |
但当apply的序列对象为一个DataFrame时,apply会作用在DataFrame的每个Series上,并将每个Series的计算结果合并成一个Series。此时需要额外传递一个参数axis指明apply的方向。默认0代表行方向,1代表列方向。
1 | df.apply(func, axis=1) |
map
map类函数与apply类似,接受一个处理函数func,一个序列seq作为参数;不同的是,apply会将seq作为一个整体被func调用,但在map中,func会作用在seq中的每个元素上,并返回对应的计算值。最终所有计算值由map收集,并拼凑成一个与原seq形状相同的数据结构返回。
通常map只作用在Series上:
1 | df['col_name'].map(func) |
如果需要对DataFrame整体进行map操作,api为applymap
reduce
reduce类函数同样接受一个处理函数func,一个序列seq作为参数;
reduce函数会取seq中前两个元素s1, s2作为参数去调用func:func(s1, s2),并将结果继续与后续元素做func运算,直到消耗完seq中的所有元素,并将最后的规约值作为结果返回;
1 | df['col_name'].reduce(func) |
合并
数据合并通常有两类:具有相同特征的两个数据框按行合并成具有更多观测的数据框、具有相同观测的两个数据框按列合并成具有更多特征的数据框。
通常前者更简单,使用concat:
1 | df = pd.concat(df_1, df_2) |
往往采用外连结的方式保证所有特征都被保留。
后者通常会复杂一些,往往使用merge:
1 | df = pd.merge(df_1, df_2, left_on="left_col", right_on="right_col", how="left") |
这里参考数据库的左右连接与内外连接,两者逻辑上完全一致。
重铸
重铸这个词来自R中的数据处理。
用来修改数据的结构。
melt
stack
unstack
窗口 window
聚合 groupby
groupby 同样是数据处理中最重要的函数之一。
groupby
groupby 将数据按照某一标准分类,以便让方便后续的apply操作;
groupby通常传入列名即可,pandas会将改列值相同的观测分成一类;
1 | df.groupby('col_name') |
groupby返回的是一个惰性对象,也就是说分类并不会立刻开始,而是会在后续运算时一并运算。
常见的会在其后加上
Pandas API 一览
Series 是Pandas 中的最小单位
强化学习笔记
教材:蘑菇书
绪论
强化学习与监督学习的不同
- 强化学习输入的样本是序列数据,而不像监督学习里面样本都是独立的。
- 强化学习需要不断的试错探索。探索和利用是强化学习里核心的问题。需要在探索和利用之间找到一个权衡。
- 强化学习里没有强监督者,只有奖励信号,并且奖励信号是延迟的。
- 强化学习可以达到超人例子,而监督学习只能接近人。
一些术语和概念
通过预演(rollout)获取一系列观测
每个观测称之为一个轨迹(trajectory)
轨迹是当前帧状态以及它的策略动作的序列:$(s_0, a_0, s_1, a_1,…)$
每个观测结束,我们可以通过观测序列以及最终奖励(eventual reward)来训练智能体
一场游戏成为一个回合(episode)或者实验(trial)
强化学习 -> 深度强化学习 将特征工程并入深度学习中
序列决策
智能体和环境
- 概念
- 强化学习研究的问题是智能体与环境交互的问题
奖励
- 概念
- 奖励是由环境给的一种标量的反馈信号,可以显示智能体在某一步的某个策略的表现。
- 强化学习的目的是最大化期望的累积奖励
序列决策
奖励分为近期奖励和远期奖励,两者的权衡是强化学习的重要课题之一
历史是观测、动作、奖励的序列:
$$H_t=o_1, a_1, r_1,…,o_t,a_t,r_t$$
智能体的当前动作依赖于它之前得到的历史,整个游戏的(智能体)状态可以看为关于历史的函数:
$$s_t=f(H_t)$$
此外,环境有自己的函数:
$$s^e_t=f^e(H_t)$$
智能体自身的函数:
$$s^a_t=f^a(H_t)$$
观测与状态:
状态是对世界的完整描述
观测是对状态的部分描述(通常是智能体能获取的信息)
完全可观测和部分可观测
完全可观测指智能体状态和环境状态等价,此时通常建模为马尔可夫决策过程,此时$o_t=s^e_t=s^a_t$
部分可观测指智能体无法获取环境运作的所有状态,此时通常建模为部分可观测马尔可夫决策过程
动作空间
动作空间指给定环境下,有效动作的集合
离散动作空间
连续动作空间
智能体的组成成分和类型
策略,智能体用策略选择下一步的动作
价值函数,用价值函数评估智能体当前的状态
模型,表示智能体对环境状态的理解
策略
策略是一个函数,将输入状态映射到动作
随机性策略,使用$\pi$函数,$\pi(a|s)=p(a_t=a|s_t=s)$,根据概率分布选择动作。
确定性策略,$a = argmax\pi(a|s)$,容易被对手预测
价值函数
价值函数的值是对未来奖励的预测,用于评估状态的好坏。
折扣因子是将收益从时间向收益的转换
价值函数定义:
$$V_{\pi}(s)=E_{\pi}[G_t|s_t=s]=E[\sum_{k=0}^{\INF}\gamma^kr_{t+k+1}|s_t=s],对于所有的s\inS$$
另一种价值函数Q函数,包含两个变量,状态和动作,定义为
$$Q_{\pi}(s)=E_{\pi}[G_t|s_t=s, a_t=a]=E[\sum_{k=0}^{\INF}\gamma^kr_{t+k+1}|s_t=s, a_t=a]$$
Q函数是需要学习的函数,可以获取某个状态需要采取的最优动作。
模型
模型由转移概率和奖励函数组成。
转移概率:
$$p^a_{ss’}=p(s_{t+1}=s’|s_t=s,a_t=a)$$
奖励函数值当前状态采取某动作,可以获得的奖励:
$$R(s, a)=E[r_{t+1}|s_t=s, a_t=a]$$
策略 + 价值函数 + 模型 => 马尔可夫决策过程
智能体分类
学习过程
基于策略的学习,基于策略的智能体,有策略梯度算法
基于价值的学习,基于价值的智能体,有Q学习,Sarsa算法,适用离散环境,连续环境下效果差
演员-评论员智能体,类似上述两者的集成
模型
有模型,通过学习状态的转移来采取策略,类似多了特征工程,可以一定程度减缓数据匮乏的问题
免模型,通过学习价值函数和策略函数做决策,泛化性强,消除了特征工程与真实环境之间的信息损失
学习与规划
学习与规划是序列决策的两个基本问题。
探索和利用
探索和利用是强化学习的两个核心问题。
探索-利用窘境
单步强化学习
知道每个动作的奖励
执行奖励最大的动作
单步强化学习对应K-臂赌博机模型。
仅探索法 (类似创业
仅利用法 (类似上班
马尔可夫决策过程
马尔可夫过程
掠过
马尔可夫性质
齐次马尔可夫,简化版
马尔可夫链
马尔可夫奖励过程
比马尔可夫链多了一个奖励函数
回报与价值函数
注意这里,回报、价值、奖励是不同的概念
奖励 r: 奖励是当前时刻触发的收益
回报 g: 回报是当前状态往后,直到最终时刻所获的所有收益
价值 v: 价值函数是回报的期望
贝尔曼方程
贝尔曼方程是价值函数的另一种形式
价值函数 = 即时奖励 + 未来所有奖励
可以通过价值函数的贝尔曼形式得到解析解,问题在于通常来说P是不知道的,需要学习
解析解的问题在于复杂度很高,对于高状态的矩阵算起来非常困难
迭代算法
蒙特卡洛
动态规划
时序差分学习
马尔可夫决策过程
决策过程中多一个智能体做决策的动作
价值函数 V
Q 函数
Q 函数是基于 V 函数的动作层面的空间划分
Q 是某个动作的价值,指站在此状态,选择某个动作的价值
V 是所有动作总空间的期望价值,指站在此状态,对所有动作的价值的期望
贝尔曼期望方程
先把价值函数写成Q函数的分解,再对Q函数作贝尔曼方程分解,得到一个贝尔曼期望方程的一种形式
先把价值函数写成即时奖励和后续奖励的分解,再将后续奖励中的价值函数作贝尔曼方程分解,得到贝尔曼期望方程的另一种形式
备份图
备份图描述了状态价值函数的计算分解
策略评估
前提:已经马尔可夫决策过程、策略
此时不断迭代,所有的价值函数最终都会收敛
预测与控制
预测:已知马尔可夫决策过程,策略,计算每个状态的价值
控制:已知马尔可夫决策过程,虚招最佳策略,以及最佳价值函数
动态规划
dp 解决同时有最优子结构和重叠子问题性质的问题。马尔可夫决策过程就具有这种性质,可以用动态规划去解。
马尔可夫决策过程中的策略评估
前提:已经马尔可夫决策过程、策略
将贝尔曼期望备份转换为迭代过程,直到收敛,这个过程是同步备份。
马尔可夫决策过程的控制
如何寻找最优策略,进而计算最佳价值函数
朴素方法:穷举
迭代算法:策略迭代、价值迭代
策略迭代
策略评估 + 策略改进
巨大的基础:马尔可夫过程已知。
- 初始化策略,和价值函数
- 根据当前策略,计算状态新价值函数
- 根据状态价值函数,推算Q 函数
- 最优化Q 函数,得到新策略
- 判断迭代是否结束,未结束,则跳转到2
基础:马尔可夫过程已知
策略 + 价值函数 –贝尔曼方程迭代–> 新价值函数
价值函数 –> Q 函数
最大化Q 函数 –> 新的策略
Q 函数可以看成一个Q 表格
这个是大致的策略迭代算法
价值迭代
最优性原理: 一个策略在状态s达到最优价值的充要条件是,任何可以从s到达的状态s’都达到了最优价值。
算法:
更新Q ,更新V 若干次
从最后的V中提取最优策略
表格型方法
使用查找表
比如蒙特卡洛、Q学习、Sarsa
有模型
使用概率函数和奖励函数来描述环境
免模型
因为很多时候环境未知,概率函数甚至奖励函数都未知。
此时免模型需要通过试探来估计概率函数等环境信息。
有模型和免模型
有模型可以直接从环境推导智能体
免模型需要不断和环境交互,迭代智能体
Q 表格
Q 表格是主要的训练对象
强化是指用下一个状态的价值来更新当前状态的价值。
免模型预测
前提:无法获取马尔可夫决策过程模型
蒙特卡洛方法
蒙特克罗使用采样,使用经验品君回报估计价值函数
优点:不需要状态转移函数和奖励函数,也不用动态规划中的自举。
缺点:只能用于有终止的马尔可夫决策过程
算法:
蒙特克罗和动态规划比较
- 蒙特克罗适用于环境未知,而且更新速度快
- 动态规划适用于有模型,但每次迭代需要更新所有状态,速度很慢
时序差分方法(TD)
免模型控制
策略迭代进行广义推广,兼容蒙特卡洛和时序差分,也就是广义策略迭代。
Sarsa 同策略时序差分控制
Q 学习 异策略时序差分控制
同策略和异策略的区别
一个例子:用Q 学习解决悬崖寻路问题
策略梯度
策略梯度算法
策略梯度实现技巧
1. 添加基线
2. 分配合适的分数
蒙特卡洛策略梯度
一个例子:用策略梯度算法解决悬崖寻路问题
近端策略优化
一个例子:用近端策略优化算法解决悬崖寻路问题
深度Q 网络
理解Flask架构
WSGI 协议
WSGI 是一种网络接口协议;在Web与Python之间构造了一层转换器;
当用户请求发到服务器后,服务器按照WSGI协议将请求转换为Python中的env变量与response函数;
通常开发人员从env中提取出必要的信息,作出反应后按照WSGI协议调用response函数,实现
WSGI协议的服务器会将返回值转换为Web响应。
Flask 架构
Flask 上下文
Flask 的架构与Flask 上下文的设计密切相关。
在Flask 应用开发代码中,有两个特点:
- 开发人员不需要处理过多的线程问题,全局变量request即可自动处理好。
- 开发人员不需要将请求作为参数或返回值在不同的函数间传来传去;
ContextVar + LocalProxy 解决方案
ContextVar 源码分析
1 | from contextvars import ContextVar |
ContextVar 是属于标准库的数据结构;旨在提供一个统一的变量入口;该变量在开发代码中唯一,在运行时会自动根据线程分离。
该标准在3.7加入python标准库,原作者也写了一个3.6的兼容版本:https://github.com/MagicStack/contextvars/blob/master/contextvars/__init__.py。
该版本的contextvar库是构建在thread.local上的。
在代码的最底部定义了一个全局变量_state用于管理所有的Context与ContextVar,而且该变量是一个thread.local创建的对象,这保证了不同线程间变量是分隔的;
比如Flask中的request上下文变量在Flask收到多个请求时,实际的数据是这样的:
上图左边Flask处理多请求;右边为对应线程中request在整个进程中的实际存储形式;
下面我们来看contextvars的源码:
首先最重要的是该模块中的全局变量_state
:
1 | _state = threading.local() |
该变量为一个local变量,会自己隔离不同线程中的变量;此外该变量没有声明在__all__
中,是一个模块级的全局变量,不用担心全局的命名冲突。
以及该变量对应的两个辅助函数:
1 | def _get_context(): |
_get_context
函数用于从获取_state
变量的 ‘context’ 属性,该’context’实际上是线程分离的(因为_state
是local变量,而’context’是_state
的属性);
若当前线程的’context’为None,则将其初始化为一个上下文对象Context,并返回;
_set_context
函数就很好理解了,传入一个上下文对象,将该上下文对象设置为当前线程的’context’。
然后我们来看上下文对象和上下文变量对象。其关系是,上下文对象为上下文变量对象的容器。我们先看上下文对象:
1 | class ContextMeta(type(collections.abc.Mapping)): |
Context 对象的结构如下:
_data
属性是一个映射字典,用于保存当前上下文中的上下文变量。
_prev_context
属性用于临时保存之前的上下文对象,用于临时上下文切换。
比较重要的是这个方法:__getitem__
__getitem__
方法实现了映射协议,该方法先检查传入的key是否为上下文变量ContextVar实例,如果不是则抛出异常,如果是则在_data
属性中以该实例为key,获取实际的保存的值;
接下来是ContextVar的代码:
1 | class ContextVarMeta(type): |
ContextVar 有两个属性:_name
与 _default
以及一个动态属性:name
_name
属性用于标记该ContextVar的名称,_default
用与指明该ContextVar的默认值;
name
返回当前保存的_name
值。
ContextVar 有三个重要的方法:get
, set
, reset
我们先聊get
方法
get
方法会先调用_get_context
加载当前线程的上下文,然后尝试在该上下文中查找当前ContextVar对应的值,并返回;如果该上下文没有当前ContextVar,则尝试返回默认值;
set
方法同样先调用_get_context
加载当前线程的上下文,然后获取该上下文的_data
,尝试先获取其ContextVar的旧值,再更新为新值,最终返回一个Token,该Token中保存了上下文对象,当前上下文变量对象以及旧值;
reset
方法传入一个Token,使用Token中的信息将当前ContextVar恢复到上一次的值;
总结一下:
_state
这个local 变量负责分离、管理线程变量;该对象是模块自身生成管理的;
Context
是一个上下文对象,用于管理上下文变量对象;该对象实现了映射协议,key值为ContextVar,val为对应的实际的值;
ContextVar
是一个上下文变量对象,与实际的值之间有一个对应关系;此外,该对象还实现了临时恢复的功能;
LocalProxy
Context 与 ContextVar帮助我们实现了变量的线程分离;但每次使用变量,总是需要调用some_var.get() 方法与some_var.set() 方法,非常的不优雅,而且容易产生语义上的误解;
werkzeug 针对ContextVar,设计了LocalProxy类,该类将ContextVar的某个属性封装起来,并将赋值,读值等方法内部转发委托给其封装的ContextVar或其某个属性;某种角度上,LocalProxy类是ContextVar的一种语法糖类;
AppContext + RequestContext
AppContext 与 RequestContext 实例在Flask中是全局变量,在所有线程中均可见,但是被local分离,互不影响;
我们检查global.py,可以看到这几个关键的全局变量声明:
1 | _cv_app: ContextVar["AppContext"] = ContextVar("flask.app_ctx") |
可以看到这里声明了两个上下文变量,分别是应用上下文变量_cv_app
与请求上下文变量_cv_request
;
而我们常用的 current_app
, g
, request
, session
都是它们的某个属性代理;
总结一下,全局环境中存在两个ContextVar变量,_cv_app
与_cv_request
,这两个变量负责管理current_app
,g
,request
,session
这些LocalProxy变量;
当我们使用_cv_app
这些ContextVar变量的时候,我们实际上使用的是当前线程对应的’context’字典中以_cv_app
作为key找到的值;
当我们使用g
这些LocalProxy变量的时候,我们实际上使用的是该LocalProxy对应的ContextVar变量本身或其某个属性;
如下图所示:
需要注意,以上的变量均是全局变量。若干的context是指不同线程中的context变量。
上下文推送
在Flask里,当我们需要使用这些LocalProxy变量时,我们需要将对应的上下文变量推送到当前上下文栈中。
在未进行上下文推送前,_cv_app
与 _cv_request
实际上是空对象,此时使用这些变量下的LocalProxy变量,往往会触发所谓的上下文错误。
在上下文推送后,这两个ContextVar会被更新,此时其对应的LocalProxy变量也才变得可用。
通常上下文推送是由Flask自动推送的,如果在开发期间需要使用,需要手动推送上下文变量。通常这个发生在初始化过程中,比如数据库初始化等等。
应用上下文
1 | class AppContext: |
这里的关键在于AppContext
维护了一个_cv_tokens
栈,用于保存之前的上下文;push
方法将全局_cv_app
设置为当前的AppContext
的实例;并将之前的_cv_app
的值以token
的形式存在栈里,方便后序恢复;
此外,该类实现了with协议,可以使用with语句激活上下文;
Flask 与 蓝图系统
蓝图实际上和FlaskApp本质上很像,两者实际上都是继承自Scaffold;
在实际开发中,几乎可以把蓝图和FlaskApp同等对待,甚至蓝图本身还可以注册蓝图;
这里我们着重讲一下蓝图的注册;
使用app.register_blueprint(blueprint)将blueprint注册到app.
register_blueprint
会调用blueprint的register方法做以下这些事情:
从blueprint构建蓝图的name
根据name判断是否已经有同名蓝图注册;如果有,则抛出异常(注意这里只是判断同名,不是判断是否已经注册过)
判断blueprint是否有自己的静态文件夹,如果有,就添加到路由规则集中;
判断blueprint是否之前被注册过了,如果注册已经注册过了,就将当前的蓝图以覆盖的形式更新注册;
将自身的命令接口注册到主app上;
更新blueprint的子blueprint的路由信息,并调用这些子blueprint的register方法,将这些子blueprint注册到主app上;
因此最终只会有一个路由规则集,那就是主app的路由规则集,其余所有蓝图及其子孙蓝图的路由规则集都会在合理变换后加到主app的路由规则集里;
路由系统
在FlaskAPP 开发过程中,每个App和蓝图都定义自己的路由系统,不过这只是开发阶段;在运行时,实际上只存在一套路由系统,那就是主App的路由系统,其余所有的子路由系统都在运行时做了合适的变换,并补充到了主App的路由系统中;
Flask 路由系统并不复杂,但在深入之前,我们先了解一些werkzeug中的路由组件;
Map 与 Rule
Map和Rule是werkzeug中负责管理路由的组件;Rule负责定义具体的路由规则,而Map则是Rule的容器,同时也负责根据路径对其保存的Rule进行查询,并给出路由终点;
一个来自官方文档的例子:
1 | url_map = Map([ |
传入请求的environ,调用Map的bind_to_environ方法,生成一个URLAdapter;
1 | adapter = url_map.bind_to_environ(request.environ) |
该adapter中保存着路由的终点以及传入的参数值;
1 | for endpoint, values in adapter.match(): |
这里我们需要注意的是,werkzeug中的路由系统仅仅完成了url到endpoint这一步,并没有进一步找到对应的视图函数;在Flask中,存在一个由endpoint到视图函数的字典,这个字典完成了拼图上的最后一块;
Flask 路由
推送上下文后,Flask会尝试做一次full_dispatch_request();
在full_dispatch_request中,Flask会做一些初始判断与对request的预处理,若一切顺利,Flask会再做一次dispatch_request();
在dispatch_request中,我们会发现Flask已经获取到负责路由请求的Rule,并根据其endpoint在Flask的view_functions中查询视图函数;后面便是对视图函数的调用了;
实际上Flask在请求传递进来,构造请求上下文时,就已经完成了路由,并将这些路由信息(如路由规则,路由终点,路由参数等)记录在了请求对象request中;
检查Flask的请求上下文的__init__函数,我们可以看到请求上下文的构造过程:
将上下文本身的app指向FlaskApp
根据FlaskApp提供的请求类与传入的environ构造请求对象,并赋值给request
使用app.create_url_adapter处理请求;在这个过程中,调用了werkzeug中的函数获取路由终点,并将结果保存在上下文的url_adapter变量中;
在请求上下文推送后,上下文会调用自己的match_request方法,尝试从自身url_adapter变量中获取路由终点,路由参数,并将这些信息绑定到request上,至此,路由信息才被写入请求中;
后面的逻辑就很平坦了,Flask从请求中获取路由终点与路由参数;以路由终点作为key,从view_functions中查询视图函数,并将路由参数作为参数调用视图函数,获取响应;
完成响应的构造与后序处理后,委托werkzeug返回给用户端;
视图函数与可拔插视图类
视图函数就是我们在web开发模型的MVC中常说的View,名副其实的视图函数;
通常视图函数就是我们的业务代码,按照正常的业务逻辑编写即可;其中有两点值得称道:
函数只用从请求链接中接受参数,并不需要传入响应本身;直接导入全局的request使用即可;
函数写完后只需要加一个装饰器即可自动注册到目标app或蓝图中,同时可以直接定义从链接中接受那些参数,非常直观方便;
Flask中优秀的上下文设计使得视图函数与请求解藕,使得开发逻辑更加清晰;
建议将后台的一些工具辅助函数单独写在一个模块中,而将业务逻辑相似的代码单独放在一起,方便开发管理;
这里我们重点讲一下可拔插视图类,这个工具让视图函数的抽象层级提高了一层,合理的利用可拔插视图类,我们可以利用继承,将相似的路由逻辑和业务逻辑抽离出来,提高我们的效率;你完全可以在工作的过程中不断自己总结,编写自己的可拔插视图类,这些代码将会在未来解放你的大量生产力;
我们来看一个简单的例子,这个例子来自Flask的官方文档:
我们需要将某个链接路由到对用户列表的查询,我们完全可以使用视图函数实现:
1 |
|
假设此时又出现一个需求,要求我们将某个链接路由到对商品列表的查询,此时你不得不在写一个逻辑类似的视图函数:
1 |
|
假设此时又出现一个需求…
这么编写非常冗余,而且有个致命的问题,万一某一天,你的老板决定要给这些查询加上一个额外的参数,用于单独查询某页的数据结果;这个时候你不得不重新修改所有的视图函数,而这些视图函数还可能分布在不同的地方,此外这种乏味单调的修改极容易出现错误;
此时有若干解决方法:
lisp程序员可能会使用宏,在程序运行时用宏自动生成对应的视图函数;也有可能想到使用函数式编程,先写一个工厂函数,在运行时用工厂函数即时计算对应的视图函数;等等
这里Flask提供一种类似函数式编程思想的工具:可拔插视图类;
首先我们将相似的逻辑抽象出来:
1 | from flask.views import View |
然后,我们继承该类,完善处理具体逻辑的子类:
1 | class UserView(ListView): |
此时如果需要临时加上额外参数,那么仅修改基类即可完成所有类的逻辑更新;
某种角度上来说,可拔插视图类的思想有些类似函数式编程中的高阶函数;不同的是在面向对象中,新的类通过继承的方式产生,通过新类的重写实现子类的定制,将相似逻辑写在基类中的方式将相似的逻辑抽离出来;而在函数式编程中,新的函数通过某个函数的计算生成,通过传参的方式定制新函数的行为,将相似逻辑写在工厂函数中的方式将相似的逻辑抽离出来;
模板系统
模板系统在视图函数返回与响应生成这两个时间点之间发挥作用;
视图函数返回的并不是最终的响应,而是最终响应的一些必要信息,保存在变量rv中,需要以此为参数,调用FlaskApp的finalize_request()才能得到最终的响应对象;
通常生成rv过程中会涉及到模板的渲染;
Flask默认使用jinja2作为渲染引擎;
开发过程中,最常用的渲染函数有render_template以及stream_template;前者用于渲染常规响应,后者用于渲染流式响应;
模板系统通常做两件事情:
根据提供的模板名称寻找到模板本身
在提供的上下文的帮助下,填充并渲染模板,并将结果返回
模板路由
模板渲染
找到渲染模板后,实际的渲染过程发生在_render
与_stream
中,两者接受相同的参数:app、template、context;但逻辑有些许差别,下面我们分开讨论:
1 | def _render(app: "Flask", template: Template, context: t.Dict[str, t.Any]) -> str: |
_render
中,首先用app的一些信息更新了context中的信息;注意这里的上下文是指用于模板渲染的上下文,而不是我们前面讨论的应用上下文等;这里更新的原因很简单,模板渲染很可能也需要一些来自app的信息;
中间插入了一个信号发送,这里暂时不展开;
紧接着用context作为上下文对模板进行渲染,获取到rv,这里的rv是字符串;
又一个信号发送;
返回渲染后的字符串rv作为渲染结果
_stream
函数与_render
类似:
1 |
|
不同的地方是这里使用template.generate替代了template_render,并且返回的是一个字符串迭代器作为rv,而非字符串;
响应
在full_dispatch_request方法的最后,获取了rv并调用finalize_request方法,完成响应的构建;
关键的响应生成逻辑写在了make_response中,这个方法非常长,但逻辑并不复杂,这里就不放出来,仅做一些逻辑说明:
make_response接受rv作为参数,并返回一个响应对象;
前面的很多叙述省略了很多异常处理的情况,真实情况,rv可能的类型很多:str、bytes、dict、list、generator、iterator、tuple;这里对其类型做了判断,并分开处理;
tuple
rv必须是长度为2或3的tuple。长度为2时应当为(rv, headers)或(rv, status)的形式将其解包;None
抛出异常不是响应类
str、bytes、bytearray
使用rv作为参数构造响应对象,并赋值回给rvdict、list
使用rv作为参数构造json响应,并赋值回给rvBaseResposne、callable
使用response_class.force_type将rv强制转换为响应对象,并返回给rv
最终rv必定为响应类型,更新rv的headers及status后,作为最终响应返回;
这里需要注意的只有一点,就是rv作为callable对象返回的情况,此时rv应当接受两个参数:environ与start_response;
基于python的算法实现
定式
用数学去分析和思考问题
- 搜索类
- 将问题看成一个代数空间
- 算法的每个部分的作用,通常前面会先处理特殊情况,然后对输入做一些规范化,转换为一般问题
- 循环要满足循环不变的性质,这在算法证明中很重要
- 从信息的角度看问题,我有哪些信息,使用那种数据结构可以充分高效地存储这些信息,以及基于这种数据结构,使用哪些算法可以充分的用到所有信息。
- 除了信息本身,被研究问题的一些数学性质可以很好的压缩解的代数空间,从而优化算法。
条件 condition
(增量式) 创建某种语境
1 | # context 1 or context 2 |
(分支式) 创建某种语境
1 | # context 1 of context 2 or ... |
(分支式) 分类处理
1 | # context 1 of context 2 or ... |
序列 sequence
一些有关序列的小技巧, 帮助你快速自信的写出正确的代码
python 以 0 作为序列首位索引
使用左闭右开区间,length 必定数组越界,length-1 为最后索引
好处是处理 start, length,end 的关系时无需+-1
1. 索引遍历数组
1 | for _ in range(len(array)): # [0, length) -> [0, length-1] |
2. start + length 切片
[start: start+length]
3. end + length 切片
[(end-1)-length, (end-1)]
4. start + end 计算 length
length = end - start
循环 loop
1 | while not target_context: |
递归 recurse
编码风格
- 终止条件写在最前面
操作式递归
- 如何理解操作式递归代码
阅读递归代码时要将递归链/树视为一个整体,而递归函数就是逐步零碎的调整整个链/树,直到触发终止条件;
实际上终止条件在大多数的递归中都不会触发,而是一种兜底,面向链/树整体的全局终止条件;
在递归点前后的代码分别称为前序代码和后序代码;前序代码对整体的调整由浅入深;直到遇到递归点,此时深度再进一层;
在触发终止条件后,后序代码开始对整体由深返浅地调整,最终结束。
计算式递归
- 如何理解计算式递归代码
同样的,首先定义终止条件,并需要注意到,终止条件中返回的值将是整个递归树的基础值;所有结果都将由基础值组合得到;
通常计算式递归并没有什么前序代码,如果有,那大部分是为了计算返回值而设计的;
计算式递归需要有一个锚点接受后续递归的计算结果;通常这个锚点是多个;用于组合成当前的计算值并返回;
因此计算式递归中有两种值:从终止条件计算的基础值;由基础值组合而成的组合值;
通常终止条件会与前序代码混在一起,用于计算基础值;后序代码通常用于计算组合值;
例题1. 计算斐波那契数列
1 | def feb_rec(n): |
例题2. 计算LCA
1 | def LCA(node, p, q): |
例题3. 判断镜像树
1 | def mirror(root): |
1. 链表倒转
链表 [1, 3, 5, 9, 10]
递归倒转为 [10, 9, 5, 3, 1]
1 | def rec(node: ListNode): |
2. 镜像树判断
二分 binary
二分是建立在以排序序列上的搜索算法
采用左必右开的习惯
需要注意的点:
何时停止
非严格排序时控制搜索位置
1 | def left_binsearch(nums, left, right, value): |
1 | def right_binsearch(nums, left, right, value): |
杂项 misc
二维数组
二维数组使用r,c坐标系
1. 二维数组的快速初始化
1 | grid = [[0] * width for _ in range(height)] |
此时
height = len(grid) 与 row 相关
width = len(grid[0]) 与 col 相关
2. 二维数组遍历
- 正常遍历(行优先,从左向右,自上而下)
1 | for r in range(len(grid)): |
- 遍历对角(r==c)
1 | for r in range(len(grid)): |
- 遍历反对角(r+c==len-1)
1 | for r in range(len(grid)): |
- 遍历对角方向(加上偏移)
3. 二维数组游走
1 | d = {(+1, +1), (+1, -1), (-1, +1), (-1, -1)} |
pyhthon specific
高效倍增与折半
1 | i <= 1 |
判断奇偶
1 | n & 1 == True # 奇数 否则为偶数 |
倍增
1 | n <<= 1 |
减半并向下取整
通常用于完全树与堆的数组实现中
1 | n >>= 1 |
数据结构
栈
栈是简化的树
栈通常与递归联系起来
单调栈
单调栈是栈的加强,在栈的基础上要求栈内部的元素有序。
单调栈的用途并不广泛,而是集中处理一系列问题。
预先定义好顺序信息后,单调只需要两个元素即可破坏。因此当某元素需要入栈时,需要检测当前元素与栈顶元素,判断是否破坏了栈内部的单调性质,如果破坏了,那么说明栈顶元素需要抛出。重复检查,直到不破坏栈内部的单调性,再将元素入栈。
和栈不同,单调栈通常并不和递归联系起来。
单调栈实践
1 | stack = [] |
并查集
并查集通常用于处理等价类
1. 并查集的字典实现
字典结构 + 函数式
前提:这些类(数学意义上的类)要可以hash,否则实现上做不了字典的key
1 | # 初始化 |
2. 并查集的类实现
类 + 对象式
TODO
1 |
堆
堆的数组实现
基于堆是完全树的拓扑结构;
数组从1开始索引;索引0位存储堆的当前存数量;
数组索引为n的节点,其父节点索引为n//2,左节点为2n,右节点为2n+1;
最后一个节点的索引heap[-1]
这里是最小堆的实现, 因此要将大的数下沉
1 | # 初始化 |
堆的树实现
略, 使用双向树, 即节点不仅需要知道子节点, 还需要知道父节点
图
表示图的数据结构
邻接矩阵
邻接矩阵在处理一些代数相关的问题时,比较常用
邻接表
1 | from collections import defaultdict |
⚠️: 对于无向图, 切记初始化的时候不要忘记记录另一个方向的边
算法
排序
冒泡排序
最简单的一种排序方式之一
1 | def sort(nums): |
选择排序
一种与插入排序对偶的排序方式
1 | def sort(nums): |
插入排序
另一种最简单的排序方式之一
1 | def sort(nums): |
希尔排序
基于插入排序的改良
1 | def sort(nums): |
归并排序
一种后递归排序
V 形数据排序
锯齿数据排序
1 | aux = [0] * len(nums) |
快速排序
归并排序的一种改良
1 | def partition(nums, lo, hi): |
另一种写法
1 | def quick_sort(nums, l , r): |
作者:jyd
链接:https://leetcode.cn/problems/ba-shu-zu-pai-cheng-zui-xiao-de-shu-lcof/solution/mian-shi-ti-45-ba-shu-zu-pai-cheng-zui-xiao-de-s-4/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
堆排序
一种动态的惰性排序
搜索
大多数常见的搜索算法依赖树(图)状数据结构
二分搜索
二分搜索利用了数据的顺序性质,这跳过了许多潜在的错误目标,以此提高效率。
左二分搜索
1 | def bisect_left(sequence, target, left, right): |
右二分搜索
1 | def bisect_right(sequence, target, left, right): |
DFS 搜索
BFS 搜索
BFS
本质上为一种暴力搜索,但是搜索的模式顺序是固定的。
如果利用数据的某些数学性质进行剪枝,则可以大幅度优化BFS
。
1 | from collections import deque |
双向BFS
BFS 的变体,可以减少搜索空间,同时压缩时间
1 | from collections import deque |
动态规划
最长子序列
最长子串
最长递增子序列
子串最大和
0/1 背包问题
0/1 是指挑选物品,拿或者不拿,而拿做多只能拿一个
限制:背包限制,每个物品有体积,而背包容积有限
优化目标:每个物品具有某种效益,最大化总效益
SOLVE:
定义 dp[i][j] 为从前i个物品中选出总重量不超过j时总价值的最大值
已知 dp[i-1][j],现在物品范围从前i-1个扩大到前i个,
dp表中,每行为i,每列为j
内循环中,j,也就是背包容量逐渐增大
外循环中,i,也就是可选的物品范围逐渐增大
当下面临的问题是,当前新增的物品能不能装下,若能装下,是拿还是不拿
1 | for i in range(1, n+1): |
- LIS 问题
串算法
- 字符
假定一集合,该集合内元素个数有限且良序,则该集合可以称为一个字符集;
该集合内的所有元素称为字符。
- 字符串
给定一指定字符集,由该字符集内字符组成的有序排列,称为字符串。
字符串定式
- 索引
1 | # 字符串首 |
字符串基础算法
双指针
同向快慢指针
对向指针
AC 树
dp
经典例题: 计算两字符串的编辑距离
前缀函数
前置定义
对于一个长度为n的字符串s,以及一个小于n的整数i,
记其前缀s[0:i]与后缀s[n-i:n]为一个前后缀组合
前缀函数定义
对于长度为n的字符串,记其前缀函数为$\pi(i)$
其定义域为[1, n] 之间的整数;
其定义为前缀s[0:i]的所有前后缀组合中,前后缀相等且长度最长的组合的长度。
朴素前缀函数计算
1 | def prefix_function(s): |
前缀函数优化(一)
注意到 pi[i+1] 最多比 p[i]
大1,这时,当且仅当新增加的字符s[i+1]与s[p[i]-1+1]相同
因此,j从i位置逐渐扫描到0的过程中,超出p[i]部分的扫描是多余的。
1 | def prefix_function(s): |
前缀函数优化(二)
当新增字符s[i+1]与s[p[i]]不同时,我们的思路为找到s[0:i]中第二长的前后缀组合,再检测
1 | def prefix_function(s): |
KMP
后缀数组
倍增法计算后缀数组
1 | from itertools import zip_longest, islice |
判断字符是否由重复字符组成
1 | def repeated(s): |
判断字符串是否回文
1 | def check(s): |
LCP 最长公共前缀
若干字符中查找最长的公共前缀,如果不存在,则返回’’
- 使用遍历
可以注意到
LCP(s1, s2, …, sn) = LCP(s1, LCP(s2, LCP(s3, LCP(…LCP(sn-1, sn)))))
这本质上是个 reduce 的关系,先写一个寻找两个字符串最大公共前缀的函数lcp
再将lcp用reduce函数作用[s1, s2, …, sn]的迭代列表上即可
某个字符串查询任意两后缀的最长公共子串
LCP例子
使用倍增预先计算QMR数组,再用QMR数组推导结果
LIS 最长递增子序列
使用前缀数组,注意到前面的前缀LIS可以推导其后紧接着的前缀LIS
这是一个广义reduce问题
记前缀为q1, q2, …, qk,则有关系:
$$LIS(q_n) = max({LIS(q_i) | i < n & q_i[-1] < q_n[-1]}) + 1$$
之所以是广义reduce,
是指每一个后续指是有前面所有数值reduce出来的,而非狭义的相邻两个
最长递增子串
同上,为广义reduce问题
$$LIS(q_n) = q_n[1] > q_{n-1} ? LIS(q_{n-1}+1) : 1 $$
LCS 最长公共子序列
前缀数组+DP 问题
1 | if i == 0 or j == 0: |
最长公共子串
前缀数组+DP 问题
1 | if i == 0 or j == 0: |
子串性质
长串包含子串,记N(S, s) 为S中s的个数,若ls包含ss,则有N(S, ls) <= N(S, ss)
可以这样计算N(S, s):
1 | def N(S, s): |
一些技巧
对于数组 a = range(10)
1. 差分
差分数组为 diff[i] = a[i] - a[i-1], i 从 0 到 9,其中d[0] = 0
且有如下性质:
差分数组与原数组的关系
d[i+1] 指 a[i] 变换到 a[i+1] 的增量;
因此有:
a[i] + d[i+1] + d[i+2] + … + d[j] = a[j]
因此有:
a[j] - a[i] = sum([d[i+1],…, d[j]])
也就是说:
a[i] 到 a[j] 的增量为 sum(d): (i, j] 左开右闭原则,中间的差分项为j-i个
差分数组增减与原数组对应的变换
d[i] += n
表示a[i] 及之后的数组全部断层平移n个单位;
d[i] += n; d[j] -= n; j >= i;
表示a[i] 到a[j]内的数组全部断层平移n个单位;包含a[i]但不包含a[j];左闭右开;
2. 前缀和
前缀和s[i] = sum(a): [0, i]
3. 倍增
倍增是一种思想,不是明确的算法。其核心的想法基于下面这个事实:
对于任意大于0的整数n,总是存在一个整数i,$2^{i-1}<n<2^i$
使得[1, n]之间的任何整数均可由${2^k: k\inZ&k[1,
i]}$中的若干元素加和而的,且每个元素至多取一次
实际上任何数字均可以这样对应,这也是二进制的原理。
其优点往往在于可以利用以往的结果来推演后续结果,而减少了重复计算;
比如后面的倍增法计算后缀数组
LCA 问题
1 | def LCA(node, left, right): |
简单图算法
邻接表
数组实现
1 | # 点数 与 边数 |
字典实现
1 | adj_table = {} |
- 读入边
1 | for start, end, weight in edges: |
- 遍历某个点的所有边
1 | for end, weight in adj_table[start]: |
defaultdict实现
1 | from collections import defaultdict |
- 读入边
1 | for start, end, weight in edges: |
- 遍历某个节点的邻接点
遍历节点s
1 | for end, weight in adj_table[s]: |
邻接矩阵
1 | num_vtex = 20 |
n次邻接矩阵
到达矩阵
邻接矩阵中元素仅有0、1;0表示无法一步到达,1表示可以一步到达。
前提:简单图
记方阵A_{nn}为这样的单步邻接矩阵,记A^k_{nn}为A_{nn}的k次矩阵幂,那么A^k_{nn}[i][j]则为k步从点i到点j的路径数目。
参考离散数学。
概率转移矩阵
参考随机过程。
快幂算法
快幂算法有点类似倍增法,数学基础都是二进制。
假设我们需要进行n次幂乘
- 找到一个k,使得2^k<=n<2^{k+1}
- 计算X^{(i)}=A^{2^i},其中i\in[0, k],需要注意的是,这里不断倍增即可,即X^{(i)}=X^{(i-1}*X^{(i-1)}
- 找到n的二进制表示,根据二进制表示用X^{(i)}组合出乘积的形式
最短路径
Floyd-Warshall 算法
求任意两点间的最短距离
使用邻接矩阵
1 | for k in range(num_vtex): |
Dijkstra 算法
求某点到其余点的最短距离
将点分为两组,P表示已找到最短距离的点的集合,Q表示未找到最短距离的点的集合
- 初始化。将原点加入P中,并标记原点的最短距离为0;
- 迭代。在Q中找到距离原点最近的点,将其加入P中。对其所有出边进行松弛操作;
- 直到Q为空集,迭代结束
维护一张表,该表记录某个点是否为P集合点,以及前点。
1 | from math import inf |
Bellman-Ford 算法
求任意两点间的最小距离,可处理负权重边
用邻接表
1 | for _ in range(num_vtex-1): |
搜索
深度优先
判断两点是否连通,计算当前连通分量内的点数
1 | visited = [False] // |
计算连通分量
广度优先
需要使用一个队列
1 | total = 0 |
判断成环
dfs 判断
适用于有环图与无环图
不断深度遍历,若出现某节点遍历到已遍历节点,则说明有环,否则无环;可以利用n个点的信息提前结束遍历
Union-Find 判断 (更适合动态情况)
适用于无环图
遍历所有边,判断边两点的根结点
若不一致,则无环,将两点并起来
若根结点一致,则有环,结束遍历
拓扑排序 判断
适用于有环图
不断删除入度为0的节点,若最终全部删除完,则无环,否则有环
最小生成树
两种最小生成树的原理都是切分定理,Krustra处理边,Prime处理点
注意:n个点的最小生成树由n-1条边组成,可以用此提前结束循环
Krustra
取一个空集,不断从边中取出最小边,试图加入该集合
若使集合成环,则舍弃;否则加入集合
最终大小为n-1的边集合必定组成最小生成树
算法的关键在于判断成环
Prime
取一个空集,随机取一个点加入空集,作为初始数据
遍历该集合所有外连边,取最小边,加入该集合
最终大小为n的点集合的边必定组成最小生成树
算法的关键在于维护、遍历外连边
高级算法
AC 自动机
多叉树 + trie
概念
推荐b站的视频
python 实现
- 定义树节点
1 | class Node: |
扫描 pattern
1
2
3
4
5
6
7
8
9
10def scan(patterns):
root = Node('')
for pattern in patterns:
p = root
for char in pattern:
if char not in p.children:
p.children[char] = Node(char, parent=p)
p = p.children[char]
p.words = len(pattern)
return root构建 failed 指针
1 | def build(root): |
- 查询
AD 自动微分
1 | import math |
LRU 缓存
LRU 缓存的另一种说法:保留插入顺序的字典
为了保证查找,插入的O(1)复杂度,我们使用字典作为缓存,此外使用双向链表来记录key的访问
1 | class DLinkedNode: |
MISC 一些重要的python细节
列表和字典key的查询方式并不相同,时间复杂度也不是一个量级
1 | # 先构建列表,字典,集合 |
列表使用线性查找O(n),而字典使用hash查找O(1)
RMQ 问题
ST 算法
参考: RMQ问题
ST 算法先对数据进行预处理,为一种离线算法,适合数据不变情况,
动态情况考虑使用线段树
假设原始数据:
nums = [3, 4, 2, 2, 2, 4, 2, 0, 0, 0, 0, 1]
先计算预处理数组
1
2
3
4
5
6
7
8
9
10
11
12length = int(math.log2(len(array))) + 1
rmq = [[0]* length for _ in range(len(nums))]
def rmq_init():
for l, num in enumerate(nums):
rmq[l][0] = num
for r in range(1, length):
for l in range(len(nums)):
if l + (1 << (r-1)) >= len(nums):
break
rmq[l][r] = max(rmq[l, r-1],rmq[l + (1 << (r-1))][r-1] )查询
1
2
3def rmq_query(l, r):
k = int(math.log2(r-l+1))
return max(rmq[l][k], rmq[r-(1<<k)+1][k])