flask 流式響應 RuntimeError: working outside of request context

一、問題

最近要實現這樣一個功能:某個 cgi 處理會很耗時,須要把處理的結果實時的反饋給前端,而不能等到後臺全完成了再咔一下全扔前端,那樣的用戶體驗誰都無法接受。javascript

web 框架選的 flask,這個比較輕量級,看了下官方文檔,剛好有個叫 Streaming from Templates 的功能:html

http://flask.pocoo.org/docs/patterns/streaming/#streaming-from-templates前端

能夠知足需求,它以 generate yield 爲基礎,流式的返回數據到前端。看了下官方的例子貌似很簡單,一筆帶過,我又搜了下 stackoverflow,上面有個老外給了個更加詳盡的例子:Streaming data with Python and Flaskhtml5

http://stackoverflow.com/questions/13386681/streaming-data-with-python-and-flaskjava

文中的答案沒有先後端的數據交互過程,那我就根據本身的需求加個 http 的交互過程了:python

@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)))

好吧,這麼一加,噩夢就開始了。。。奇葩的問題出現了:react

要麼第 5 行和第 8 行不等,要麼就是第 9 行報錯:jquery

 if request.method == 'POST':  # RuntimeError: working outside of request contextgit

繼續在 stackoverflow 上搜索,發現有人遇到了一樣的問題,獲得的建議是在調用前聲明一個 request 上下文:github

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 使用。

以上這兩種辦法都曾試過,可是因爲理解上的誤差,致使一直未能成功。後來通過 堅實 同窗的指點,才明白箇中原因,問題得以解決。

二、解決方案

(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

三、結論

(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 不到答案的時候,讀源碼或許是最後的選擇,這也是一種能力吧。。。 - _ -

四、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

五、最後附上完整的測試源碼:

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

[4] flask 源碼解析:session

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

相關文章
相關標籤/搜索