最近要实现这样一个功能:某个 cgi 处理会很耗时,需要把处理的结果实时的反馈给前端,而不能等到后台全完成了再咔一下全扔前端,那样的用户体验谁都没法接受。
web 框架选的 flask,这个比较轻量级,看了下官方文档,恰好有个叫 Streaming from Templates 的功能:
http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates
可以满足需求,它以 generate yield 为基础,流式的返回数据到前端。看了下官方的例子貌似很简单,一笔带过,我又搜了下 stackoverflow,上面有个老外给了个更加详尽的例子:Streaming data with Python and Flask
http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flask
文中的答案没有前后端的数据交互过程,那我就根据自己的需求加个 http 的交互过程了:
@app.route('/username', methods=['GET', 'POST']) def index(): req =request print req print "111------------" + req.method + "\n" def ggg1(req): print req # the req not my pass into the req.... print "444------------" + req.method + "\n" if req.method == 'POST': if request.form['username']: urlList = request.form['username'].splitlines() i = 0 for url in urlList(): i += 1 resultStr = url print i, resultStr yield i, resultStr print req print "222------------" + req.method + "\n" return Response(stream_template('index.html', data=ggg1(req)))
好吧,这么一加,噩梦就开始了。。。奇葩的问题出现了:
要么第 5 行和第 8 行不等,要么就是第 9 行报错:
if request.method == 'POST': # RuntimeError: working outside of request context
继续在 stackoverflow 上搜索,发现有人遇到了同样的问题,得到的建议是在调用前声明一个 request 上下文:
with app.test_request_context('/username', method='GET'): index()
折腾了老半天,还是依旧报错:RuntimeError: working outside of request context
看起来似乎是在进入迭代器以前,原本的 request 的生命周期就已经结束了,因此就没办法再调用了。
那么要解决就有 2 种办法了:
(1)在进入 generationFunc 前将请求复制一份保存下来以供 generationFunc 调用。
(2)利用 app.test_request_context 创建的是一个全新的 request,将数据传给 generationFunc 使用。
以上这两种办法都曾试过,但是由于理解上的偏差,导致一直未能成功。后来经过 坚实 同学的指点,才明白个中缘由,问题得以解决。
将请求复制下来但不能直接 req = request 这种形式,这只是给 request 取了个别名,它们是共享引用。正确的代码如下:
from flask.ctx import _request_ctx_stack global new_request @app.route('/') @app.route('/demo', methods=['POST']) def index(): ctx = _request_ctx_stack.top.copy() new_request = ctx.request def generateFunc(): if new_request.method == 'POST': if new_request.form['digitValue']: num = int(new_request.form['digitValue']) i = 0 for n in xrange(num): i += 1 print "%s:\t%s" % (i, n) yield i, n return Response(stream_template('index.html', data=generateFunc()))
PS: 其实像 _request_ctx_stack 这种以下划线开头的变量属于私有变量,外部是不应该调用的,不过坚实同学暂时也没有找到其他能正式调用到它的方法 ,就先这么用着吧。
上面的这种写法:with app.test_request_context('/username', method='GET'):
之所以不可以是因为 app.test_request_context 创建的是一个全新的 request,它包含的 url, method, headers, form 值都是要在创建时自定义的,它不会把原来的 request 里的数据带进来,需要自己传进去,类似这样:
with app.test_request_context('/demo', method='POST', data=request.form) as new_context: def generateFunc():
PS: test_request_context 应该是做单元测试用的,用来模仿用户发起的 HTTP 请求。
它做的事,和你通过浏览器提交一个表单或访问某个网页是差不多的。
例如你传给它 url='xxx'、method='post' 等等参数就是告诉它:向 xxx 发起一个 http 请求
这是官方宣称在 1.0 中实现的一个新特性,http://flask.pocoo.org/docs/api/#flask.copy_current_request_context 看说明应该可以更加优雅的解决上述问题,
但是试了下貌似不行,可能是组件间的兼容性问题。
New in version 0.9.
Note that when you stream data, the request context is already gone the moment the function executes. Flask 0.9 provides you with a helper that can keep the request context around during the execution of the generator:
from flask import stream_with_context, request, Response @app.route('/stream') def streamed_response(): def generate(): yield 'Hello ' yield request.args['name'] yield '!' return Response(stream_with_context(generate()))
Without the stream_with_context() function you would get a RuntimeError at that point.
REF:
http://stackoverflow.com/questions/19755557/streaming-data-with-python-and-flask-raise-runtimeerror-working-outside-of-requ/20189866?noredirect=1#20189866
(1)flask.request 和 streaming templates 兼容性不是很好,应该尽量不在 streaming templates 里调用 request,
把需要的值提前准备好,然后再传到 templates 里。这里也有人遇到同样的问题:
http://flask.pocoo.org/mailinglist/archive/2012/4/1/jinja2-stream-doesn-t-work/#8afda9ecd9682b16e8198a2f34e336fb
用 copy_current_request_context 没有效果应该也是上面这个原因。
(2)在文档语焉不详,同时 google 不到答案的时候,读源码或许是最后的选择,这也是一种能力吧。。。 - _ -
http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flask
http://flask.pocoo.org/docs/patterns/streaming/
http://stackoverflow.com/questions/8224333/scrolling-log-file-tail-f-animation-using-javascript
http://jsfiddle.net/manuel/zejCD/1/
附坚实同学的 github 与 sf 地址:
https://github.com/anjianshi
http://segmentfault.com/u/anjianshi
# -*- coding: utf-8 -*- import sys reload(sys) sys.setdefaultencoding('utf-8') from flask import Flask, request, Response app = Flask(__name__) def stream_template(template_name, **context): # http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates app.update_template_context(context) t = app.jinja_env.get_template(template_name) rv = t.stream(context) # uncomment if you don't need immediate reaction ##rv.enable_buffering(5) return rv @app.route('/') @app.route('/demo', methods=['POST']) def index(): with app.test_request_context('/demo', method='POST', data=request.form) as new_context: def generateFunc(): new_request = new_context.request if new_request.method == 'POST': if new_request.form['digitValue']: num = int(new_request.form['digitValue']) i = 0 for n in xrange(num): i += 1 print "%s:\t%s" % (i, n) yield i, n return Response(stream_template('index.html', data=generateFunc())) if __name__ == "__main__": app.run(host='localhost', port=8888, debug=True)
<!DOCTYPE html> <html> <head> <title>Bootstrap 101 Template</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- Bootstrap --> <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- WARNING: Respond.js doesn't work if you view the page via file:// --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script> <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script> <![endif]--> </head> <body> <style> #data { border: 1px solid blue; height: 500px; width: 500px; overflow: hidden; } </style> <script src="http://code.jquery.com/jquery-latest.js"></script> <script> function tailScroll() { var height = $("#data").get(0).scrollHeight; $("#data").animate({ scrollTop: height }, 5); } </script> <form role="form" action="/demo" method="POST"> <textarea class="form-control" rows="1" name="digitValue"></textarea> <button type="submit" class="btn btn-default">Submit</button> </form> <div id="data" style="position:relative;height:400px; overflow-x:auto;overflow-y:auto">nothing received yet</div> {% for i, resultStr in data: %} <script> $("<div />").text("{{ i }}:\t{{ resultStr }}").appendTo("#data") tailScroll(); </script> {% endfor %} <!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> <script src="https://code.jquery.com/jquery.js"></script> <!-- Include all compiled plugins (below), or include individual files as needed --> <script src="/static/dist/js/bootstrap.min.js"></script> </body> </html>
[1] 用Flask实现视频数据流传输
http://python.jobbole.com/80994/
https://github.com/miguelgrinberg/flask-video-streaming
[2] Video Streaming with Flask
http://blog.miguelgrinberg.com/post/video-streaming-with-flask
[3] Flask 的 Context 机制
https://blog.tonyseek.com/post/the-context-mechanism-of-flask/