flask 流式响应 RuntimeError: working outside of request context

1、问题

最近要实现这样一个功能:某个 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 使用。

以上这两种办法都曾试过,但是由于理解上的偏差,导致一直未能成功。后来经过 坚实 同学的指点,才明白个中缘由,问题得以解决。

2、解决方案

(1)复制 request

将请求复制下来但不能直接 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 这种以下划线开头的变量属于私有变量,外部是不应该调用的,不过坚实同学暂时也没有找到其他能正式调用到它的方法 ,就先这么用着吧。

(2)构造全新 request

上面的这种写法: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 请求

(3)关于 @copy_current_request_context

这是官方宣称在 1.0 中实现的一个新特性,http://flask.pocoo.org/docs/api/#flask.copy_current_request_context 看说明应该可以更加优雅的解决上述问题,

但是试了下貌似不行,可能是组件间的兼容性问题。

(4)关于 Streaming with 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

3、结论

(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 不到答案的时候,读源码或许是最后的选择,这也是一种能力吧。。。 - _ -

4、Refer:

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

5、最后附上完整的测试源码:

# -*- 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>

6、推荐阅读:

[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/

[4] flask 源码解析:session

http://python.jobbole.com/87450/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

setup.py即将下岗,PEP 518 说明继任者

2557
来自专栏数据之美

玩转 SHELL 脚本之:Shell 命令 Buffer 知多少?

1、问题: 下午有同学问了这么一个问题: tail -n +$(tail -n1 /root/tmp/n) -F /root/tmp/ip.txt 2>...

3566
来自专栏崔庆才的专栏

对你没有看错!不到 10 行代码完成抖音热门视频的爬取!

最近研究了一下抖音的爬虫,目前实现了热门话题和热门音乐下面所有相关视频的爬取,并且我已经将该爬虫打包成了一个 Python 库并发布,名称就叫做 douyin,...

2373
来自专栏小怪聊职场

HTTP|GET 和 POST 区别?网上多数答案都是错的!

32710
来自专栏阮一峰的网络日志

搭建一个免费的,无限流量的Blog----github Pages和Jekyll入门

喜欢写Blog的人,会经历三个阶段。   第一阶段,刚接触Blog,觉得很新鲜,试着选择一个免费空间来写。   第二阶段,发现免费空间限制太多,就自己购买域...

4936
来自专栏owent

关于BUS通信系统的一些思考(一)

如何保证一个进程或线程能安全稳定地把一段消息发送到另一个进程和线程,甚至是另一台机器的进程或线程,再或是要通过代理转发到另一个进程或线程,一直是一个比较麻烦的问...

1281
来自专栏木宛城主

SharePoint 2010、2013多个域之间互信(Domain Trust)的设计与实施

在现实的业务场景中,有时为了更好的管理域用户和服务。我们往往会创建多个分散式的域,每个域的Administrator专注于维护特定域中的用户和资源,Admin...

2529
来自专栏phodal

编辑-发布-开发分离:git作为NoSQL数据库

动态网页是下一个要解决的难题。我们从数据库中读取数据,再用动态去渲染出一个静态页面,并且缓存服务器来缓存这个页面。既然我们都可以用Varnish、Squid这样...

20710
来自专栏华章科技

Python 开发者的 6 个必备库

无论你是正在使用 Python 进行快速开发,还是在为 Python 桌面应用制作原生 UI ,或者是在优化现有的 Python 代码,以下这些 Python ...

692
来自专栏一名合格java开发的自我修养

IT咨询顾问:一次吐血的项目救火

  年后的一个合作公司上线了一个子业务系统,对接公司内部的单点系统。我收到该公司的技术咨询:项目启动后没有规律的突然无法登录了,重新启动后,登录一断时间后又无法...

952

扫码关注云+社区

领取腾讯云代金券