【问题标题】:Django: Subprocess Continuous Output To HTML ViewDjango:子进程连续输出到 HTML 视图
【发布时间】:2020-11-12 02:19:34
【问题描述】:

我需要在我的 Django 应用程序中使用 HTML 网页来加载并在可滚动框中显示脚本的 连续 输出。这可能吗?

我目前正在使用子进程来运行 Python 脚本,但 HTML 页面要到 脚本完成后才会加载(这可能需要大约 5 分钟)。我希望用户看到正在发生的事情,而不仅仅是一个旋转的圆圈。

我已经卸载了文本中带有“\n”的脚本的完整输出;如果可能的话,我希望它输出每个新行。

我的代码如下:

Views.py:

def projectprogress(request):
    GenerateProjectConfig(request)
    home = os.getcwd()
    project_id = request.session['projectname']
    staging_folder = home + "/staging/" + project_id + "/"
    output = ""
    os.chdir(staging_folder)
    script = home + '/webscripts/terraformdeploy.py'
    try:
        output = subprocess.check_output(['python', script], shell=True)
    except subprocess.CalledProcessError:
        exit_code, error_msg = output.returncode, output.output
    os.chdir(home)
    return render(request, 'projectprogress.html', locals())

projectprogress.html:

<style>
  div.ex1 {
  background-color: black;
  width: 900px;
  height: 500px;
  overflow: scroll;
  margin: 50px;
}
</style>

<body style="background-color: #565c60; font-family: Georgia, 'Times New Roman', Times, serif; color: white; margin:0"></body>
    <div class="ex1">
        {% if output %}<h3>{{ output }}</h3>{% endif %}
        {% if exit_code %}<h3> The command returned an error: {{ error_msg }}</h3>{% endif %}
    </div>
    <div class="container">
        <a class="button button--wide button--white" href="home.html" title="Home" style="color: white; margin: 60px;">
            <span class="button__inner">
          Home
        </span>
        </a>
    </div>
</body>
</html>

【问题讨论】:

  • 是的,有可能。我建议使用服务器端事件来执行此操作,因为您只需要一种通信方式,而 websockets 会引入新协议并且过于矫枉过正。这是我的一个答案,它概述了以最简单的形式制作 SSE stackoverflow.com/a/62077516/5180047
  • @NickBrady 这真的很有趣!我需要一个 ASGI 服务器,还是一个普通的 WSGI 服务器符合要求?
  • 也可以使用 REST API,每隔几秒从 HTTP 服务器请求有关命令状态的更新。很简单,但我不确定这样的事情是否实用。
  • 我没有,抱歉。 SEE 使用 HTTP 协议,因此您真正需要做的就是发送通用文本/流响应。我不会想太多关于 Django 或不是 Django 的事情。研究如何返回文本流,然后以与我的示例类似的方式进行。不要忘记在线程上运行它:)
  • @RobTheRobot16 很高兴听到这个消息!确保在此处发布您的解决方案作为答案;)似乎是一个受欢迎的问题。如果需要,请随时与我个人联系(可以在我的简历中找到一个简单的小网站)。

标签: python html django subprocess


【解决方案1】:

你想要的是 websockets,或者 Django 中已知的 Channels。

https://channels.readthedocs.io/en/latest/

这允许您将消息从后端发送到前端,而无需在前端拉消息或重新加载页面。

值得一提的是,您还可以将输出流式传输到多个客户端,并将命令发送回您的后端。

为您的代码量身定制的方法

请注意,这是未经测试的,因为我无权访问您的代码,因此您可能需要进行一些小的调整,但我相信提供的代码应该能说明这个概念。

Settings.py

INSTALLED_APPS = (
#Other installed Apps
       'Channels',
)
CHANNEL_LAYERS = {
      "default": {
          "BACKEND": "asgiref.inmemory.ChannelLayer",
            "ROUTING": "django_channels.routing.channel_routing",
      },
}

routing.py(将文件添加到与 settings.py 相同的文件夹中)

from django_channels_app.consumers import message_ws, listener_add, listener_discconect

channel_routing = [
      route("websocket.receive", message_ws),
      route("websocket.disconnect", listener_discconect),
      route("websocket.connect", listener_add),
]

在您的模块中:

import threading
from channels import Group

class PreserializeThread(threading.Thread):
    def __init__(self, request, *args, **kwargs):
        self.request = request
        super(PreserializeThread, self).__init__(*args, **kwargs)

    def run(self):
        GenerateProjectConfig(request)
        home = os.getcwd()
        project_id = request.session['projectname']
        staging_folder = home + "/staging/" + project_id + "/"
        output = ""
        os.chdir(staging_folder)
        script = home + '/webscripts/terraformdeploy.py'
        try:
            output = subprocess.check_output(['python', script], shell=True)
            Group("django_channels_group").send({
                "text": output,
            })

            # NOTICE THIS WILL BLOCK; 
            # You could try the following, untested snippet


#    proc = subprocess.Popen(['python', script], shell=True, #stdout=subprocess.PIPE)
#    
#    line = proc.stdout.readline()
#    while line:
#        line = proc.stdout.readline()
#        Group("django_channels_group").send({
#                        "text": line,
#                    })
#    Group("django_channels_group").send({
#        "text": "Finished",
#    })
        except subprocess.CalledProcessError:
            exit_code, error_msg = (
                output.returncode,output.output)
        os.chdir(home)

def listener_add(message):
    Group("django_channels_group").add(
        message.reply_channel)

def listener_discconect(message):
    Group("django_channels_group").discard(
        message.reply_channel)

def message_ws(message):
    Group("django_channels_group").send({
          "text": "My group message",
     })

def projectprogress(request):
    ProgressThread(request).start()
    return render(request, 'projectprogress.html', locals())

html

<style>
  div.ex1 {
  background-color: black;
  width: 900px;
  height: 500px;
  overflow: scroll;
  margin: 50px;
}
</style>

<body style="background-color: #565c60; font-family: Georgia, 'Times New Roman', Times, serif; color: white; margin:0"></body>
    <div id="output">
        
    </div>
    <div class="container">
        <a class="button button--wide button--white" href="home.html" title="Home" style="color: white; margin: 60px;">
            <span class="button__inner">
          Home
        </span>
        </a>
    </div>
</body>
</html>

<script>
socket = new WebSocket("ws://127.0.0.1:8000/"); #Or your server IP address
socket.onmessage = function(e) {
    const data = JSON.parse(e.data);
    document.querySelector('#ouput').value += (data.message + '\n');
}
socket.onopen = function() {
    socket.send("Test message");
}
</script>

更通用的答案

后端:

聊天/consumers.py:

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            selfreturn render(request, 'projectprogress.html', locals()).channel_name
        )

    def send_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))

mysite/settings.py:

# Channels
ASGI_APPLICATION = 'mysite.routing.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

mysite/routing.py:

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

聊天/routing.py:

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]

前端:

<script>
const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/chat/'
            + roomName
            + '/'
        );

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };
</script>

【讨论】:

  • 谢谢;考虑到我对 Django 和 Channels 的使用完全陌生,我将不得不对此进行一些研究。你知道我应该把 subprocess 命令放在哪里吗?在某处的 consumer.py 文件中?
  • @RobTheRobot16 很抱歉没有早点回复您的评论,我会更新我的答案以说明如何做到这一点。
  • @RobTheRobot16 我已经更新了我的答案,如果您已经看到更新,您可能需要再次检查,因为我纠正了一些错别字。
  • 添加了一条关于使其成为流输出而不是等待它完成的评论。
  • 谢谢@alexisdevarennes!我不得不更新我的设置以包含“ASGI_APPLICATION = 'django_forms.routing.application'”(根据stackoverflow.com/questions/58841118/…),但我现在对ProtocolTypeRouter 在routing.py 文件中发挥作用的位置有点困惑你的具体例子。在通用答案中,您使用 URL 模式,但在更具体的答案中,您只需列出路线。你能解释一下区别吗?
【解决方案2】:

您可以使用StreamingHttpResponsePopen 来简化您的任务:

def test_iterator():
    from subprocess import Popen, PIPE, CalledProcessError

    with Popen(['ping', 'localhost'], stdout=PIPE, bufsize=1, universal_newlines=True) as p:
        for line in p.stdout:
            yield(line + '<br>') # process line here

    if p.returncode != 0:
        raise CalledProcessError(p.returncode, p.args)

def busy_view(request):
    from django.http import StreamingHttpResponse
    return StreamingHttpResponse(test_iterator())

StreamingHttpResponse 需要一个迭代器作为其参数。迭代器函数是具有yield 表达式(或生成器表达式)的函数,其返回值是生成器对象(迭代器)。

在这个例子中,我只是简单地回显 ping 命令来证明它有效。

用列表替换['ping', 'localhost'](如果您将参数传递给命令,则它必须是列表 - 在本例中为localhost)。你原来的['python', script] 应该可以工作。

如果您想了解更多关于生成器的信息,我会推荐 Trey Hunner 的 talk,并且强烈建议您阅读 Fluent Python 书的第 14 章。两者都是惊人的来源。

免责声明:

性能考虑

Django 专为短期请求而设计。流式响应将 在整个响应期间绑定一个工作进程。这可能 导致性能不佳。

一般来说,您应该在外部执行昂贵的任务 请求-响应循环,而不是诉诸流式响应。

【讨论】:

    猜你喜欢
    • 2022-11-05
    • 2017-06-29
    • 1970-01-01
    • 2011-06-19
    • 1970-01-01
    • 1970-01-01
    • 2011-08-29
    • 2021-11-06
    • 1970-01-01
    相关资源
    最近更新 更多