先说总结:某些资源搁浅在网盘上理论上是救不了的了,感觉写后台的好兄弟应该是有认真考虑过这个问题的。要么老老实实多备个硬盘,要么就加密上传。
青年大学习API分析+自动打卡姬
事不过三,确实是被好几次“10分钟倒计时”折磨到了。把手机扔一边放个视频不是问题,问题是中间还得手动点个地方,还可能答几道题,被烦到了,于是有了此文。(适用于广东共青团,其他地方未知)
抓包分析#
从电脑端的“12355青年之声”开始,用Fiddler抓包,如图:

第一次登录需要微信的授权,紫线以上都是微信的授权请求,而蓝线就是个人主页了,右下角可以看到纯文本形式的样子。不过没想到微信接入居然还是用http,真不怕中间人劫持吗(虽然也没啥好劫持就是了,最多拿拿个人资料)
微信授权的作用范围是tuanapi.12355.net开头的请求,内部用Java的Session Cookie进行追踪,在点开始学习后,会有这么一个请求,然后跳转到最新一期的学习页面。(下面的youthstudy.12355.net/h5/就是开始学习的页面了)

一波胡乱分析,保持登录状态无非就两个要素,一个是Session Cookie(微信授权给tuanapi请求的那个set-cookie),另外一个是url中的mid参数了。万幸的是,这个接口只验证了mid,没检查cookie是否存在(即使没有cookie,mid合理时请求依然会返回成功,而mid没给的时候会提示bad request)。如果真要这个Session Cookie,可能还真一时半会不能解决,毕竟这需要伪造微信授权了,已经超出目前能力范围了……
那么mid从何而来,Ctrl+F查找一番,之前一个请求被高亮标出了:

它包含了获取当前用户的所有数据,其中有一栏就是mid=xxxx,直接搬过来用就好了。这个请求就要用到头疼的Session Cookie了。
在正常情况下,输入完来自……之后,就会跳转到当前最新的一期,这时候的请求是这样的,章节ID和url就拿到手了:

由于session cookie是在tuanapi.12355.net域名下有效的,因此青年大学习(youthstudy.12355.net开头的API请求)需要另外一个认证机制,这里它用的是token,就是上图红色框出来的那一段。它从何来,就在前面一点的一个post请求,post的内容就是前面用mid请求的那个API返回的URL后面的那一坨sign,token就在响应JSON的data下的entity里面,就叫token。注意这里的header也有X-Litemall-IdentiFication和X-Litemall-Token,只不过token留空了而已。

获取完最新一期的数据后,反手就是POST保存学习的进度,内容很简单,就一个章节ID,前面获取最新章节的id就是了(那个pid不是,别搞混了): (真良心啊,是我我就写播完视频之后用一段js触发保存了,什么没保存?那麻烦请重新看一遍)

由于我们还要学完截个图,所以还得整个 假装已经 学完的页面出来,不过既然有URL,翻翻图片就好了,就这个以..../end.jpg结尾的文件,然后自己写个网页,套点css,用微信打开,截个图走人就好了。

实现代码#
整个代码如下:
1 | #! /usr/bin/python3 |
有一个int类型的必要参数:--mid,这个靠自己抓包了,爱莫能助。复制粘贴保存为xxx.py然后用python xxx.py --mid xxx差不多就这样了。其他参数--save_chapter_file用于避免多次请求保存(虽然有没有重复请求其实都差不多,但是科技玩家还是低调一点比较好),--deploy_webpage_path是生成的网页地址,这里默认是我apache2服务器的根目录下的youth_study.html:https://cdn.zhouxuebin.club/youth_study.html
无root权限下hadoop与map reduce的完全分布式搭建方案
首先说用一下,用到了域名,纯IP地址理论上也是可以的,不过没试。网上搜了一大堆教程,全都要求改/etc/hosts,对于一个没有root权限的用户来说,后面的教程全部都是一纸废话罢了。
这里就记录下自己的一次搭建过程,踩过的坑。
搭建主要有两部分,一个是Hadoop的分布式文件存储系统HDFS,另外一个是基于Yarn的Map Reduce框架。
推荐系统中的常用指标计算
算是给自己整理一下吧。主要是MF和FM的对比。
Toy Matrix#
假设有那么个用户数为6,物品数为5的大小为$6\times 5$的评分矩阵:
空缺的评分用0填充。
评分预测#
MF模型#
这个是相对比较简单的,将MF的两个矩阵算一下点积,就能得到所有用户对所有物品的评分$\hat R$。
FM模型#
FM对输入数据$\bm x$(假设是行向量)的表示一般可以分为三部分:代表用户ID的one-hot向量$\bm{x}_u\in\mathbb{R}^6$,代表物品ID的one-hot向量$\bm{x}_i\in\mathbb{R}^5$,以及其他与该评分有关的上下文(context)特征向量$\bm{x}_c\in\mathbb{R}^c$($c$为特征维度数)。即:$\bm x = [\bm x_u, \bm x_i, \bm x_c]$。
如果context是跟item挂钩的,即每个item下的所有评分都共享同一个context特征的话,事情就好办多了(至少这次跑的实验是这样的)。因此,FM的预测公式可以分解为与item相关以及与user相关两部分,跟MF模型相似。
原本计算FM的实现如下,$X$为行向量,$\bm w$为列向量,而$V$为$k\times(11+c)$的交叉项权重矩阵。
1 | y_hat = w0 + X * w - sum((X .^ 2) * (V' .^ 2), 2) / 2 + sum((X * V') .^ 2, 2) / 2; |
由于用户ID和物品ID都是one-hot向量,而context是跟item挂钩的,可以简化成:1
y_hat = w0 + w(u) + w(i) + Xc(i) * wc - sum((Vu(u)' .^ 2) + (Vi(i)' .^ 2) + (Xc(i) .^ 2) * (Vc' .^ 2), 2) / 2 + sum((Vu(u)' + Vi(i)' + Xc(i) * Vc') .^ 2, 2) / 2
其中$u$和$i$是每条评分的用户和物品的ID。
然后跟MF一样,拆成与item挂钩的一部分:1
2
3linear_i = w_item + ctx * w_ctx; % [n_item, 1]
cross_i_pos = V_item' + ctx * V_ctx'; % [n_item, k]
cross_i_neg = V_item' .^ 2 + (ctx .^ 2) * (V_ctx' .^ 2); % [n_item, k]
ctx为n_item$\times c$的context特征矩阵。
以及跟user挂钩的另一部分:1
2
3linear_u = w_user; % [n_user, 1]
cross_u_pos = V_user'; % [n_user, k]
cross_u_neg = V_user' .^ 2; % [n_user, k]
最后对每个用户$u$的,预测公式如下:1
y_hat = w0 + linear_u(u) + linear_i + sum(cross_u_pos(u) + cross_i_pos, 2)/2 - sum(cross_u_neg(u) + cross_i_neg, 2)/2; % [n_item, 1]
对用户没评过分的item,把itemID对应的位置置1,然后补上这个item的context特征进行预测就行了。
指标计算#
Precision@N#
N指的是推荐列表的长度。对推荐系统而言,前N个item的预测标签为1,而在后面的均为0。而对于ground truth标签,用户为其评过分则为1,否则为0。
For个example,假如$N=3$,推荐系统为第一个user生成的推荐列表为1,3,5,4,2。
| item ID | 预测 | GT |
|---|---|---|
| 1 | 1 | 1 |
| 3 | 1 | 1 |
| 5 | 1 | 0 |
| 4 | 0 | 1 |
| 2 | 0 | 0 |
因此TP=2,FP=1,Precision@3=TP/(TP+FP)=2/3=0.667。
实际上,计算可以简化为Pre@N=TP/N,TP为true positive的item数。代表的含义是前N个item列表中出现用户评过分的item的概率。
Recall@N#
跟上面类似,FN指的是出现在第N个item后面,用户有评过分的item数,如item 4。
因此,FN=1,Recall@3=TP/(TP+FN)=2/3=0.667。
实际上,计算也可以简化为Rec@N=TP/Np,Np为当前用户评过分的item个数。代表的含义是用户评过分的item出现在前N个item列表中的概率。这个数值是随N递增而单调递增的。
Average Precision#
这个指标综合评估Precision和Recall的质量,跟N无关。顾名思义,AP是对Precision求均值,而具体做法则是以recall为横坐标,precision为纵坐标,对precision求均值。人懒,原理就不解释了。
Normalized Discounted Cumulative Gain@N (NDCG@N)#
好像是这么拼来着吧?平时都直接喊的嗯滴希鸡。
推荐系统按照所有item预测的评分分数做个倒序排序,得到一个item列表:
| item ID | 评分$r$ | 折损因子$d$ |
|---|---|---|
| 1 | 4 | log2(2) |
| 3 | 5 | log2(3) |
| 5 | 0 | log2(4) |
| 4 | 1 | log2(5) |
| 2 | 0 | log2(6) |
Cumulative Gain,既然是cumulative,那自然就少不了sum嘛,CG@N=sum(2^r-1),r按照生成item列表的顺序来。有些实现版本则是CG@N=sum(r),没有取指数。
同样假设$N=3$,CG@3=(2^4-1)+(2^5-1)=46。
Discounted Cumulative Gain,多了个折损因子,DCG@N=CG@N/d。DCG@3=(2^4-1)/log2(2) + (2^5-1)/log2(3)=15+31/1.585=34.5588。
而Normalized则需要计算理想的DCG,即Ideal DCG(IDCG),它是按照评分倒序排的:
| item ID | 评分$r$ | 折损因子$d$ |
|---|---|---|
| 3 | 5 | log2(2) |
| 1 | 4 | log2(3) |
| 4 | 1 | log2(4) |
| 2 | 0 | log2(5) |
| 5 | 0 | log2(6) |
根据上表算DCG得到IDCG@3=DCG@3=(2^5-1)/log2(2) + (2^4-1)/log2(3) + (2^1-1)/log2(4)=31+15/1.585+1/2=40.9639。
最后的NDCG@N=DCG@N/IDCG@N=34.5588/40.9639=0.8436。
而某些论文中(这里就不点名是谁啦),NDCG的计算只考虑了用户评过分的物品,而未评过分的物品则不参与计算,因此实际计算NDCG时的列表如下(即去掉评分为0的item):
| item ID | 评分$r$ | 折损因子$d$ |
|---|---|---|
| 1 | 4 | log2(2) |
| 3 | 5 | log2(3) |
| 4 | 1 | log2(4) |
计算IDCG时,是否去掉评分为0的item其实不影响结果,毕竟它们全排到最后面了嘛,求sum的时候都是0。
最后算出的DCG@3=15+31/1.585+1/2=35.0588。即NDCG@3=35.0588/40.9639=0.8558。用这种计算方法得出来的NDCG是偏高的,不过嘛,作者开心就好。