1. 程式人生 > 其它 >flask 流式響應 RuntimeError: working outside of request context

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/