面写了一个知乎爬虫、API 和小程序一条龙第一弹,反响还不错,于是在这些天的空闲时间里,我又优化下代码,并且把服务部署到了云服务器上,开放了 API 供需要的小伙伴使用。
也有很多人要源代码看看,想自己动手实践下,今天就把代码放出来,写的不好,仅供参考,也欢迎一起讨论维护!
功能增强之token
因为准备开放 API 接口出来,所以考虑了下,还是做一些简单的验证,毕竟安全措施做的好,你好我也好!
首先我们先来看下整体的请求流程
客户端先通过 getToken 接口来获取一个具有时间期限的 token 信息,然后再携带该 token 信息访问对应的数据接口
token 实现
我这里使用第三方库 itsdangerous 来做 token 签名
代码语言:javascript复制from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
itsdangerous 提供了多种生成签名令牌的方式,我这里选择的 TimedJSONWebSignatureSerializer 可以生成一种具有过期时间的 JSON Web 签名,这样我们也就可以控制我们所签发的 token 是具有时效性的。
生成签名并加密成 token
代码语言:javascript复制access_token_gen = Serializer(secret_key=secret_key, salt=salt, expires_in=access_token_expires_in)
timtstamp = time.time()
access_token = access_token_gen.dumps({
"userid": userid,
"iat": timtstamp
})
然后在需要解析 token 时,只要调用 loads 即可
代码语言:javascript复制s = Serializer(secret_key=secret_key, salt=salt)
data = s.loads(token)
访问限制装饰器
装饰器是 Python 语言的一大利器,我们当然要好好利用起来了。
在最开始的设计中,我们的路由都是可以直接访问的,没有任何限制
代码语言:javascript复制@api.route('/api/zhihu/hot/', methods=['GET', 'POST'])
def zhihu_api_data():
pass
现在我们想达到一种效果,就是不改变当前视图函数的写法,还要增加访问限制,只有携带了正确 token 的请求才能够正确访问对应的路由
代码语言:javascript复制@api.route('/api/zhihu/hot/', methods=['GET', 'POST'])
@token.tokenRequired
def zhihu_api_data():
pass
毫无疑问,这个功能交给装饰器真是再好不过了
代码语言:javascript复制def tokenRequired(f):
@wraps(f)
def decorated_function(*args, **kwargs):
pass
return decorated_function
下面的工作就是编写 decorated_function 函数的内容了,只需要加上我们需要的判断即可
代码语言:javascript复制if request.method == 'POST':
post_data = json.loads(request.data)
if 'token' in post_data and 'secret' in post_data and post_data['secret'] == '周萝卜真帅':
token = post_data['token']
check_result = check_token(token)
if check_result is True:
return f(*args, **kwargs)
else:
return jsonify(check_result), 401
return jsonify({'code': 422, 'message': '按套路出牌啊'}), 422
当请求方法是 POST 时,如果 token 字段不在请求体内或者请求体的 secret 字段没有按照套路出牌的话,都会返回错误响应的(这里请牢记暗号啊,夸我就对了!)
接下来我们再看看 check_token 函数,这就是具体的校验 token 的方法了
代码语言:javascript复制def check_token(token):
token_list = []
if rd.keys("token*"):
for t in rd.keys("token*"):
token_list.append(rd.get(t))
if token in token_list:
return {'code': 401, 'message': 'token is blocked'}, 401
validator = validateToken(token)
if validator['code'] != 200:
if validator['message'] == 'toekn expired':
return validator
else:
return validator
elif validator['code'] == 200:
return True
留用了 block token 的功能,以便后面使用。而 validateToken 函数就是调用 loads 方法解析加密后的 token。
功能增强之频率限制
所谓的频率限制,就是在指定的时间之内,访问请求的次数不能超过多少次。我这里设置的是一分钟之内,访问次数不能超过20次
代码语言:javascript复制REQUEST_NUM = 20
为了实现这个功能,我们需要用到 Flask 程序的全局请求钩子 before_app_request。该钩子的作用就是在任何请求发生之前,都会先调用该函数。这样我们就可以添加自己的判断逻辑,增加访问频率限制
代码语言:javascript复制@main.before_app_request
def before_request():
remote_add = request.remote_addr
rd_add = rd.get('access_num_%s' % remote_add)
if rd_add:
if int(rd_add) <= Config.REQUEST_NUM:
rd.incr('access_num_%s' % remote_add)
else:
return jsonify({'code': 422, 'message': '访问太频繁啦!'}), 422
else:
rd.set('access_num_%s' % remote_add, 1, ex=60)
每个 IP 的访问频率都存储在 redis 中,且该 redis key 的过期时间为60秒。当然这种限制属于防君子不防小人的做法,为什么这么说呢,因为如果你想突破这种入门级的限制,实在是太 easy 啦,而且使用手机4G网络的请求,IP 地址还会不停变化,太楠啦!
功能增强之高频词汇
在上一次的文章中,我们在前端(小程序端)只展示了知乎热点随着时间的走势情况,今天再加上每个热点的回答中的高频词汇,通过 jieba 来分词,还是很容易实现的。
将获取到的回答内容分词并统计词频
代码语言:javascript复制def cut_word(word):
word = re.sub('[a-zA-Z0-9]', '', word)
empty_str = ' '
with open(stopwords_path, encoding='utf-8') as f:
stop_words = f.read()
stop_words = stop_words empty_str
counts = {}
txt = jieba.lcut(word)
for w in txt:
if w not in stop_words:
counts[w] = counts.get(w, 0) 1
sort_counts = sorted(counts.items(), key=lambda item: item[1], reverse=True)
return sort_counts[:20]
在这里我们去掉了英文和数字,并且返回了词频前20的数据
然后我们修改视图函数 zhihu_api_detail
代码语言:javascript复制@api.route('/api/zhihu/detail/<id>/', methods=['GET', 'POST'])
@token.tokenRequired
def zhihu_api_detail(id):
zhihu_detail = zhihudetail(id)
redis_word = rd.get('wordcloud_%s' %id)
redis_content = rd.get('content_%s' % id)
if redis_word:
count_list = json.loads(redis_word)
content_list = json.loads(redis_content)
else:
count_list = []
count_word, content_list = zhihucontent(id) # 获取回答的词频数据和回答内容
for count in count_word:
count_list.append({'name': count[0], 'textSize': count[1]})
rd.set('wordcloud_%s' %id, json.dumps(count_list), ex=604800)
rd.set('content_%s' %id, json.dumps(content_list), ex=604800)
if count_list[0]['textSize'] < 10:
for i in count_list:
i['textSize'] = i['textSize']*10
elif count_list[0]['textSize'] > 200:
for i in count_list:
i['textSize'] = i['textSize']/10
return jsonify({'code': 0, 'data': zhihu_detail, 'count_word': count_list, 'content': content_list}), 200
因为每次使用 jieba 分词时还是比较耗费时间的,所以这里把处理好的数据保存到 redis 中,下次再请求时直接拿数据即可。
现在我们的详情页面展示如下
部署 API
最后我们把已经完成的代码部署到云服务器上,使用的还是那套 Nginx Gunicorn Flask MySQL
配置详情
Nginx 配置
代码语言:javascript复制server {
gzip on;
listen 443;
server_name www.luobodazahui.top;
ssl on;
root /home/mini/mini/ ;
ssl_certificate cert/luobodazahui.top.crt;
ssl_certificate_key cert/luobodazahui.top.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:5002;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
index index.html index.htm;
}
proxy_set_header X-Real-IP $remote_addr;
}
server {
listen 80;
server_name luobodazahui.top;
rewrite ^(.*)$ https://$host$1 permanent;
}
因为 API 后面想给小程序使用,所以应用了 域名 HTTPS
Gunicorn 配置
代码语言:javascript复制#from gevent import monkey
#monkey.patch_all()
import multiprocessing
#debug = True
loglevel = 'debug'
bind = '127.0.0.1:5002'
#bind = '0.0.0.0:5000'
#pidfile = 'pid/gunicorn.pid'
accesslog = '/home/mini/mini/log/ser_access.log'
errorlog = '/home/mini/mini/log/ser_error.log'
workers = 1
#workers = multiprocessing.cpu_count() * 2 1
worker_class = 'sync'
#reload = True
同样是比较简单的配置,打印了访问和错误日志,还启用了适量的 workers。
启动脚本 run.sh
代码语言:javascript复制/root/miniconda3/bin/gunicorn -D -c /home/mini/mini/gunicorn manage:app
停止脚本 stop.sh
代码语言:javascript复制kill -9 $(ps -ef | grep '/home/mini/mini/gunicorn' | grep -v grep | awk '{print $2}') 2>&1 >/dev/null;echo 0
API 信息
我们来看下当前提供的 API 信息
API地址 | 请求参数 | 支持方法 |
---|---|---|
https://www.luobodazahui.top/api/auth/token/ | table1 | POST/GET |
https://www.luobodazahui.top/api/zhihu/hot/ | table2 | POST/GET |
https://www.luobodazahui.top/api/zhihu/detail// | table3 | POST/GET |
table1
代码语言:javascript复制{
"username": "admin",
"pwd": "admin"
}
请求示例
table2
代码语言:javascript复制{
"token":"eyJhbGciOiJIUzUxMiIsImlhdCI6MTU3NzI0NDE4MywiZXhwIjoxNTc3MjQ1OTgzfQ.eyJ1c2VyaWQiOjEsImlhdCI6MTU3NzI0NDE4My4zMjcwNjY0fQ.FptYNm0KnA8b4G_zcRJn9POrOgkiZxpvfBbzQqxoTTt7q96WeMo7Y6xCLL_oS4ksBP8jMztqopDRRqScXPKowg",
"secret":"周萝卜真帅"}
请求示例
table3
代码语言:javascript复制{
"token":"eyJhbGciOiJIUzUxMiIsImlhdCI6MTU3NzI0NDE4MywiZXhwIjoxNTc3MjQ1OTgzfQ.eyJ1c2VyaWQiOjEsImlhdCI6MTU3NzI0NDE4My4zMjcwNjY0fQ.FptYNm0KnA8b4G_zcRJn9POrOgkiZxpvfBbzQqxoTTt7q96WeMo7Y6xCLL_oS4ksBP8jMztqopDRRqScXPKowg",
"secret":"周萝卜真帅"}
请求示例
未来优化
- 完善日志:当前只在定时任务当中加了日志,其余功能都未打印日志,后续把日志优化进来,方便问题定位
- 接口完善:当前接口返回数据庞杂,后续将接口拆分,增加更多参数,比如按照时间请求等
- 其他数据:后续增加微博、金融,票房等相关数据接口和展示
最后给出代码地址:https://github.com/zhouwei713/Mini_Flask